|
| 1 | +import { describe, expect, it } from "vitest"; |
| 2 | +import { RoutingRunStore } from "./runOpsStore.js"; |
| 3 | +import type { RunStore } from "./types.js"; |
| 4 | + |
| 5 | +// Env-scoped writes with no owning run (waitpoint tags; idempotency-key reset by predicate) must |
| 6 | +// route to NEW when the env mints run-ops ids, instead of defaulting to LEGACY / fanning a wrong-DB |
| 7 | +// write. Pure routing: fake RunStore slots record which store the router dispatches to. |
| 8 | + |
| 9 | +type Call = { method: string; args: unknown[] }; |
| 10 | +type FakeStore = RunStore & { slot: "new" | "legacy"; calls: Call[] }; |
| 11 | + |
| 12 | +function fakeStore(slot: "new" | "legacy"): FakeStore { |
| 13 | + const calls: Call[] = []; |
| 14 | + const rec = |
| 15 | + (method: string, result: unknown) => |
| 16 | + (...args: unknown[]) => { |
| 17 | + calls.push({ method, args }); |
| 18 | + return Promise.resolve(result); |
| 19 | + }; |
| 20 | + return { |
| 21 | + slot, |
| 22 | + calls, |
| 23 | + upsertWaitpointTag: rec("upsertWaitpointTag", { id: slot, slot }), |
| 24 | + clearIdempotencyKey: rec("clearIdempotencyKey", { count: slot === "new" ? 1 : 0 }), |
| 25 | + } as unknown as FakeStore; |
| 26 | +} |
| 27 | + |
| 28 | +function buildRouter() { |
| 29 | + const newStore = fakeStore("new"); |
| 30 | + const legacyStore = fakeStore("legacy"); |
| 31 | + const router = new RoutingRunStore({ |
| 32 | + new: newStore, |
| 33 | + legacy: legacyStore, |
| 34 | + classify: (id: string) => (id.startsWith("new") ? "NEW" : "LEGACY"), |
| 35 | + }); |
| 36 | + return { router, newStore, legacyStore }; |
| 37 | +} |
| 38 | + |
| 39 | +describe("RoutingRunStore.upsertWaitpointTag — residency hint for a tag with no minted id", () => { |
| 40 | + it("routes to NEW when residency is NEW and no id is supplied", async () => { |
| 41 | + const { router, newStore, legacyStore } = buildRouter(); |
| 42 | + await router.upsertWaitpointTag({ environmentId: "env", name: "t", projectId: "p" }, undefined, "NEW"); |
| 43 | + expect(newStore.calls.map((c) => c.method)).toEqual(["upsertWaitpointTag"]); |
| 44 | + expect(legacyStore.calls).toHaveLength(0); |
| 45 | + }); |
| 46 | + |
| 47 | + it("still falls back to LEGACY when no id and no residency are supplied", async () => { |
| 48 | + const { router, newStore, legacyStore } = buildRouter(); |
| 49 | + await router.upsertWaitpointTag({ environmentId: "env", name: "t", projectId: "p" }); |
| 50 | + expect(legacyStore.calls.map((c) => c.method)).toEqual(["upsertWaitpointTag"]); |
| 51 | + expect(newStore.calls).toHaveLength(0); |
| 52 | + }); |
| 53 | +}); |
| 54 | + |
| 55 | +describe("RoutingRunStore.clearIdempotencyKey — predicate routes NEW when the env mints new", () => { |
| 56 | + it("routes a byPredicate reset to NEW only when residency is NEW (no legacy fan-out)", async () => { |
| 57 | + const { router, newStore, legacyStore } = buildRouter(); |
| 58 | + const result = await router.clearIdempotencyKey({ |
| 59 | + byPredicate: { |
| 60 | + idempotencyKey: "k", |
| 61 | + taskIdentifier: "task", |
| 62 | + runtimeEnvironmentId: "env", |
| 63 | + residency: "NEW", |
| 64 | + }, |
| 65 | + }); |
| 66 | + expect(newStore.calls.map((c) => c.method)).toEqual(["clearIdempotencyKey"]); |
| 67 | + expect(legacyStore.calls).toHaveLength(0); |
| 68 | + expect(result.count).toBe(1); |
| 69 | + }); |
| 70 | + |
| 71 | + it("still fans out a byPredicate reset with no residency (mixed residency)", async () => { |
| 72 | + const { router, newStore, legacyStore } = buildRouter(); |
| 73 | + await router.clearIdempotencyKey({ |
| 74 | + byPredicate: { idempotencyKey: "k", taskIdentifier: "task", runtimeEnvironmentId: "env" }, |
| 75 | + }); |
| 76 | + expect(newStore.calls).toHaveLength(1); |
| 77 | + expect(legacyStore.calls).toHaveLength(1); |
| 78 | + }); |
| 79 | + |
| 80 | + it("routes byId to the owning store (unchanged)", async () => { |
| 81 | + const { router, newStore, legacyStore } = buildRouter(); |
| 82 | + await router.clearIdempotencyKey({ byId: { runId: "new_run", idempotencyKey: "k" } }); |
| 83 | + expect(newStore.calls.map((c) => c.method)).toEqual(["clearIdempotencyKey"]); |
| 84 | + expect(legacyStore.calls).toHaveLength(0); |
| 85 | + }); |
| 86 | +}); |
0 commit comments