@@ -741,6 +741,48 @@ describe("guardedWrite (S4a, epic #1375)", () => {
741741 } )
742742 } )
743743
744+ // Ported from 1918 guardedWrite.spec.ts:943 (row 4507855167). This row answers root 4509003168 here.
745+ it ( "the second publication does not start while the first is still running" , async ( ) => {
746+ // Row 4507855167. Call order and non-overlap are different claims: NthCalledWith says which
747+ // publish ran first, and a second publish that began while the first was still inside the publish
748+ // primitive satisfies it. The first publish is held open on a promise this row controls, the
749+ // invocation list is read while that hold is open, and only then is the hold released.
750+ const reg = new ObservationRegistry ( )
751+ reg . observe ( abs ( "overlap.txt" ) , "v1" )
752+ mockedComputeVersionToken . mockResolvedValue ( "v1" )
753+ const task = createMockTask ( { observationRegistry : reg } )
754+
755+ const started : string [ ] = [ ]
756+ let releaseFirst : ( ) => void = ( ) => { }
757+ const firstHold = new Promise < void > ( ( resolve ) => {
758+ releaseFirst = resolve
759+ } )
760+ mockedSafeWriteText
761+ . mockImplementationOnce ( async ( ) => {
762+ started . push ( "first" )
763+ await firstHold
764+ } )
765+ . mockImplementationOnce ( async ( ) => {
766+ started . push ( "second" )
767+ } )
768+
769+ const p1 = guardedWrite ( task , "overlap.txt" , "first" , "update" )
770+ const p2 = guardedWrite ( task , "overlap.txt" , "second" , "update" )
771+
772+ // One macrotask tick is enough for every microtask-only step ahead of a publish to run for both
773+ // writes, so a second link that was not waiting on the first would already have published here.
774+ await new Promise ( ( resolve ) => {
775+ setTimeout ( resolve , 0 )
776+ } )
777+ expect ( started ) . toEqual ( [ "first" ] )
778+
779+ releaseFirst ( )
780+ const [ r1 , r2 ] = await Promise . allSettled ( [ p1 , p2 ] )
781+ expect ( r1 . status ) . toBe ( "fulfilled" )
782+ expect ( r2 . status ) . toBe ( "fulfilled" )
783+ expect ( started ) . toEqual ( [ "first" , "second" ] )
784+ } )
785+
744786 it ( "the chain settles after a rejection - a later matching write still runs" , async ( ) => {
745787 const reg = new ObservationRegistry ( )
746788 reg . observe ( abs ( "settle.txt" ) , "v1" )
@@ -801,6 +843,44 @@ describe("guardedWrite (S4a, epic #1375)", () => {
801843
802844 expect ( mockedSafeWriteText ) . toHaveBeenCalledTimes ( 2 )
803845 } )
846+ // Ported from 1918 guardedWrite.spec.ts:1043. This row answers root 4509003174 on this branch.
847+ it ( "lets a write on another path publish while the first path's publish is blocked" , async ( ) => {
848+ // The row is about progress during the blocked window, which a settled call
849+ // count cannot show: with one chain shared by every path the second write would
850+ // queue behind the first and this assertion would never see a publish at all.
851+ const reg = new ObservationRegistry ( )
852+ reg . observe ( abs ( "gate-a.txt" ) , "v1" )
853+ reg . observe ( abs ( "gate-b.txt" ) , "v1" )
854+ mockedComputeVersionToken . mockResolvedValue ( "v1" )
855+ const task = createMockTask ( { observationRegistry : reg } )
856+
857+ let releaseGateA : ( ) => void = ( ) => { }
858+ const gateA = new Promise < void > ( ( resolve ) => {
859+ releaseGateA = resolve
860+ } )
861+ const publishOrder : string [ ] = [ ]
862+ mockedSafeWriteText . mockImplementation ( async ( target : string ) => {
863+ if ( target === abs ( "gate-a.txt" ) ) {
864+ await gateA
865+ }
866+ publishOrder . push ( target )
867+ } )
868+
869+ const writeA = guardedWrite ( task , "gate-a.txt" , "a" , "update" )
870+ const writeB = guardedWrite ( task , "gate-b.txt" , "b" , "update" )
871+
872+ // Settle until b has published; a is still inside its own publish here.
873+ for ( let tick = 0 ; tick < 200 && publishOrder . length === 0 ; tick ++ ) {
874+ await new Promise ( ( resolve ) => setTimeout ( resolve , 5 ) )
875+ }
876+ expect ( publishOrder ) . toEqual ( [ abs ( "gate-b.txt" ) ] )
877+
878+ releaseGateA ( )
879+ await Promise . all ( [ writeA , writeB ] )
880+
881+ expect ( publishOrder ) . toEqual ( [ abs ( "gate-b.txt" ) , abs ( "gate-a.txt" ) ] )
882+ expect ( mockedSafeWriteText ) . toHaveBeenCalledTimes ( 2 )
883+ } )
804884 } )
805885
806886 describe ( "path resolution" , ( ) => {
@@ -967,6 +1047,53 @@ describe("guardedWrite (S4a, epic #1375)", () => {
9671047
9681048 expect ( mockedSafeWriteText ) . toHaveBeenLastCalledWith ( abs ( "x.txt" ) , "b" , { backup : true } )
9691049 } )
1050+ // Ported from 1918 guardedWrite.spec.ts:1246. This row answers root 4509003182 on this branch.
1051+ // The last assertion carries this branch's publish shape: guardedWrite.ts:383 and :480 call
1052+ // safeWriteText(target, content, { backup: true }), where 1918 calls it without options
1053+ // (guardedWrite.ts:207 and :290). The in-flight assertions above it are verbatim from 1918.
1054+ it ( "detaches a link that is still publishing so the next write starts immediately" , async ( ) => {
1055+ // The settled result of the row above cannot tell a detached chain from an
1056+ // emptied one, because nothing was in flight there. Here the first publish is
1057+ // still awaiting its gate when resetChain runs: if resetChain left the pending
1058+ // link as the path's tail, the second write would queue behind a publish that
1059+ // cannot finish until the second one is already waited on - the wedge the hook
1060+ // exists to undo.
1061+ const reg = new ObservationRegistry ( )
1062+ reg . observe ( abs ( "chain.txt" ) , "v1" )
1063+ mockedComputeVersionToken . mockResolvedValue ( "v1" )
1064+ const task = createMockTask ( { observationRegistry : reg } )
1065+
1066+ let releaseGate : ( ) => void = ( ) => { }
1067+ const gate = new Promise < void > ( ( resolve ) => {
1068+ releaseGate = resolve
1069+ } )
1070+ const entered : string [ ] = [ ]
1071+ mockedSafeWriteText . mockImplementation ( async ( _target : string , content : string | Uint8Array ) => {
1072+ entered . push ( String ( content ) )
1073+ if ( content === "a" ) {
1074+ await gate
1075+ }
1076+ } )
1077+
1078+ const firstWrite = guardedWrite ( task , "chain.txt" , "a" , "update" )
1079+ for ( let tick = 0 ; tick < 200 && entered . length === 0 ; tick ++ ) {
1080+ await new Promise ( ( resolve ) => setTimeout ( resolve , 5 ) )
1081+ }
1082+ // The first publish is in flight and unfinished.
1083+ expect ( entered ) . toEqual ( [ "a" ] )
1084+
1085+ resetChain ( )
1086+ const secondWrite = guardedWrite ( task , "chain.txt" , "b" , "update" )
1087+ for ( let tick = 0 ; tick < 200 && entered . length < 2 ; tick ++ ) {
1088+ await new Promise ( ( resolve ) => setTimeout ( resolve , 5 ) )
1089+ }
1090+ expect ( entered ) . toEqual ( [ "a" , "b" ] )
1091+
1092+ releaseGate ( )
1093+ await Promise . all ( [ firstWrite , secondWrite ] )
1094+ expect ( entered ) . toEqual ( [ "a" , "b" ] )
1095+ expect ( mockedSafeWriteText ) . toHaveBeenLastCalledWith ( abs ( "chain.txt" ) , "b" , { backup : true } )
1096+ } )
9701097 } )
9711098
9721099 it ( "does not run a queued write once the owning task is cancelled" , async ( ) => {
0 commit comments