@code-yeongyu/senpi-agent-core 2026.10.27-2 → 2026.10.30-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/conformance/index.d.ts +42 -0
  2. package/dist/conformance/index.js +23 -0
  3. package/dist/conformance/mapping.d.ts +24 -0
  4. package/dist/conformance/mapping.js +62 -0
  5. package/dist/conformance/registry.d.ts +15 -0
  6. package/dist/conformance/registry.js +75 -0
  7. package/dist/conformance/runner.d.ts +27 -0
  8. package/dist/conformance/runner.js +23 -0
  9. package/dist/conformance/suite-hash.d.ts +12 -0
  10. package/dist/conformance/suite-hash.js +20 -0
  11. package/dist/durable/store-admission.d.ts +7 -0
  12. package/dist/durable/store-admission.js +11 -0
  13. package/dist/durable/store-memory.d.ts +15 -0
  14. package/dist/durable/store-memory.js +140 -0
  15. package/dist/durable/store.d.ts +116 -0
  16. package/dist/durable/store.js +14 -0
  17. package/dist/durable/testing/kill-store.d.ts +13 -0
  18. package/dist/durable/testing/kill-store.js +77 -0
  19. package/dist/durable/testing/kill-wake.d.ts +12 -0
  20. package/dist/durable/testing/kill-wake.js +123 -0
  21. package/dist/durable/testing/manual-timers.d.ts +8 -0
  22. package/dist/durable/testing/manual-timers.js +31 -0
  23. package/dist/durable/testing/multi-host-cases.d.ts +6 -0
  24. package/dist/durable/testing/multi-host-cases.js +32 -0
  25. package/dist/durable/testing/store-cases.d.ts +21 -0
  26. package/dist/durable/testing/store-cases.js +168 -0
  27. package/dist/durable/testing/store-harness.d.ts +23 -0
  28. package/dist/durable/testing/store-harness.js +64 -0
  29. package/dist/durable/testing/wake-cases.d.ts +6 -0
  30. package/dist/durable/testing/wake-cases.js +100 -0
  31. package/dist/durable/testing/wake-harness.d.ts +64 -0
  32. package/dist/durable/testing/wake-harness.js +127 -0
  33. package/dist/durable/testing/wake-test-loop.d.ts +13 -0
  34. package/dist/durable/testing/wake-test-loop.js +40 -0
  35. package/dist/durable/wake-host-bun-timer.d.ts +31 -0
  36. package/dist/durable/wake-host-bun-timer.js +66 -0
  37. package/dist/durable/wake-host-store-alarm.d.ts +13 -0
  38. package/dist/durable/wake-host-store-alarm.js +20 -0
  39. package/dist/durable/wake-host.d.ts +68 -0
  40. package/dist/durable/wake-host.js +70 -0
  41. package/dist/harness/utils/read-folders/index.d.ts +1 -1
  42. package/dist/harness/utils/read-folders/index.js +1 -1
  43. package/dist/harness/utils/read-folders/tree-sitter/engine.d.ts +1 -1
  44. package/dist/harness/utils/read-folders/tree-sitter/engine.js +38 -19
  45. package/dist/testing/win32-sim.d.ts +76 -0
  46. package/dist/testing/win32-sim.js +197 -0
  47. package/dist/transport/in-process-carrier.d.ts +19 -0
  48. package/dist/transport/in-process-carrier.js +63 -0
  49. package/dist/transport/local-endpoint.d.ts +13 -0
  50. package/dist/transport/local-endpoint.js +20 -0
  51. package/dist/transport/reliable-transport.d.ts +40 -0
  52. package/dist/transport/reliable-transport.js +232 -0
  53. package/dist/transport/testing/fault-cases.d.ts +6 -0
  54. package/dist/transport/testing/fault-cases.js +30 -0
  55. package/dist/transport/testing/fault-transport.d.ts +44 -0
  56. package/dist/transport/testing/fault-transport.js +144 -0
  57. package/dist/transport/testing/multi-cases.d.ts +6 -0
  58. package/dist/transport/testing/multi-cases.js +113 -0
  59. package/dist/transport/testing/transport-cases.d.ts +6 -0
  60. package/dist/transport/testing/transport-cases.js +60 -0
  61. package/dist/transport/testing/transport-harness.d.ts +53 -0
  62. package/dist/transport/testing/transport-harness.js +127 -0
  63. package/dist/transport/transport.d.ts +60 -0
  64. package/dist/transport/transport.js +15 -0
  65. package/package.json +11 -3
@@ -0,0 +1,13 @@
1
+ import type { CaseImplementation } from "../../conformance/runner.ts";
2
+ import type { StoreOperation } from "../store.ts";
3
+ import { type StoreCaseAdapters } from "./store-harness.ts";
4
+ /** Where a KILL case applies; a host report logs it in kill[] with the registry's expected outcome. */
5
+ export interface KillDeclaration {
6
+ readonly id: string;
7
+ readonly op: StoreOperation;
8
+ readonly at: "before" | "durable";
9
+ readonly placement: "everywhere";
10
+ }
11
+ export declare const KILL_STORE_DECLARATIONS: readonly KillDeclaration[];
12
+ export declare const KILL_STORE_CASES: ReadonlyMap<string, CaseImplementation<StoreCaseAdapters>>;
13
+ //# sourceMappingURL=kill-store.d.ts.map
@@ -0,0 +1,77 @@
1
+ import { append, claimedStore, put, stored } from "./store-cases.js";
2
+ import { adoptLog, cutOnceAt, expectCut, expectEqual, expectStoreError, } from "./store-harness.js";
3
+ const killCase = (id, op, at, body) => ({ id, op, at, placement: "everywhere", run: ({ store }) => body(store, { op, at }) });
4
+ async function seeded(adapter) {
5
+ const { medium, store } = await claimedStore(adapter);
6
+ await store.commit(1, [append("r1"), put("doc", 1)], () => { });
7
+ return { medium, baseline: await store.exportSnapshot() };
8
+ }
9
+ async function killThenReopen(adapter, medium, kill, operation) {
10
+ await expectCut(operation(await adapter.open(medium, cutOnceAt(kill))), `kill ${kill.at} ${kill.op}`);
11
+ return adapter.open(medium);
12
+ }
13
+ const content = ({ head, records, documents }) => ({ head, records, documents });
14
+ const SECOND_BATCH = [append("r2"), put("doc", 2)];
15
+ const KILL_CASES = [
16
+ killCase("KILL-STORE-01", "commit", "before", async (adapter, kill) => {
17
+ const { medium, baseline } = await seeded(adapter);
18
+ const log = adoptLog();
19
+ const restarted = await killThenReopen(adapter, medium, kill, (s) => s.commit(1, SECOND_BATCH, log.adopt));
20
+ expectEqual(await restarted.exportSnapshot(), baseline, "nothing of the batch is durable");
21
+ expectEqual(await restarted.commit(1, SECOND_BATCH, log.adopt), { seqs: [2], head: 2 }, "same IDs, next seq");
22
+ expectEqual(log.calls, ["adopt:2"], "only the committed batch adopts");
23
+ }),
24
+ killCase("KILL-STORE-02", "commit", "durable", async (adapter, kill) => {
25
+ const { medium, baseline } = await seeded(adapter);
26
+ const log = adoptLog();
27
+ const restarted = await killThenReopen(adapter, medium, kill, (s) => s.commit(1, SECOND_BATCH, log.adopt));
28
+ const expected = {
29
+ ...baseline,
30
+ head: 2,
31
+ records: [stored(1, "r1"), stored(2, "r2")],
32
+ documents: [{ key: "doc", data: 2 }],
33
+ };
34
+ expectEqual(log.calls, [], "the killed process never adopted");
35
+ expectEqual(await restarted.exportSnapshot(), expected, "the whole batch is durable with its IDs and seqs");
36
+ await expectStoreError(restarted.commit(1, SECOND_BATCH, log.adopt), "duplicate-record-id", "a retried batch");
37
+ expectEqual(await restarted.exportSnapshot(), expected, "the retry applies nothing twice");
38
+ }),
39
+ killCase("KILL-STORE-03", "claim", "before", async (adapter, kill) => {
40
+ const { medium, baseline } = await seeded(adapter);
41
+ const restarted = await killThenReopen(adapter, medium, kill, (s) => s.claim(1, 2));
42
+ expectEqual(await restarted.exportSnapshot(), baseline, "the epoch and log are unchanged");
43
+ await restarted.claim(1, 2);
44
+ expectEqual(await restarted.readEpoch(), 2, "the claim succeeds after restart");
45
+ }),
46
+ killCase("KILL-STORE-04", "claim", "durable", async (adapter, kill) => {
47
+ const { medium, baseline } = await seeded(adapter);
48
+ const restarted = await killThenReopen(adapter, medium, kill, (s) => s.claim(1, 2));
49
+ expectEqual(await restarted.exportSnapshot(), { ...baseline, epoch: 2 }, "the claim is durable; the log is intact");
50
+ await expectStoreError(restarted.claim(1, 2), "stale-epoch", "the claim cannot be taken twice");
51
+ await expectStoreError(restarted.commit(1, [append("late")], () => { }), "stale-epoch", "the old epoch is fenced");
52
+ }),
53
+ killCase("KILL-STORE-05", "import", "before", async (adapter, kill) => {
54
+ const { baseline } = await seeded(adapter);
55
+ const { medium } = await claimedStore(adapter, 2);
56
+ const restarted = await killThenReopen(adapter, medium, kill, (s) => s.importSnapshot(2, baseline));
57
+ expectEqual(await restarted.exportSnapshot(), { epoch: 2, head: 0, records: [], documents: [] }, "nothing imported");
58
+ await restarted.importSnapshot(2, baseline);
59
+ expectEqual(content(await restarted.exportSnapshot()), content(baseline), "the import succeeds after restart");
60
+ }),
61
+ killCase("KILL-STORE-06", "import", "durable", async (adapter, kill) => {
62
+ const { baseline } = await seeded(adapter);
63
+ const { medium } = await claimedStore(adapter, 2);
64
+ const restarted = await killThenReopen(adapter, medium, kill, (s) => s.importSnapshot(2, baseline));
65
+ expectEqual(content(await restarted.exportSnapshot()), content(baseline), "the whole import is durable");
66
+ await restarted.importSnapshot(2, baseline);
67
+ expectEqual(content(await restarted.exportSnapshot()), content(baseline), "re-import adds nothing twice");
68
+ }),
69
+ ];
70
+ export const KILL_STORE_DECLARATIONS = KILL_CASES.map(({ id, op, at, placement }) => ({
71
+ id,
72
+ op,
73
+ at,
74
+ placement,
75
+ }));
76
+ export const KILL_STORE_CASES = new Map(KILL_CASES.map(({ id, run }) => [id, run]));
77
+ //# sourceMappingURL=kill-store.js.map
@@ -0,0 +1,12 @@
1
+ import type { CaseImplementation } from "../../conformance/runner.ts";
2
+ import { type WakeCaseAdapters } from "./wake-harness.ts";
3
+ /** Where a KILL-WAKE case applies; a host report logs it in kill[] with the registry's expected outcome. */
4
+ export interface KillWakeDeclaration {
5
+ readonly id: string;
6
+ readonly operation: "admit" | "commit" | "update" | "fire";
7
+ readonly point: string;
8
+ readonly placement: "everywhere";
9
+ }
10
+ export declare const KILL_WAKE_DECLARATIONS: readonly KillWakeDeclaration[];
11
+ export declare const KILL_WAKE_CASES: ReadonlyMap<string, CaseImplementation<WakeCaseAdapters>>;
12
+ //# sourceMappingURL=kill-wake.d.ts.map
@@ -0,0 +1,123 @@
1
+ import { expectEqual } from "./store-harness.js";
2
+ import { createSlot, cutAlarm, cutStore, cutWake, expectWakeCut, HEARTBEAT_MS, } from "./wake-harness.js";
3
+ const killCase = (id, operation, point, run) => ({ id, operation, point, placement: "everywhere", run });
4
+ const LATER = 600_000;
5
+ const DUE = 120_000;
6
+ async function killedAdmission(adapters, cut) {
7
+ const slot = await createSlot(adapters);
8
+ const { host, loop } = await slot.instance(cut);
9
+ await expectWakeCut(host.admit(() => loop.schedule("a", slot.base + LATER)), "the killed admission");
10
+ slot.kill();
11
+ return slot;
12
+ }
13
+ /** Recovery with no request from anyone: only the armed alarm brings the session back. */
14
+ async function recoversThroughHeartbeat(adapters, slot) {
15
+ expectEqual(await slot.alarmAt(), HEARTBEAT_MS, "the pre-armed heartbeat survived the kill");
16
+ await adapters.wake.advanceTo(slot.base + HEARTBEAT_MS);
17
+ expectEqual(await slot.alarmAt(), LATER, "the heartbeat's instance armed the committed wake");
18
+ await adapters.wake.advanceTo(slot.base + LATER);
19
+ expectEqual(await slot.ran(), ["a"], "the committed task ran exactly once");
20
+ expectEqual(await slot.alarmAt(), undefined, "and the alarm is cleared");
21
+ }
22
+ async function killedFire(adapters, cut) {
23
+ const slot = await createSlot(adapters);
24
+ const { host, loop } = await slot.instance(cut);
25
+ await host.admit(() => loop.schedule("a", slot.base + DUE));
26
+ await adapters.wake.advanceTo(slot.base + DUE);
27
+ expectEqual(slot.fires(), 1, "the alarm fired into the instance that is killed");
28
+ return slot;
29
+ }
30
+ async function recoversAfterKilledFire(adapters, slot) {
31
+ expectEqual(await slot.alarmAt(), DUE + HEARTBEAT_MS, "the fire's re-arm survived the kill");
32
+ await adapters.wake.advanceTo(slot.base + DUE + HEARTBEAT_MS);
33
+ expectEqual(await slot.ran(), ["a"], "the task ran exactly once");
34
+ expectEqual(await slot.alarmAt(), undefined, "and the alarm is cleared");
35
+ await adapters.wake.advanceTo(slot.base + LATER);
36
+ expectEqual(slot.fires(), 2, "no fire beyond the recovery");
37
+ }
38
+ /**
39
+ * An admission B runs while a fire resumes other work on the same instance. The fire's updates see an idle loop (B's
40
+ * task is not committed yet), so they must not clear the heartbeat B armed; B is killed after its commit is durable,
41
+ * before its own update. The session runs at epoch 2.
42
+ */
43
+ async function admissionDuringFire(adapters) {
44
+ const fireDone = Promise.withResolvers();
45
+ let admission;
46
+ let session;
47
+ const slot = await createSlot(adapters, 2);
48
+ const first = await slot.instance({
49
+ store: cutStore("commit", "durable", 3),
50
+ onWakePoint: (point) => {
51
+ if (point.op !== "fire" || session === undefined || admission !== undefined)
52
+ return;
53
+ const { host, loop } = session;
54
+ admission = host.admit(async () => {
55
+ await fireDone.promise;
56
+ await loop.schedule("b", slot.base + LATER);
57
+ });
58
+ },
59
+ });
60
+ session = first;
61
+ await first.host.admit(() => first.loop.schedule("a", slot.base + DUE));
62
+ await adapters.wake.advanceTo(slot.base + DUE);
63
+ expectEqual(await slot.ran(), ["a"], "the fire resumed its due task");
64
+ if (admission === undefined)
65
+ throw new Error("the admission did not start during the fire");
66
+ fireDone.resolve();
67
+ await expectWakeCut(admission, "the admission killed after its commit");
68
+ slot.kill();
69
+ return slot;
70
+ }
71
+ const KILL_CASES = [
72
+ killCase("KILL-WAKE-01", "admit", "alarm write before durable", async (adapters) => {
73
+ const slot = await killedAdmission(adapters, { alarm: cutAlarm("set", "before", 2) });
74
+ expectEqual(await slot.alarmAt(), undefined, "nothing of the admission is durable");
75
+ const { host, loop } = await slot.instance();
76
+ await host.admit(() => loop.schedule("a", slot.base + LATER));
77
+ await adapters.wake.advanceTo(slot.base + LATER);
78
+ expectEqual(await slot.ran(), ["a"], "the retried admission runs once");
79
+ }),
80
+ killCase("KILL-WAKE-02", "admit", "after the arm, before work", async (adapters) => {
81
+ const slot = await killedAdmission(adapters, { onWakePoint: cutWake("admit") });
82
+ expectEqual(await slot.alarmAt(), HEARTBEAT_MS, "the arm is durable");
83
+ await adapters.wake.advanceTo(slot.base + HEARTBEAT_MS);
84
+ expectEqual(await slot.ran(), [], "an alarm without work runs nothing");
85
+ expectEqual(await slot.alarmAt(), undefined, "and ends cleared");
86
+ expectEqual(slot.fires(), 1, "one harmless fire");
87
+ }),
88
+ killCase("KILL-WAKE-03", "commit", "after the commit is durable, before the alarm update", async (adapters) => {
89
+ const slot = await killedAdmission(adapters, { store: cutStore("commit", "durable") });
90
+ await recoversThroughHeartbeat(adapters, slot);
91
+ }),
92
+ killCase("KILL-WAKE-04", "update", "eviction during the alarm write", async (adapters) => {
93
+ const slot = await killedAdmission(adapters, { alarm: cutAlarm("set", "before", 3) });
94
+ await recoversThroughHeartbeat(adapters, slot);
95
+ }),
96
+ killCase("KILL-WAKE-05", "fire", "after the re-arm, before resume", async (adapters) => {
97
+ const slot = await killedFire(adapters, { onWakePoint: cutWake("fire") });
98
+ expectEqual(await slot.ran(), [], "nothing ran before the kill");
99
+ await recoversAfterKilledFire(adapters, slot);
100
+ }),
101
+ killCase("KILL-WAKE-06", "fire", "after a resumed task's commit is durable", async (adapters) => {
102
+ const slot = await killedFire(adapters, { store: cutStore("commit", "durable", 2) });
103
+ expectEqual(await slot.ran(), ["a"], "the task's commit is durable");
104
+ await recoversAfterKilledFire(adapters, slot);
105
+ }),
106
+ killCase("KILL-WAKE-07", "fire", "eviction during the alarm clear after the work", async (adapters) => {
107
+ const slot = await killedFire(adapters, { alarm: cutAlarm("clear", "before", 2) });
108
+ expectEqual(await slot.ran(), ["a"], "the work is durable");
109
+ await recoversAfterKilledFire(adapters, slot);
110
+ }),
111
+ killCase("KILL-WAKE-08", "admit", "a concurrent fire's updates while the admission is uncommitted, then a kill after its commit", async (adapters) => {
112
+ const slot = await admissionDuringFire(adapters);
113
+ expectEqual(await slot.alarmAt(), DUE + HEARTBEAT_MS, "the admission's heartbeat outlived the fire's updates");
114
+ await adapters.wake.advanceTo(slot.base + DUE + HEARTBEAT_MS);
115
+ expectEqual(await slot.alarmAt(), LATER, "the heartbeat's instance armed the committed wake");
116
+ await adapters.wake.advanceTo(slot.base + LATER);
117
+ expectEqual(await slot.ran(), ["a", "b"], "each task ran exactly once");
118
+ expectEqual(await slot.alarmAt(), undefined, "and the alarm is cleared");
119
+ }),
120
+ ];
121
+ export const KILL_WAKE_DECLARATIONS = KILL_CASES.map(({ id, operation, point, placement }) => ({ id, operation, point, placement }));
122
+ export const KILL_WAKE_CASES = new Map(KILL_CASES.map(({ id, run }) => [id, run]));
123
+ //# sourceMappingURL=kill-wake.js.map
@@ -0,0 +1,8 @@
1
+ /** Manual timers for a host under test: a clock that moves only through `advanceTo`, which runs due timers in order. */
2
+ export interface ManualTimers {
3
+ now(): number;
4
+ setTimer(callback: () => unknown, ms: number): () => void;
5
+ advanceTo(at: number): Promise<void>;
6
+ }
7
+ export declare function createManualTimers(start: number): ManualTimers;
8
+ //# sourceMappingURL=manual-timers.d.ts.map
@@ -0,0 +1,31 @@
1
+ export function createManualTimers(start) {
2
+ let time = start;
3
+ let nextId = 1;
4
+ const timers = new Map();
5
+ return {
6
+ now: () => time,
7
+ setTimer: (callback, ms) => {
8
+ const id = nextId++;
9
+ timers.set(id, { due: time + Math.max(0, ms), callback });
10
+ return () => void timers.delete(id);
11
+ },
12
+ async advanceTo(at) {
13
+ for (let step = 0;; step++) {
14
+ if (step > 10_000)
15
+ throw new Error("manual timers: more than 10000 timers fired in one advance");
16
+ let next;
17
+ for (const entry of timers) {
18
+ if (entry[1].due <= at && (next === undefined || entry[1].due < next[1].due))
19
+ next = entry;
20
+ }
21
+ if (next === undefined)
22
+ break;
23
+ timers.delete(next[0]);
24
+ time = Math.max(time, next[1].due);
25
+ await next[1].callback();
26
+ }
27
+ time = Math.max(time, at);
28
+ },
29
+ };
30
+ }
31
+ //# sourceMappingURL=manual-timers.js.map
@@ -0,0 +1,6 @@
1
+ import type { CaseImplementation } from "../../conformance/runner.ts";
2
+ import { type WakeCaseAdapters } from "./wake-harness.ts";
3
+ type MultiHostCase = CaseImplementation<WakeCaseAdapters>;
4
+ export declare const MULTI_HOST_CASES: ReadonlyMap<string, MultiHostCase>;
5
+ export {};
6
+ //# sourceMappingURL=multi-host-cases.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { expectEqual } from "./store-harness.js";
2
+ import { createSlot } from "./wake-harness.js";
3
+ async function schedule(slot, id, offset) {
4
+ const { host, loop } = await slot.instance();
5
+ await host.admit(() => loop.schedule(id, slot.base + offset));
6
+ }
7
+ const observe = async (slots) => ({
8
+ fires: slots.map((slot) => slot.fires()),
9
+ ran: await Promise.all(slots.map((slot) => slot.ran())),
10
+ alarms: await Promise.all(slots.map((slot) => slot.alarmAt())),
11
+ });
12
+ const manySessionsOnOneHost = async (adapters) => {
13
+ const slots = [await createSlot(adapters), await createSlot(adapters), await createSlot(adapters)];
14
+ const [first, second, third] = slots;
15
+ if (first === undefined || second === undefined || third === undefined)
16
+ throw new Error("three sessions");
17
+ await schedule(first, "x", 120_000);
18
+ await schedule(first, "y", 300_000);
19
+ await schedule(second, "x", 200_000);
20
+ await third.instance();
21
+ expectEqual(await observe(slots), { fires: [0, 0, 0], ran: [[], [], []], alarms: [120_000, 200_000, undefined] }, "armed per session");
22
+ second.kill();
23
+ await adapters.wake.advanceTo(first.base + 120_000);
24
+ expectEqual(await observe(slots), { fires: [1, 0, 0], ran: [["x"], [], []], alarms: [300_000, 200_000, undefined] }, "a fire resumes only its own session; the same task ID elsewhere is untouched");
25
+ third.kill();
26
+ await third.instance();
27
+ expectEqual((await observe(slots)).alarms, [300_000, 200_000, undefined], "restarting one session moves no other session's alarm");
28
+ await adapters.wake.advanceTo(first.base + 300_000);
29
+ expectEqual(await observe(slots), { fires: [2, 1, 0], ran: [["x", "y"], ["x"], []], alarms: [undefined, undefined, undefined] }, "an evicted session comes back on its own alarm; records and wakes stay per session");
30
+ };
31
+ export const MULTI_HOST_CASES = new Map([["MULTI-HOST-01", manySessionsOnOneHost]]);
32
+ //# sourceMappingURL=multi-host-cases.js.map
@@ -0,0 +1,21 @@
1
+ import type { CaseImplementation } from "../../conformance/runner.ts";
2
+ import type { StoreValue, StoreWrite } from "../store.ts";
3
+ import { type StoreCaseAdapters, type StoreConformanceAdapter } from "./store-harness.ts";
4
+ type StoreCase = CaseImplementation<StoreCaseAdapters>;
5
+ export declare const append: (id: string, kind?: string) => StoreWrite;
6
+ export declare const put: (key: string, data: StoreValue) => StoreWrite;
7
+ export declare const stored: (seq: number, id: string, kind?: string) => {
8
+ seq: number;
9
+ id: string;
10
+ kind: string;
11
+ data: {
12
+ id: string;
13
+ };
14
+ };
15
+ export declare function claimedStore(adapter: StoreConformanceAdapter, epoch?: number): Promise<{
16
+ medium: string;
17
+ store: import("../store.ts").DurableStore;
18
+ }>;
19
+ export declare const STORE_CASES: ReadonlyMap<string, StoreCase>;
20
+ export {};
21
+ //# sourceMappingURL=store-cases.d.ts.map
@@ -0,0 +1,168 @@
1
+ import { createAdmissionLine } from "../store-admission.js";
2
+ import { adoptLog, cutOnceAt, expectCut, expectEqual, expectStoreError, reopenSnapshot, } from "./store-harness.js";
3
+ export const append = (id, kind = "entry") => ({ op: "append", id, kind, data: { id } });
4
+ export const put = (key, data) => ({ op: "put", key, data });
5
+ export const stored = (seq, id, kind = "entry") => ({ seq, id, kind, data: { id } });
6
+ export async function claimedStore(adapter, epoch = 1) {
7
+ const medium = await adapter.createMedium();
8
+ const store = await adapter.open(medium);
9
+ for (let next = 1; next <= epoch; next++)
10
+ await store.claim(next - 1, next);
11
+ return { medium, store };
12
+ }
13
+ const commitAdoptsOnDurable = async ({ store: adapter }) => {
14
+ const medium = await adapter.createMedium();
15
+ const events = [];
16
+ const store = await adapter.open(medium, {
17
+ onWritePoint: (point) => void (point.op === "commit" && point.at === "durable" && events.push("durable")),
18
+ });
19
+ await store.claim(0, 1);
20
+ const adopt = (result) => void events.push(`adopt:${result.seqs.join(",")}`);
21
+ expectEqual(await store.commit(1, [append("r1"), append("r2"), put("doc", 1)], adopt), { seqs: [1, 2], head: 2 }, "batch");
22
+ expectEqual(events, ["durable", "adopt:1,2"], "adopt runs once, after the batch is durable");
23
+ expectEqual(await store.commit(1, [put("doc", 2)], adopt), { seqs: [], head: 2 }, "documents-only batch");
24
+ expectEqual(await reopenSnapshot(adapter, medium), {
25
+ epoch: 1,
26
+ head: 2,
27
+ records: [stored(1, "r1"), stored(2, "r2")],
28
+ documents: [{ key: "doc", data: 2 }],
29
+ }, "another instance on the medium sees every committed batch whole");
30
+ };
31
+ const BATCH = [append("r2"), put("a", 2), { op: "delete", key: "b" }, append("r3"), put("c", 1)];
32
+ const cutAtEveryWritePoint = async ({ store: adapter }) => {
33
+ const points = [
34
+ { op: "commit", at: "before" },
35
+ ...BATCH.map((_, index) => ({ op: "commit", at: "write", index })),
36
+ ];
37
+ for (const point of points) {
38
+ const what = `cut at ${JSON.stringify(point)}`;
39
+ const { medium, store: setup } = await claimedStore(adapter);
40
+ await setup.commit(1, [append("r1"), put("a", 1), put("b", 1)], () => { });
41
+ const baseline = await setup.exportSnapshot();
42
+ const store = await adapter.open(medium, cutOnceAt(point));
43
+ const log = adoptLog();
44
+ await expectCut(store.commit(1, BATCH, log.adopt), what);
45
+ expectEqual(log.calls, [], `${what}: no adopt`);
46
+ expectEqual(await reopenSnapshot(adapter, medium), baseline, `${what}: nothing of the batch is durable`);
47
+ expectEqual(await store.commit(1, BATCH, log.adopt), { seqs: [2, 3], head: 3 }, `${what}: no seq burned`);
48
+ expectEqual(log.calls, ["adopt:2,3"], `${what}: the retried batch adopts once`);
49
+ }
50
+ };
51
+ const concurrentAdmissions = async ({ store: adapter }) => {
52
+ const { store } = await claimedStore(adapter);
53
+ const line = createAdmissionLine();
54
+ const events = [];
55
+ const gate = Promise.withResolvers();
56
+ const admit = (name, hold) => line.admit(async () => {
57
+ events.push(`${name}:start`);
58
+ if (hold !== undefined)
59
+ await hold;
60
+ return store.commit(1, [append(name)], (result) => void events.push(`${name}:adopt:${result.seqs.join(",")}`));
61
+ });
62
+ const admissions = [
63
+ admit("a", gate.promise),
64
+ admit("b"),
65
+ line.admit(async () => {
66
+ events.push("c:start");
67
+ throw new Error("mutation refused");
68
+ }),
69
+ admit("d"),
70
+ ];
71
+ gate.resolve();
72
+ const settled = await Promise.allSettled(admissions);
73
+ expectEqual(settled.map((s) => s.status), ["fulfilled", "fulfilled", "rejected", "fulfilled"], "admission outcomes");
74
+ expectEqual(events, ["a:start", "a:adopt:1", "b:start", "b:adopt:2", "c:start", "d:start", "d:adopt:3"], "one admission at a time, in admission order; a refused mutation releases the line");
75
+ expectEqual((await store.scanRecords()).map((r) => `${r.seq}:${r.id}`), ["1:a", "2:b", "3:d"], "records");
76
+ };
77
+ const delayedOldOwner = async ({ store: adapter }) => {
78
+ const { medium, store: oldOwner } = await claimedStore(adapter);
79
+ await oldOwner.commit(1, [append("old-1")], () => { });
80
+ const newOwner = await adapter.open(medium);
81
+ await newOwner.claim(1, 2);
82
+ await newOwner.commit(2, [append("new-1")], () => { });
83
+ const late = adoptLog();
84
+ await expectStoreError(oldOwner.commit(1, [append("old-2")], late.adopt), "stale-epoch", "old owner's delayed commit");
85
+ await expectStoreError(oldOwner.claim(1, 2), "stale-epoch", "old owner's delayed claim");
86
+ await expectStoreError(newOwner.commit(3, [append("x")], late.adopt), "stale-epoch", "a commit from a future epoch");
87
+ await expectStoreError(newOwner.claim(2, 2), "invalid-claim", "a claim that does not raise the epoch");
88
+ const fresh = await adapter.open(await adapter.createMedium());
89
+ await expectStoreError(fresh.commit(0, [append("x")], late.adopt), "unclaimed", "a commit to an unclaimed store");
90
+ expectEqual(late.calls, [], "no refused commit adopts");
91
+ expectEqual(await reopenSnapshot(adapter, medium), {
92
+ epoch: 2,
93
+ head: 2,
94
+ records: [stored(1, "old-1"), stored(2, "new-1")],
95
+ documents: [],
96
+ }, "the new owner's state");
97
+ };
98
+ const claimAndImport = async ({ store: adapter }) => {
99
+ const { store: source } = await claimedStore(adapter);
100
+ await source.commit(1, [append("r1"), append("r2"), put("doc", "v1")], () => { });
101
+ const first = await source.exportSnapshot();
102
+ const { medium, store: target } = await claimedStore(adapter, 2);
103
+ await expectStoreError(target.importSnapshot(1, first), "stale-epoch", "import under a non-current epoch");
104
+ await target.importSnapshot(2, first);
105
+ await target.importSnapshot(2, first);
106
+ expectEqual(await reopenSnapshot(adapter, medium), { ...first, epoch: 2 }, "import is whole and idempotent");
107
+ await source.commit(1, [append("r3"), put("doc", "v2")], () => { });
108
+ const tail = await source.exportSnapshot();
109
+ await target.importSnapshot(2, tail);
110
+ expectEqual(await reopenSnapshot(adapter, medium), { ...tail, epoch: 2 }, "the tail past the head is imported once");
111
+ const conflicting = { ...tail, records: [stored(1, "r1"), stored(2, "other"), stored(3, "r3")] };
112
+ await expectStoreError(target.importSnapshot(2, conflicting), "import-conflict", "a diverged log");
113
+ await expectStoreError(target.importSnapshot(2, { ...tail, epoch: 2 }), "invalid-claim", "an import not above the source");
114
+ expectEqual(await reopenSnapshot(adapter, medium), { ...tail, epoch: 2 }, "refused imports change nothing");
115
+ expectEqual(await target.commit(2, [append("r4")], () => { }), { seqs: [4], head: 4 }, "the new owner continues the log");
116
+ };
117
+ const claimRaisesEpochByOne = async ({ store: adapter }) => {
118
+ const { medium, store } = await claimedStore(adapter);
119
+ await store.commit(1, [append("r1")], () => { });
120
+ const baseline = await store.exportSnapshot();
121
+ const fresh = await adapter.open(await adapter.createMedium());
122
+ await expectStoreError(store.claim(1, 3), "invalid-claim", "a claim that skips an epoch");
123
+ await expectStoreError(fresh.claim(0, 2), "invalid-claim", "a first claim that skips epoch 1");
124
+ expectEqual(await fresh.readEpoch(), 0, "a refused first claim leaves the store unclaimed");
125
+ expectEqual(await reopenSnapshot(adapter, medium), baseline, "a refused claim leaves the epoch and log unchanged");
126
+ await store.claim(1, 2);
127
+ expectEqual(await store.readEpoch(), 2, "the next epoch is still claimable");
128
+ };
129
+ const idsScansAndDocuments = async ({ store: adapter }) => {
130
+ const { medium, store } = await claimedStore(adapter);
131
+ const reopened = await adapter.open(medium);
132
+ const elsewhere = await adapter.open(await adapter.createMedium());
133
+ const ids = [store, reopened, elsewhere].flatMap((s) => Array.from({ length: 100 }, () => s.mintId()));
134
+ expectEqual(new Set(ids).size, ids.length, "minted IDs are unique across instances, reopens and hosts");
135
+ const kinds = ["a", "b", "a", "b", "a"];
136
+ await store.commit(1, [...kinds.map((kind, i) => append(`r${i + 1}`, kind)), put("s/2", 2), put("s/1", 1), put("t/1", 3)], () => { });
137
+ const seqsOf = async (query) => (await store.scanRecords(query)).map((r) => r.seq);
138
+ expectEqual(await seqsOf(undefined), [1, 2, 3, 4, 5], "full scan");
139
+ expectEqual(await seqsOf({ fromSeq: 2, toSeq: 4 }), [2, 3, 4], "range scan");
140
+ expectEqual(await seqsOf({ kind: "b" }), [2, 4], "kind scan");
141
+ expectEqual(await seqsOf({ order: "desc", limit: 2 }), [5, 4], "descending, limited");
142
+ expectEqual(await seqsOf({ kind: "a", order: "desc", limit: 1 }), [5], "kind, descending, limited");
143
+ expectEqual(await store.scanDocuments("s/"), [
144
+ { key: "s/1", data: 1 },
145
+ { key: "s/2", data: 2 },
146
+ ], "prefix scan");
147
+ await store.commit(1, [{ op: "delete", key: "s/1" }], () => { });
148
+ expectEqual(await store.getDocument("s/1"), undefined, "a deleted document");
149
+ expectEqual((await store.scanDocuments("")).map((d) => d.key), ["s/2", "t/1"], "all documents by key");
150
+ await expectStoreError(store.commit(1, [append("r1")], () => { }), "duplicate-record-id", "an ID already in the log");
151
+ await expectStoreError(store.commit(1, [append("x"), append("x")], () => { }), "duplicate-record-id", "a repeated ID");
152
+ expectEqual(await store.readHead(), 5, "refused batches change nothing");
153
+ const value = { nested: { n: 1 } };
154
+ await store.commit(1, [put("v", value)], () => { });
155
+ value.nested.n = 2;
156
+ const read = await store.getDocument("v");
157
+ expectEqual(read, { nested: { n: 1 } }, "the store keeps its own copy of written values");
158
+ };
159
+ export const STORE_CASES = new Map([
160
+ ["STORE-01", commitAdoptsOnDurable],
161
+ ["STORE-02", cutAtEveryWritePoint],
162
+ ["STORE-03", concurrentAdmissions],
163
+ ["STORE-04", delayedOldOwner],
164
+ ["STORE-05", claimAndImport],
165
+ ["STORE-06", idsScansAndDocuments],
166
+ ["STORE-07", claimRaisesEpochByOne],
167
+ ]);
168
+ //# sourceMappingURL=store-cases.js.map
@@ -0,0 +1,23 @@
1
+ import type { DurableStore, DurableStoreErrorCode, DurableStoreOptions, StoreSnapshot, StoreWritePoint } from "../store.ts";
2
+ /** What a host provides to the STORE and KILL-STORE cases. */
3
+ export interface StoreConformanceAdapter {
4
+ /** A new, empty durable medium (a Durable Object's storage, an SQLite file); its name opens it again. */
5
+ createMedium(): Promise<string>;
6
+ /** A store instance over `medium`. Instances opened on one medium share its durable state. */
7
+ open(medium: string, options?: DurableStoreOptions): Promise<DurableStore>;
8
+ }
9
+ export interface StoreCaseAdapters {
10
+ readonly store: StoreConformanceAdapter;
11
+ }
12
+ export declare function cutOnceAt(target: StoreWritePoint): DurableStoreOptions;
13
+ export declare function expectEqual(actual: unknown, expected: unknown, what: string): void;
14
+ export declare function expectStoreError(operation: Promise<unknown>, code: DurableStoreErrorCode, what: string): Promise<void>;
15
+ export declare function expectCut(operation: Promise<unknown>, what: string): Promise<void>;
16
+ export declare function reopenSnapshot(adapter: StoreConformanceAdapter, medium: string): Promise<StoreSnapshot>;
17
+ export declare function adoptLog(): {
18
+ readonly calls: string[];
19
+ adopt: (result: {
20
+ readonly seqs: readonly number[];
21
+ }) => void;
22
+ };
23
+ //# sourceMappingURL=store-harness.d.ts.map
@@ -0,0 +1,64 @@
1
+ import { DurableStoreError } from "../store.js";
2
+ class CutSignal extends Error {
3
+ constructor(point) {
4
+ super(`cut at ${JSON.stringify(point)}`);
5
+ this.name = "CutSignal";
6
+ }
7
+ }
8
+ const samePoint = (a, b) => a.op === b.op && a.at === b.at && (a.at !== "write" || (b.at === "write" && a.index === b.index));
9
+ export function cutOnceAt(target) {
10
+ let armed = true;
11
+ return {
12
+ onWritePoint: (point) => {
13
+ if (armed && samePoint(point, target)) {
14
+ armed = false;
15
+ throw new CutSignal(point);
16
+ }
17
+ },
18
+ };
19
+ }
20
+ function canonical(value) {
21
+ if (Array.isArray(value))
22
+ return `[${value.map(canonical).join(",")}]`;
23
+ if (value !== null && typeof value === "object") {
24
+ const entries = Object.entries(value).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
25
+ return `{${entries.map(([key, inner]) => `${JSON.stringify(key)}:${canonical(inner)}`).join(",")}}`;
26
+ }
27
+ return JSON.stringify(value) ?? "undefined";
28
+ }
29
+ export function expectEqual(actual, expected, what) {
30
+ const a = canonical(actual);
31
+ const e = canonical(expected);
32
+ if (a !== e)
33
+ throw new Error(`${what}: expected ${e}, got ${a}`);
34
+ }
35
+ export async function expectStoreError(operation, code, what) {
36
+ try {
37
+ await operation;
38
+ }
39
+ catch (error) {
40
+ if (error instanceof DurableStoreError && error.code === code)
41
+ return;
42
+ throw new Error(`${what}: expected DurableStoreError ${code}, got ${String(error)}`);
43
+ }
44
+ throw new Error(`${what}: expected DurableStoreError ${code}, but it resolved`);
45
+ }
46
+ export async function expectCut(operation, what) {
47
+ try {
48
+ await operation;
49
+ }
50
+ catch (error) {
51
+ if (error instanceof CutSignal)
52
+ return;
53
+ throw new Error(`${what}: expected the cut to surface, got ${String(error)}`);
54
+ }
55
+ throw new Error(`${what}: expected the cut to surface, but it resolved`);
56
+ }
57
+ export async function reopenSnapshot(adapter, medium) {
58
+ return (await adapter.open(medium)).exportSnapshot();
59
+ }
60
+ export function adoptLog() {
61
+ const calls = [];
62
+ return { calls, adopt: (result) => void calls.push(`adopt:${result.seqs.join(",")}`) };
63
+ }
64
+ //# sourceMappingURL=store-harness.js.map
@@ -0,0 +1,6 @@
1
+ import type { CaseImplementation } from "../../conformance/runner.ts";
2
+ import { type WakeCaseAdapters } from "./wake-harness.ts";
3
+ type WakeCase = CaseImplementation<WakeCaseAdapters>;
4
+ export declare const WAKE_CASES: ReadonlyMap<string, WakeCase>;
5
+ export {};
6
+ //# sourceMappingURL=wake-cases.d.ts.map