@@ -12,10 +12,11 @@ import { saveState, loadState, type RunState } from "../../../src/session/state.
1212
1313// End-to-end coverage for the run.json turn-boundary snapshot fix (CL-5534):
1414// createRunSink, saveState, and loadState run for real against a temp
15- // session directory — nothing mocked. This is what now covers
16- // isRunSnapshotTurnBoundary's observable effect, since that predicate was
17- // un-exported from src/tui/runner.ts as a testability-only surface with a
18- // single production caller.
15+ // session directory — nothing mocked. The snapshot cadence itself now lives
16+ // in createRunSink's onTurnBoundary callback (moved out of src/tui/runner.ts
17+ // so a future renderer swap cannot silently drop it again), so these tests
18+ // drive that callback exactly as production wiring does: nothing here calls
19+ // saveState directly from the turn loop, only from inside onTurnBoundary.
1920
2021const noopHookManager = { dispatchPostTurn : ( ) => undefined , getStatuses : ( ) => [ ] } ;
2122
@@ -48,18 +49,22 @@ describe("run.json turn-boundary snapshots — end to end", () => {
4849 const home = mkdtempSync ( join ( tmpdir ( ) , "corbits-run-state-home-" ) ) ;
4950 const sessionId = generateSessionId ( ) ;
5051 try {
51- const runSink = createRunSink ( { emitter : new EventEmitter ( ) , hookManager : noopHookManager } ) ;
52+ const writes : Promise < void > [ ] = [ ] ;
53+ const runSink = createRunSink ( {
54+ emitter : new EventEmitter ( ) ,
55+ hookManager : noopHookManager ,
56+ onTurnBoundary : ( ) => {
57+ writes . push ( saveState ( cwd , sessionId , baseState ( { status : "running" } , runSink . getTurnCount ( ) ) , home ) ) ;
58+ } ,
59+ } ) ;
5260
53- const observed : number [ ] = [ ] ;
5461 for ( let turn = 1 ; turn <= 4 ; turn ++ ) {
5562 runSink . sink ( inferenceDone ( ) ) ;
56- await saveState ( cwd , sessionId , baseState ( { status : "running" } , runSink . getTurnCount ( ) ) , home ) ;
57- const onDisk = await loadState ( cwd , sessionId , home ) ;
58- expect ( onDisk ) . not . toBeNull ( ) ;
59- observed . push ( onDisk ! . turnsUsed ) ;
6063 }
64+ await Promise . all ( writes ) ;
6165
62- expect ( observed ) . toEqual ( [ 1 , 2 , 3 , 4 ] ) ;
66+ const onDisk = await loadState ( cwd , sessionId , home ) ;
67+ expect ( onDisk ?. turnsUsed ) . toBe ( 4 ) ;
6368
6469 runSink . sink ( { type : "reactor.done" , data : { } } as unknown as ReactorEmittedEvent ) ;
6570 await saveState (
@@ -82,16 +87,21 @@ describe("run.json turn-boundary snapshots — end to end", () => {
8287 const home = mkdtempSync ( join ( tmpdir ( ) , "corbits-run-state-home-" ) ) ;
8388 const sessionId = generateSessionId ( ) ;
8489 try {
85- const runSink = createRunSink ( { emitter : new EventEmitter ( ) , hookManager : noopHookManager } ) ;
86-
87- // Fire all 20 turns and their snapshot writes back to back, with no
88- // await between them — the per-session writeChains promise chain in
89- // src/session/state.ts is what keeps these ordered rather than the
90- // caller awaiting each one before starting the next.
9190 const writes : Promise < void > [ ] = [ ] ;
91+ const runSink = createRunSink ( {
92+ emitter : new EventEmitter ( ) ,
93+ hookManager : noopHookManager ,
94+ onTurnBoundary : ( ) => {
95+ writes . push ( saveState ( cwd , sessionId , baseState ( { status : "running" } , runSink . getTurnCount ( ) ) , home ) ) ;
96+ } ,
97+ } ) ;
98+
99+ // Fire all 20 turns back to back, with no await between them — the
100+ // per-session writeChains promise chain in src/session/state.ts is what
101+ // keeps the resulting writes ordered, not the caller awaiting each one
102+ // before starting the next.
92103 for ( let turn = 1 ; turn <= 20 ; turn ++ ) {
93104 runSink . sink ( inferenceDone ( ) ) ;
94- writes . push ( saveState ( cwd , sessionId , baseState ( { status : "running" } , runSink . getTurnCount ( ) ) , home ) ) ;
95105 }
96106 await Promise . all ( writes ) ;
97107
@@ -109,18 +119,24 @@ describe("run.json turn-boundary snapshots — end to end", () => {
109119 const home = mkdtempSync ( join ( tmpdir ( ) , "corbits-run-state-home-" ) ) ;
110120 const sessionId = generateSessionId ( ) ;
111121 try {
112- const runSink = createRunSink ( { emitter : new EventEmitter ( ) , hookManager : noopHookManager } ) ;
113- runSink . sink ( inferenceDone ( ) ) ;
114-
115- // Issue a "running" progress snapshot but do not await it before
116- // issuing the terminal "done" write right behind it — this models a
122+ let runningWrite : Promise < void > | undefined ;
123+ const runSink = createRunSink ( {
124+ emitter : new EventEmitter ( ) ,
125+ hookManager : noopHookManager ,
126+ onTurnBoundary : ( ) => {
127+ runningWrite = saveState (
128+ cwd ,
129+ sessionId ,
130+ baseState ( { status : "running" } , runSink . getTurnCount ( ) ) ,
131+ home ,
132+ ) ;
133+ } ,
134+ } ) ;
135+
136+ // The turn-boundary snapshot fires and is left un-awaited before the
137+ // terminal "done" write follows right behind it — this models a
117138 // straggler turn-boundary snapshot racing the close-out write.
118- const runningWrite = saveState (
119- cwd ,
120- sessionId ,
121- baseState ( { status : "running" } , runSink . getTurnCount ( ) ) ,
122- home ,
123- ) ;
139+ runSink . sink ( inferenceDone ( ) ) ;
124140 runSink . sink ( { type : "reactor.done" , data : { } } as unknown as ReactorEmittedEvent ) ;
125141 const doneWrite = saveState (
126142 cwd ,
0 commit comments