@@ -36,6 +36,10 @@ vi.mock("path", () => ({
3636 basename : vi . fn ( ( path ) => path . split ( "/" ) . pop ( ) ) ,
3737 dirname : vi . fn ( ( path ) => path . split ( "/" ) . slice ( 0 , - 1 ) . join ( "/" ) || "/" ) ,
3838 join : ( ...args : string [ ] ) => args . join ( "/" ) ,
39+ // diagnosticsToProblemsString formats its output header via
40+ // path.relative(cwd, uri.fsPath).toPosix(); the object-with-toPosix shape
41+ // mirrors the repo's own diagnostics.spec.ts mock.
42+ relative : vi . fn ( ( cwd : string , p : string ) => ( { toPosix : ( ) => p . replace ( `${ cwd } /` , "" ) } ) ) ,
3943} ) )
4044
4145// Mock vscode
@@ -174,6 +178,8 @@ describe("DiffViewProvider", () => {
174178 } ) ,
175179 } ) ,
176180 } ,
181+ // L1: saveDirectly's fire-and-forget diagnostics tail emits via say().
182+ say : vi . fn ( ) . mockResolvedValue ( true ) ,
177183 }
178184
179185 diffViewProvider = new DiffViewProvider ( mockCwd , mockTask )
@@ -811,12 +817,17 @@ describe("DiffViewProvider", () => {
811817 { preview : false , preserveFocus : true } ,
812818 )
813819
814- // Verify diagnostics were checked after delay
820+ // L1: saveDirectly resolves before the fire-and-forget diagnostics
821+ // tail runs; flush one macrotask tick so the mocked delay (and the
822+ // post-write getDiagnostics) have been reached before asserting.
823+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) )
824+
825+ // Verify the tail applied the configured write delay
815826 expect ( mockDelay ) . toHaveBeenCalledWith ( 2000 )
816827 expect ( vscode . languages . getDiagnostics ) . toHaveBeenCalled ( )
817828
818- // Verify result
819- expect ( result . newProblemsMessage ) . toBe ( "" )
829+ // Verify result: L1 no longer returns a problems message
830+ expect ( result . newProblemsMessage ) . toBeUndefined ( )
820831 expect ( result . userEdits ) . toBeUndefined ( )
821832 expect ( result . finalContent ) . toBe ( "new content" )
822833 } )
@@ -847,6 +858,10 @@ describe("DiffViewProvider", () => {
847858 expect ( mockDelay ) . not . toHaveBeenCalled ( )
848859 // getDiagnostics is called once for pre-diagnostics, but not for post-diagnostics
849860 expect ( vscode . languages . getDiagnostics ) . toHaveBeenCalledTimes ( 1 )
861+
862+ // L1: no diagnostics tail is launched, so nothing is ever emitted
863+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) )
864+ expect ( mockTask . say ) . not . toHaveBeenCalled ( )
850865 } )
851866
852867 it ( "should handle negative delay values" , async ( ) => {
@@ -855,18 +870,129 @@ describe("DiffViewProvider", () => {
855870
856871 await diffViewProvider . saveDirectly ( "test.ts" , "new content" , true , true , - 500 )
857872
873+ // L1: the tail runs after resolve; flush one macrotask tick first.
874+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) )
875+
858876 // Verify delay was called with 0 (safe minimum)
859877 expect ( mockDelay ) . toHaveBeenCalledWith ( 0 )
860878 } )
861879
862880 it ( "should store results for formatFileWriteResponse" , async ( ) => {
863881 await diffViewProvider . saveDirectly ( "test.ts" , "new content" , true , true , 1000 )
864882
865- // Verify internal state was updated
866- expect ( ( diffViewProvider as any ) . newProblemsMessage ) . toBe ( "" )
867- expect ( ( diffViewProvider as any ) . userEdits ) . toBeUndefined ( )
868- expect ( ( diffViewProvider as any ) . relPath ) . toBe ( "test.ts" )
869- expect ( ( diffViewProvider as any ) . newContent ) . toBe ( "new content" )
883+ // Verify internal state was updated (L1: the problems message is no
884+ // longer stored; it is emitted asynchronously via say("error"))
885+ expect ( diffViewProvider [ "newProblemsMessage" ] ) . toBeUndefined ( )
886+ expect ( diffViewProvider [ "userEdits" ] ) . toBeUndefined ( )
887+ expect ( diffViewProvider [ "relPath" ] ) . toBe ( "test.ts" )
888+ expect ( diffViewProvider [ "newContent" ] ) . toBe ( "new content" )
889+ } )
890+
891+ it ( "resolves immediately and emits new problems via say('error') after the write delay" , async ( ) => {
892+ const mockDelay = vi . mocked ( delay )
893+ mockDelay . mockClear ( )
894+ vi . mocked ( vscode . languages . getDiagnostics ) . mockClear ( )
895+
896+ // Pre-write diagnostics are empty; the post-write snapshot (read by
897+ // the fire-and-forget tail) reports one new Error-severity problem.
898+ // vscode.workspace.fs.stat is an unimplemented vi.fn() mock, so
899+ // diagnosticsToProblemsString takes its "(unavailable)" fallback
900+ // branch and still formats the line.
901+ const newDiag : vscode . Diagnostic = {
902+ severity : vscode . DiagnosticSeverity . Error ,
903+ range : new vscode . Range ( 0 , 0 , 0 , 1 ) ,
904+ message : "boom" ,
905+ }
906+ const postDiagnostics : [ vscode . Uri , vscode . Diagnostic [ ] ] [ ] = [ [ makeUri ( `${ mockCwd } /test.ts` ) , [ newDiag ] ] ]
907+ vi . mocked ( vscode . languages . getDiagnostics ) . mockReturnValueOnce ( [ ] ) . mockReturnValue ( postDiagnostics )
908+
909+ const result = await diffViewProvider . saveDirectly ( "test.ts" , "new content" , true , true , 100 )
910+
911+ // L1: saveDirectly resolves before the tail emits anything.
912+ expect ( result . newProblemsMessage ) . toBeUndefined ( )
913+ expect ( mockTask . say ) . not . toHaveBeenCalled ( )
914+
915+ // Flush the fire-and-forget tail (the mocked delay resolves immediately).
916+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) )
917+
918+ expect ( mockDelay ) . toHaveBeenCalledWith ( 100 )
919+ expect ( mockTask . say ) . toHaveBeenCalledTimes ( 1 )
920+ // The existing "error" ClineSay type is used, with the new-problems text.
921+ expect ( mockTask . say ) . toHaveBeenCalledWith (
922+ "error" ,
923+ expect . stringContaining ( "New problems detected after saving file: test.ts" ) ,
924+ )
925+ expect ( mockTask . say . mock . calls [ 0 ] ?. [ 1 ] ) . toContain ( "boom" )
926+ } )
927+
928+ it ( "attributes only the saved file's new problems to the saved file" , async ( ) => {
929+ vi . mocked ( vscode . languages . getDiagnostics ) . mockClear ( )
930+
931+ // Multi-file write sequence: a later file's new error must not be
932+ // attributed to this tail's relPath by the workspace-wide snapshot.
933+ const ownDiag : vscode . Diagnostic = {
934+ severity : vscode . DiagnosticSeverity . Error ,
935+ range : new vscode . Range ( 0 , 0 , 0 , 1 ) ,
936+ message : "own-problem" ,
937+ }
938+ const otherDiag : vscode . Diagnostic = {
939+ severity : vscode . DiagnosticSeverity . Error ,
940+ range : new vscode . Range ( 0 , 0 , 0 , 1 ) ,
941+ message : "other-file-problem" ,
942+ }
943+ const postDiagnostics : [ vscode . Uri , vscode . Diagnostic [ ] ] [ ] = [
944+ [ makeUri ( `${ mockCwd } /test.ts` ) , [ ownDiag ] ] ,
945+ [ makeUri ( `${ mockCwd } /other.ts` ) , [ otherDiag ] ] ,
946+ ]
947+ vi . mocked ( vscode . languages . getDiagnostics ) . mockReturnValueOnce ( [ ] ) . mockReturnValue ( postDiagnostics )
948+
949+ await diffViewProvider . saveDirectly ( "test.ts" , "new content" , true , true , 100 )
950+
951+ // Flush the fire-and-forget tail.
952+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) )
953+
954+ expect ( mockTask . say ) . toHaveBeenCalledTimes ( 1 )
955+ expect ( mockTask . say . mock . calls [ 0 ] ?. [ 1 ] ) . toContain ( "own-problem" )
956+ expect ( mockTask . say . mock . calls [ 0 ] ?. [ 1 ] ) . not . toContain ( "other-file-problem" )
957+ } )
958+
959+ it ( "never calls say when there are no new problems" , async ( ) => {
960+ vi . mocked ( vscode . languages . getDiagnostics ) . mockClear ( )
961+ vi . mocked ( vscode . languages . getDiagnostics ) . mockReturnValue ( [ ] )
962+
963+ await diffViewProvider . saveDirectly ( "test.ts" , "new content" , true , true , 50 )
964+
965+ // Flush the fire-and-forget tail (pre/post snapshots are both empty,
966+ // so diagnosticsToProblemsString returns "" and nothing is emitted).
967+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) )
968+
969+ expect ( mockTask . say ) . not . toHaveBeenCalled ( )
970+ } )
971+
972+ it ( "logs a warning instead of an unhandled rejection when the post-save say() is aborted" , async ( ) => {
973+ const consoleWarnSpy = vi . spyOn ( console , "warn" ) . mockImplementation ( ( ) => { } )
974+ vi . mocked ( vscode . languages . getDiagnostics ) . mockClear ( )
975+
976+ // One new Error-severity problem so the tail reaches say().
977+ const newDiag : vscode . Diagnostic = {
978+ severity : vscode . DiagnosticSeverity . Error ,
979+ range : new vscode . Range ( 0 , 0 , 0 , 1 ) ,
980+ message : "boom" ,
981+ }
982+ const postDiagnostics : [ vscode . Uri , vscode . Diagnostic [ ] ] [ ] = [ [ makeUri ( `${ mockCwd } /test.ts` ) , [ newDiag ] ] ]
983+ vi . mocked ( vscode . languages . getDiagnostics ) . mockReturnValueOnce ( [ ] ) . mockReturnValue ( postDiagnostics )
984+
985+ // The task is aborted while the diagnostics tail is emitting: say() rejects.
986+ mockTask . say . mockRejectedValueOnce ( new Error ( "aborted" ) )
987+
988+ await diffViewProvider . saveDirectly ( "test.ts" , "new content" , true , true , 0 )
989+ // Flush the fire-and-forget tail.
990+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) )
991+
992+ // The method's outer catch swallows the rejection with a warning.
993+ expect ( consoleWarnSpy ) . toHaveBeenCalledWith ( expect . stringContaining ( "Post-save diagnostics emit failed:" ) )
994+
995+ consoleWarnSpy . mockRestore ( )
870996 } )
871997 } )
872998
0 commit comments