Skip to content

Commit 5115d62

Browse files
test(core): port 1918's held-window rows to guard the publish chain
Answers review threads 4509003168 (the second publication must not start while the first is still running), 4509003174 (a write on another path publishes while the first path's publish is blocked) and 4509003182 (resetChain detaches a link that is still publishing so the next same-path write starts). The rows are verbatim from 1918 guardedWrite.spec.ts:943, :1043 and :1246; the resetChain row's closing assertion carries this branch's publish shape, safeWriteText(target, content, { backup: true }) at guardedWrite.ts:383 and :480, because 1918 publishes without the options argument. Focused suite: 82 passed; each row fails alone under a targeted mutation of guardedWrite.ts. Test-only commit: no changeset, no changelog.
1 parent a71761a commit 5115d62

1 file changed

Lines changed: 127 additions & 0 deletions

File tree

‎src/core/tools/__tests__/guardedWrite.spec.ts‎

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)