From db08cf5f20433973040e797775187cd61e6a9479 Mon Sep 17 00:00:00 2001 From: Daniel Sutton Date: Fri, 21 Aug 2026 14:22:37 +0100 Subject: [PATCH] test(run-store): the optional compare-and-set on the current snapshot pointer --- .../run-store/src/redisSnapshotStore.test.ts | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/internal-packages/run-store/src/redisSnapshotStore.test.ts b/internal-packages/run-store/src/redisSnapshotStore.test.ts index 9065be64a..088fc5cb5 100644 --- a/internal-packages/run-store/src/redisSnapshotStore.test.ts +++ b/internal-packages/run-store/src/redisSnapshotStore.test.ts @@ -769,3 +769,87 @@ describe("environment scoping", () => { } }); }); + +describe("expectedCur compare-and-set", () => { + redisTest("absent by default: cur advances unconditionally", async ({ redisOptions }) => { + const store = new RedisSnapshotStore({ redisOptions, completedTtlMs: 1000 }); + try { + await store.append({ entry: entry({ id: "s1" }), kind: "birth", isTerminal: false }); + const r = await store.append({ + entry: entry({ id: "s2", previousSnapshotId: "stale" }), + kind: "transition", + isTerminal: false, + }); + expect(r).toMatchObject({ outcome: "written" }); + expect((await store.getLatest("run_1"))?.id).toBe("s2"); + } finally { + await store.quit(); + } + }); + + redisTest("supplied and matching: the append proceeds", async ({ redisOptions }) => { + const store = new RedisSnapshotStore({ redisOptions, completedTtlMs: 1000 }); + try { + await store.append({ entry: entry({ id: "s1" }), kind: "birth", isTerminal: false }); + const r = await store.append({ + entry: entry({ id: "s2" }), + kind: "transition", + isTerminal: false, + expectedCur: "s1", + }); + expect(r).toMatchObject({ outcome: "written", seq: 2 }); + } finally { + await store.quit(); + } + }); + + redisTest("supplied and stale: writes NOTHING and reports the fork", async ({ redisOptions }) => { + const store = new RedisSnapshotStore({ redisOptions, completedTtlMs: 1000 }); + try { + await store.append({ entry: entry({ id: "s1" }), kind: "birth", isTerminal: false }); + await store.append({ entry: entry({ id: "s2" }), kind: "transition", isTerminal: false }); + + // A second concurrent transition that read cur = s1 before s2 landed. + const r = await store.append({ + entry: entry({ id: "s3" }), + kind: "transition", + isTerminal: false, + expectedCur: "s1", + }); + expect(r).toEqual({ outcome: "forked", actualCur: "s2" }); + + // Nothing was written: no entry, and the seq counter did not move. + expect(await store.getById("run_1", "s3")).toBeNull(); + const next = await store.append({ + entry: entry({ id: "s4" }), + kind: "transition", + isTerminal: false, + }); + expect(next).toMatchObject({ seq: 3 }); + } finally { + await store.quit(); + } + }); + + redisTest( + "supplied as empty string: still enforces a check against an unset cur", + async ({ redisOptions }) => { + const store = new RedisSnapshotStore({ redisOptions, completedTtlMs: 1000 }); + try { + // The birth sets cur to "s1", so a caller claiming cur is UNSET (expectedCur: "") must + // fork rather than have "" silently treated as "no compare-and-set requested". + await store.append({ entry: entry({ id: "s1" }), kind: "birth", isTerminal: false }); + const r = await store.append({ + entry: entry({ id: "s2" }), + kind: "transition", + isTerminal: false, + expectedCur: "", + }); + expect(r).toEqual({ outcome: "forked", actualCur: "s1" }); + expect(await store.getById("run_1", "s2")).toBeNull(); + } finally { + await store.quit(); + } + } + ); +});