@@ -58,6 +58,7 @@ const mocks = vi.hoisted(() => {
5858 track : lifecycleTrack ,
5959 } ) ) ,
6060 resolveKimiHome : vi . fn ( ( homeDir ?: string ) => homeDir ?? '/tmp/kimi-code-test-home' ) ,
61+ flushDiagnosticLogsSync : vi . fn ( ) ,
6162 harnessCreatesDeviceIdOnConstruction : false ,
6263 execSync : vi . fn ( ) ,
6364 TuiConfigParseError,
@@ -69,6 +70,7 @@ vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => {
6970 return {
7071 ...actual ,
7172 resolveKimiHome : mocks . resolveKimiHome ,
73+ flushDiagnosticLogsSync : mocks . flushDiagnosticLogsSync ,
7274 createKimiHarness : ( ...args : unknown [ ] ) => {
7375 const options = args [ 0 ] as { readonly homeDir ?: string } | undefined ;
7476 const homeDir = options ?. homeDir ?? '/tmp/kimi-code-test-home' ;
@@ -508,6 +510,101 @@ describe('runShell', () => {
508510 } ) ;
509511 } ) ;
510512
513+ it ( 'flushes diagnostic logs synchronously before exiting on a runtime crash' , async ( ) => {
514+ mocks . loadTuiConfig . mockResolvedValue ( {
515+ theme : 'dark' ,
516+ editorCommand : null ,
517+ notifications : { enabled : true , condition : 'unfocused' } ,
518+ } ) ;
519+ mocks . tuiStart . mockResolvedValue ( undefined ) ;
520+
521+ const processOnSpy = vi . spyOn ( process , 'on' ) ;
522+ const stdout = captureProcessWrite ( 'stdout' ) ;
523+ const exitSpy = mockProcessExit ( ) ;
524+
525+ try {
526+ await runShell (
527+ {
528+ session : undefined ,
529+ continue : false ,
530+ yolo : false ,
531+ auto : false ,
532+ plan : false ,
533+ model : undefined ,
534+ outputFormat : undefined ,
535+ prompt : undefined ,
536+ skillsDirs : [ ] ,
537+ } ,
538+ '1.2.3-test' ,
539+ ) ;
540+
541+ const handler = processOnSpy . mock . calls . find (
542+ ( [ event ] ) => event === 'uncaughtException' ,
543+ ) ?. [ 1 ] as ( ( error : unknown ) => void ) | undefined ;
544+ expect ( handler ) . toBeDefined ( ) ;
545+
546+ // The async log sink cannot flush before process.exit() runs, so the
547+ // crash handler must force a synchronous flush or the crash reason is
548+ // lost (regression: uncaughtException logs never reached disk).
549+ expect ( ( ) => handler ?.( new Error ( 'boom' ) ) ) . toThrow ( ExitCalled ) ;
550+ expect ( mocks . flushDiagnosticLogsSync ) . toHaveBeenCalledOnce ( ) ;
551+ expect ( exitSpy ) . toHaveBeenCalledWith ( 1 ) ;
552+ expect ( mocks . flushDiagnosticLogsSync . mock . invocationCallOrder [ 0 ] ! ) . toBeLessThan (
553+ exitSpy . mock . invocationCallOrder [ 0 ] ! ,
554+ ) ;
555+ } finally {
556+ processOnSpy . mockRestore ( ) ;
557+ exitSpy . mockRestore ( ) ;
558+ stdout . restore ( ) ;
559+ }
560+ } ) ;
561+
562+ it ( 'flushes diagnostic logs synchronously before exiting on an unhandled rejection' , async ( ) => {
563+ mocks . loadTuiConfig . mockResolvedValue ( {
564+ theme : 'dark' ,
565+ editorCommand : null ,
566+ notifications : { enabled : true , condition : 'unfocused' } ,
567+ } ) ;
568+ mocks . tuiStart . mockResolvedValue ( undefined ) ;
569+
570+ const processOnSpy = vi . spyOn ( process , 'on' ) ;
571+ const stdout = captureProcessWrite ( 'stdout' ) ;
572+ const exitSpy = mockProcessExit ( ) ;
573+
574+ try {
575+ await runShell (
576+ {
577+ session : undefined ,
578+ continue : false ,
579+ yolo : false ,
580+ auto : false ,
581+ plan : false ,
582+ model : undefined ,
583+ outputFormat : undefined ,
584+ prompt : undefined ,
585+ skillsDirs : [ ] ,
586+ } ,
587+ '1.2.3-test' ,
588+ ) ;
589+
590+ const handler = processOnSpy . mock . calls . find (
591+ ( [ event ] ) => event === 'unhandledRejection' ,
592+ ) ?. [ 1 ] as ( ( reason : unknown ) => void ) | undefined ;
593+ expect ( handler ) . toBeDefined ( ) ;
594+
595+ expect ( ( ) => handler ?.( new Error ( 'boom' ) ) ) . toThrow ( ExitCalled ) ;
596+ expect ( mocks . flushDiagnosticLogsSync ) . toHaveBeenCalledOnce ( ) ;
597+ expect ( exitSpy ) . toHaveBeenCalledWith ( 1 ) ;
598+ expect ( mocks . flushDiagnosticLogsSync . mock . invocationCallOrder [ 0 ] ! ) . toBeLessThan (
599+ exitSpy . mock . invocationCallOrder [ 0 ] ! ,
600+ ) ;
601+ } finally {
602+ processOnSpy . mockRestore ( ) ;
603+ exitSpy . mockRestore ( ) ;
604+ stdout . restore ( ) ;
605+ }
606+ } ) ;
607+
511608 it ( 'closes the harness when TUI startup fails' , async ( ) => {
512609 mocks . loadTuiConfig . mockResolvedValue ( {
513610 theme : 'dark' ,
0 commit comments