@@ -6107,6 +6107,7 @@ public async Task App_Startup_DefaultWorkspaceWithRestorableSubAgents_DismissesL
61076107 await using var viewModel = CreateTestMainWindowViewModel ( ) ;
61086108
61096109 var succeeded = await StartupSplashRunner . RunWithSplashDismissAsync (
6110+ loggerFactory : Microsoft . Extensions . Logging . Abstractions . NullLoggerFactory . Instance ,
61106111 initializeAsync : ( ) => viewModel . InitializeAsync ( ) ,
61116112 setStatus : _ => { } ,
61126113 onFaultDelay : ( ) => Task . CompletedTask ,
@@ -6127,6 +6128,7 @@ public async Task App_Startup_ViewModelInitializeAsyncFaults_LoadingWindowIsClos
61276128 var postInitializeRan = false ;
61286129
61296130 var succeeded = await StartupSplashRunner . RunWithSplashDismissAsync (
6131+ loggerFactory : Microsoft . Extensions . Logging . Abstractions . NullLoggerFactory . Instance ,
61306132 initializeAsync : ( ) => Task . FromException ( new InvalidOperationException ( "boom" ) ) ,
61316133 setStatus : _ => { } ,
61326134 onFaultDelay : ( ) => Task . CompletedTask ,
@@ -6150,6 +6152,7 @@ public async Task App_Startup_SubAgentRestoreThrows_DoesNotHangSplash()
61506152 var statusMessages = new List < string > ( ) ;
61516153
61526154 var succeeded = await StartupSplashRunner . RunWithSplashDismissAsync (
6155+ loggerFactory : Microsoft . Extensions . Logging . Abstractions . NullLoggerFactory . Instance ,
61536156 initializeAsync : ( ) => Task . FromException (
61546157 new InvalidOperationException ( "Agent definition does not specify a model." ) ) ,
61556158 setStatus : msg => statusMessages . Add ( msg ) ,
@@ -6163,6 +6166,121 @@ public async Task App_Startup_SubAgentRestoreThrows_DoesNotHangSplash()
61636166 Assert . Contains ( statusMessages , m => m . Contains ( "Agent definition does not specify a model." , StringComparison . Ordinal ) ) ;
61646167 }
61656168
6169+ // ── Issue #1294: startup connect failures must be written to the rolling log file ──
6170+
6171+ private sealed class RecordingStartupLoggerFactory : Microsoft . Extensions . Logging . ILoggerFactory
6172+ {
6173+ public List < TestLogger < StartupSplashRunner > . LogEntry > Entries { get ; } = [ ] ;
6174+ private readonly TestLogger < StartupSplashRunner > logger ;
6175+
6176+ public RecordingStartupLoggerFactory ( )
6177+ {
6178+ this . logger = new TestLogger < StartupSplashRunner > ( ) ;
6179+ }
6180+
6181+ public void AddProvider ( Microsoft . Extensions . Logging . ILoggerProvider provider ) { }
6182+ public Microsoft . Extensions . Logging . ILogger CreateLogger ( string categoryName ) => this . logger ;
6183+ public void Dispose ( ) { }
6184+
6185+ public IReadOnlyList < TestLogger < StartupSplashRunner > . LogEntry > Snapshot ( ) => this . logger . Entries ;
6186+ }
6187+
6188+ [ AvaloniaFact ( Timeout = 15_000 ) ]
6189+ public async Task StartupSplashRunner_WhenInitializeThrows_LogsError ( )
6190+ {
6191+ var factory = new RecordingStartupLoggerFactory ( ) ;
6192+ var boom = new InvalidOperationException ( "boom-1294" ) ;
6193+
6194+ await StartupSplashRunner . RunWithSplashDismissAsync (
6195+ loggerFactory : factory ,
6196+ initializeAsync : ( ) => Task . FromException ( boom ) ,
6197+ setStatus : _ => { } ,
6198+ onFaultDelay : ( ) => Task . CompletedTask ,
6199+ shutdown : ( ) => { } ,
6200+ postInitialize : ( ) => { } ,
6201+ closeSplash : ( ) => { } ) ;
6202+
6203+ var errors = factory . Snapshot ( )
6204+ . Where ( e => e . Level == Microsoft . Extensions . Logging . LogLevel . Error )
6205+ . ToList ( ) ;
6206+ Assert . Single ( errors ) ;
6207+ Assert . Same ( boom , errors [ 0 ] . Exception ) ;
6208+ Assert . Contains ( "Startup connect failed" , errors [ 0 ] . Message , StringComparison . Ordinal ) ;
6209+ }
6210+
6211+ [ AvaloniaFact ( Timeout = 15_000 ) ]
6212+ public async Task StartupSplashRunner_WhenInitializeThrows_SetsSplashStatus ( )
6213+ {
6214+ var factory = new RecordingStartupLoggerFactory ( ) ;
6215+ var statusMessages = new List < string > ( ) ;
6216+
6217+ await StartupSplashRunner . RunWithSplashDismissAsync (
6218+ loggerFactory : factory ,
6219+ initializeAsync : ( ) => Task . FromException ( new InvalidOperationException ( "boom-status" ) ) ,
6220+ setStatus : msg => statusMessages . Add ( msg ) ,
6221+ onFaultDelay : ( ) => Task . CompletedTask ,
6222+ shutdown : ( ) => { } ,
6223+ postInitialize : ( ) => { } ,
6224+ closeSplash : ( ) => { } ) ;
6225+
6226+ Assert . Contains ( "Failed to connect: boom-status" , statusMessages ) ;
6227+ }
6228+
6229+ [ AvaloniaFact ( Timeout = 15_000 ) ]
6230+ public async Task StartupSplashRunner_WhenInitializeSucceeds_DoesNotLogError ( )
6231+ {
6232+ var factory = new RecordingStartupLoggerFactory ( ) ;
6233+
6234+ await StartupSplashRunner . RunWithSplashDismissAsync (
6235+ loggerFactory : factory ,
6236+ initializeAsync : ( ) => Task . CompletedTask ,
6237+ setStatus : _ => { } ,
6238+ onFaultDelay : ( ) => Task . CompletedTask ,
6239+ shutdown : ( ) => { } ,
6240+ postInitialize : ( ) => { } ,
6241+ closeSplash : ( ) => { } ) ;
6242+
6243+ Assert . DoesNotContain ( factory . Snapshot ( ) , e => e . Level == Microsoft . Extensions . Logging . LogLevel . Error ) ;
6244+ }
6245+
6246+ [ AvaloniaFact ( Timeout = 15_000 ) ]
6247+ public async Task StartupSplashRunner_WhenInitializeThrows_LogsBeforeShutdown ( )
6248+ {
6249+ var factory = new RecordingStartupLoggerFactory ( ) ;
6250+ var events = new List < string > ( ) ;
6251+
6252+ // TestLogger records to factory.Snapshot() synchronously; we also snapshot ordering
6253+ // via events for the shutdown/onFaultDelay callbacks so we can assert LogError fired
6254+ // strictly before either.
6255+ await StartupSplashRunner . RunWithSplashDismissAsync (
6256+ loggerFactory : factory ,
6257+ initializeAsync : ( ) => Task . FromException ( new InvalidOperationException ( "order-check" ) ) ,
6258+ setStatus : _ => events . Add ( "setStatus" ) ,
6259+ onFaultDelay : ( ) =>
6260+ {
6261+ events . Add ( "onFaultDelay" ) ;
6262+ return Task . CompletedTask ;
6263+ } ,
6264+ shutdown : ( ) => events . Add ( "shutdown" ) ,
6265+ postInitialize : ( ) => events . Add ( "postInitialize" ) ,
6266+ closeSplash : ( ) => events . Add ( "closeSplash" ) ) ;
6267+
6268+ // Snapshot the error entry — the RecordingStartupLoggerFactory records synchronously
6269+ // inside LogError, so its presence at snapshot time means LogError has returned.
6270+ var errorEntry = factory . Snapshot ( )
6271+ . Single ( e => e . Level == Microsoft . Extensions . Logging . LogLevel . Error ) ;
6272+ Assert . Contains ( "Startup connect failed" , errorEntry . Message , StringComparison . Ordinal ) ;
6273+
6274+ // Ordering: shutdown and onFaultDelay both follow the LogError call, which happens
6275+ // before setStatus in the catch block. shutdown must not precede either.
6276+ var shutdownIndex = events . IndexOf ( "shutdown" ) ;
6277+ var onFaultDelayIndex = events . IndexOf ( "onFaultDelay" ) ;
6278+ var setStatusIndex = events . IndexOf ( "setStatus" ) ;
6279+ Assert . True ( setStatusIndex >= 0 ) ;
6280+ Assert . True ( onFaultDelayIndex > setStatusIndex , "onFaultDelay must run after setStatus (and after LogError)." ) ;
6281+ Assert . True ( shutdownIndex > onFaultDelayIndex , "shutdown must run after onFaultDelay (and after LogError)." ) ;
6282+ }
6283+
61666284 private static RepositorySource CreateInMemoryRepositorySource ( )
61676285 {
61686286 return new UnknownRepositorySource ( ) ;
0 commit comments