@alma-harness/testing 0.1.0 → 0.3.0

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.
package/README.md CHANGED
@@ -3,7 +3,8 @@
3
3
  The shared storage contract suites of [Alma](https://github.com/FabioFernandesCarneiro/alma)
4
4
  — what "correct" means for every storage adapter, on any backend.
5
5
 
6
- > **Status: pre-release.** Not yet published to npm.
6
+ > **Status: 0.2.0 on npm, pre-1.0.** The API is still moving; see the
7
+ > [roadmap](../../docs/architecture.md#12-adoption-roadmap) for where it stands.
7
8
 
8
9
  ## What it owns
9
10
 
@@ -12,10 +13,20 @@ The shared storage contract suites of [Alma](https://github.com/FabioFernandesCa
12
13
  storage contracts: deterministic ids, three states never conflated, the
13
14
  three-case confidence rule, identity never extracted, a budget on every read.
14
15
  - `describeMemoryErasureContract` — the LGPD/GDPR story as a test: content
15
- gone, derived data invalidated, every declared copy surface reached.
16
+ gone, derived data invalidated, every declared copy surface reached. The
17
+ fixture's `watermarks` and `accessEvents` are required: six cases used to
18
+ return silently without them, green on an in-flight guard and a trail the
19
+ adapter never proved (spec: erasure-reaches-the-claims).
16
20
  - `describeSpendStoreContract` — the `SpendStore` contract (spec: spend-store):
17
21
  atomic increment-and-return, UTC day bucketing, org-wide day aggregation,
18
22
  no lost increments under concurrency.
23
+ - `describeRoutineStoreContract` — the `RoutineStore` contract (spec:
24
+ clock-tick): a routine round-trips with its registration stamp, the stamp
25
+ survives re-registration, cancel removes, `list` sees every scope.
26
+ - `describeRoutineRunStoreContract` — the `RoutineRunStore` contract (spec:
27
+ postgres-routine-runs): a run round-trips whole, upserts by key, lists
28
+ newest first with an inclusive `since` that compares instants, and is
29
+ isolated by routine, uid and org.
19
30
  - `describeTurnStoreContract` — the `TurnStore` contract (spec 030): exactly one
20
31
  winner per session, leases that expire so a crashed holder never blocks
21
32
  forever, a stale release that touches nothing, claims that replay a finished
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { SessionStore, EpisodeStore, ProfileStore, MemoryErasure, ErasureWatermarkStore, AccessEvent, SpendStore, TurnStore } from '@alma-harness/core';
1
+ import { SessionStore, EpisodeStore, ProfileStore, MemoryErasure, ErasureWatermarkStore, AccessEvent, SpendStore, TurnStore, RoutineRunStore, RoutineStore } from '@alma-harness/core';
2
2
 
3
3
  /**
4
4
  * Shared `SessionStore` contract suite — §6: storage contracts are
@@ -72,9 +72,15 @@ interface MemoryErasureFixture {
72
72
  erasure: MemoryErasure;
73
73
  episodes: EpisodeStore;
74
74
  profile: ProfileStore;
75
- watermarks?: ErasureWatermarkStore;
75
+ /**
76
+ * REQUIRED — spec: erasure-reaches-the-claims. Both used to be optional and
77
+ * six cases returned silently without them, so an adapter with no in-flight
78
+ * guard and no trail was green on both. A fixture that cannot supply them
79
+ * has nothing to prove here.
80
+ */
81
+ watermarks: ErasureWatermarkStore;
76
82
  /** Events the wired `AuditLog` received — the erasure trail (§6.8, §10). */
77
- accessEvents?: readonly AccessEvent[];
83
+ accessEvents: readonly AccessEvent[];
78
84
  }
79
85
  declare function describeMemoryErasureContract(name: string, factory: MemoryErasureFactory): void;
80
86
 
@@ -109,7 +115,8 @@ declare function describeSpendStoreContract(name: string, factory: SpendStoreFac
109
115
  * Timing is exercised with REAL time and small TTLs, deliberately. A fake
110
116
  * clock cannot reach a Postgres `now()`, and one that desynchronizes from the
111
117
  * `setTimeout` a wait is built on makes the reference store and the adapter
112
- * prove different things.
118
+ * prove different things. An expiry is waited for through `acquire`'s own
119
+ * `waitMs`, never a fixed sleep (spec: close-review-part-two).
113
120
  *
114
121
  * ```ts
115
122
  * describeTurnStoreContract("PostgresTurnStore", { create: () => … });
@@ -122,4 +129,34 @@ interface TurnStoreFactory {
122
129
  }
123
130
  declare function describeTurnStoreContract(name: string, factory: TurnStoreFactory): void;
124
131
 
125
- export { type EpisodeStoreFactory, type MemoryErasureFactory, type MemoryErasureFixture, type ProfileStoreFactory, type SessionStoreFactory, type SpendStoreFactory, type TurnStoreFactory, describeEpisodeStoreContract, describeMemoryErasureContract, describeProfileStoreContract, describeSessionStoreContract, describeSpendStoreContract, describeTurnStoreContract };
132
+ /**
133
+ * Shared `RoutineRunStore` contract suite — spec: postgres-routine-runs. Pins
134
+ * the three reads the routine runner relies on (spec: routine-runner): the
135
+ * same fire again, today's runs for the ceiling, the last run of an outcome
136
+ * for the dedupe — and that a record is metadata that round-trips whole.
137
+ *
138
+ * ```ts
139
+ * describeRoutineRunStoreContract("PostgresRoutineRunStore", { create: () => … });
140
+ * ```
141
+ */
142
+ interface RoutineRunStoreFactory {
143
+ /** Must return a store holding NO runs for the scopes below. */
144
+ create(): Promise<RoutineRunStore> | RoutineRunStore;
145
+ destroy?(store: RoutineRunStore): Promise<void> | void;
146
+ }
147
+ declare function describeRoutineRunStoreContract(name: string, factory: RoutineRunStoreFactory): void;
148
+
149
+ /**
150
+ * Shared `RoutineStore` contract suite — spec: clock-tick. What the tick
151
+ * relies on: a routine round-trips whole with its registration stamp, the
152
+ * stamp survives re-registration, cancel removes, and `list` sees every
153
+ * scope while `get`/`cancel` are exact.
154
+ */
155
+ interface RoutineStoreFactory {
156
+ /** Must return a store holding NO routines. */
157
+ create(): Promise<RoutineStore> | RoutineStore;
158
+ destroy?(store: RoutineStore): Promise<void> | void;
159
+ }
160
+ declare function describeRoutineStoreContract(name: string, factory: RoutineStoreFactory): void;
161
+
162
+ export { type EpisodeStoreFactory, type MemoryErasureFactory, type MemoryErasureFixture, type ProfileStoreFactory, type RoutineRunStoreFactory, type RoutineStoreFactory, type SessionStoreFactory, type SpendStoreFactory, type TurnStoreFactory, describeEpisodeStoreContract, describeMemoryErasureContract, describeProfileStoreContract, describeRoutineRunStoreContract, describeRoutineStoreContract, describeSessionStoreContract, describeSpendStoreContract, describeTurnStoreContract };
package/dist/index.js CHANGED
@@ -65,6 +65,18 @@ function describeSessionStoreContract(name, factory) {
65
65
  await store.append(ALICE, "s1", [text("assistant", "4")]);
66
66
  expect(bodies(await store.load(ALICE, "s1"))).toEqual(["1", "2", "3", "4"]);
67
67
  });
68
+ it("lands every message of concurrent appends on one session exactly once", async () => {
69
+ await Promise.all(
70
+ Array.from(
71
+ { length: 12 },
72
+ (_, i) => store.append(ALICE, "s1", [text("user", `q${i}`), text("assistant", `a${i}`)])
73
+ )
74
+ );
75
+ const seen = bodies(await store.load(ALICE, "s1")).map(String);
76
+ expect(seen).toHaveLength(24);
77
+ expect(new Set(seen).size).toBe(24);
78
+ for (let i = 0; i < seen.length; i += 2) expect(seen[i + 1]).toBe(seen[i].replace("q", "a"));
79
+ });
68
80
  it("resolves an unknown session to an empty history", async () => {
69
81
  await expect(store.load(ALICE, "never-created")).resolves.toEqual([]);
70
82
  });
@@ -141,6 +153,37 @@ function describeSessionStoreContract(name, factory) {
141
153
  expect(flat).not.toContain("tool_call");
142
154
  expect(flat).not.toContain("tool_result");
143
155
  });
156
+ it("expires reasoning blocks with the tool traffic \u2014 backstage, not what the person saw", async () => {
157
+ await store.append(ALICE, "s1", [
158
+ {
159
+ role: "assistant",
160
+ blocks: [
161
+ { type: "reasoning", provider: "anthropic", text: "the patient asked twice", opaque: { signature: "sig" } },
162
+ { type: "tool_call", id: "c1", name: "list", input: {} }
163
+ ],
164
+ meta: { at: "2026-01-01T00:00:00.000Z" }
165
+ },
166
+ {
167
+ role: "tool",
168
+ blocks: [{ type: "tool_result", callId: "c1", output: "two" }],
169
+ meta: { at: "2026-01-01T00:00:00.000Z" }
170
+ },
171
+ {
172
+ role: "assistant",
173
+ blocks: [
174
+ { type: "reasoning", provider: "anthropic", text: "answer briefly", opaque: { signature: "sig2" } },
175
+ { type: "text", text: "you have two" }
176
+ ],
177
+ meta: { at: "2026-01-01T00:00:00.000Z" }
178
+ }
179
+ ]);
180
+ const report = await store.expireToolTraffic(ALICE, "s1", {
181
+ inactiveSince: "2026-06-01T00:00:00.000Z"
182
+ });
183
+ expect(report).toEqual({ blocks: 4, messages: 2, expired: true });
184
+ expect(kinds(await store.load(ALICE, "s1"))).toEqual([["text"]]);
185
+ expect(JSON.stringify(await store.load(ALICE, "s1"))).not.toContain("the patient asked twice");
186
+ });
144
187
  it("keeps media blocks \u2014 a pointer the user saw, not tool traffic", async () => {
145
188
  await store.append(ALICE, "s1", [
146
189
  {
@@ -364,6 +407,12 @@ function describeEpisodeStoreContract(name, factory) {
364
407
  const { episodes } = await store.query(ALICE2, { text: "kayaking" });
365
408
  expect2(summaries(episodes)).toEqual(["loves kayaking"]);
366
409
  });
410
+ it2("finds an inflected form: a query in the singular matches a summary in the plural", async () => {
411
+ await store.append(ALICE2, { kind: "note", summary: "duas reuni\xF5es marcadas para ter\xE7a" });
412
+ await store.append(ALICE2, { kind: "note", summary: "nada a ver com isso", dedupeKey: "other" });
413
+ const { episodes } = await store.query(ALICE2, { text: "reuni\xE3o" });
414
+ expect2(summaries(episodes)).toEqual(["duas reuni\xF5es marcadas para ter\xE7a"]);
415
+ });
367
416
  it2("finds a term deep in a long summary, past the query tokenizer's cap", async () => {
368
417
  const filler = Array.from({ length: 40 }, (_, i) => `filler${i}`).join(" ");
369
418
  await store.append(ALICE2, { kind: "note", summary: `${filler} chocolate` });
@@ -1021,7 +1070,6 @@ function describeMemoryErasureContract(name, factory) {
1021
1070
  expect4(report.complete).toBe(true);
1022
1071
  });
1023
1072
  it4("stamps the scope's erasure watermark for the in-flight job guard", async () => {
1024
- if (!fx.watermarks) return;
1025
1073
  await expect4(fx.watermarks.get(ALICE4)).resolves.toBeNull();
1026
1074
  const report = await fx.erasure.erase(ALICE4, { kind: "all" });
1027
1075
  await expect4(fx.watermarks.get(ALICE4)).resolves.toBe(report.erasedAt);
@@ -1034,10 +1082,9 @@ function describeMemoryErasureContract(name, factory) {
1034
1082
  await fx.erasure.erase(ALICE4, { kind: "all" });
1035
1083
  expect4((await fx.episodes.get(TWIN, [twin.id]))[0]).toMatchObject({ state: "active" });
1036
1084
  expect4((await fx.profile.get(TWIN)).facts).toHaveLength(1);
1037
- if (fx.watermarks) await expect4(fx.watermarks.get(TWIN)).resolves.toBeNull();
1085
+ await expect4(fx.watermarks.get(TWIN)).resolves.toBeNull();
1038
1086
  });
1039
1087
  it4("never moves the watermark backwards, whatever clock stamps the second erasure", async () => {
1040
- if (!fx.watermarks) return;
1041
1088
  await fx.watermarks.set(ALICE4, "2026-06-01T00:00:10.000Z");
1042
1089
  await fx.watermarks.set(ALICE4, "2026-06-01T00:00:00.000Z");
1043
1090
  await expect4(fx.watermarks.get(ALICE4)).resolves.toBe("2026-06-01T00:00:10.000Z");
@@ -1077,7 +1124,6 @@ function describeMemoryErasureContract(name, factory) {
1077
1124
  await expect4(fx.profile.get(ALICE4)).resolves.toMatchObject({ facts: [] });
1078
1125
  });
1079
1126
  it4("refuses an observation submitted from before the erasure", async () => {
1080
- if (!fx.watermarks) return;
1081
1127
  const before = new Date(Date.now() - 6e4).toISOString();
1082
1128
  await fx.erasure.erase(ALICE4, { kind: "all" });
1083
1129
  const [result] = await fx.profile.observe(ALICE4, [
@@ -1097,7 +1143,6 @@ function describeMemoryErasureContract(name, factory) {
1097
1143
  await expect4(fx.profile.get(ALICE4)).resolves.toMatchObject({ facts: [] });
1098
1144
  });
1099
1145
  it4("refuses an episode stamped before the erasure, not only a fact", async () => {
1100
- if (!fx.watermarks) return;
1101
1146
  const before = new Date(Date.now() - 6e4).toISOString();
1102
1147
  await fx.erasure.erase(ALICE4, { kind: "all" });
1103
1148
  await expect4(
@@ -1105,7 +1150,6 @@ function describeMemoryErasureContract(name, factory) {
1105
1150
  ).rejects.toThrow();
1106
1151
  });
1107
1152
  it4("records a trail even when the erasure fails partway", async () => {
1108
- if (!fx.accessEvents) return;
1109
1153
  const before = fx.accessEvents.length;
1110
1154
  await expect4(
1111
1155
  fx.erasure.erase(ALICE4, {
@@ -1114,10 +1158,9 @@ function describeMemoryErasureContract(name, factory) {
1114
1158
  })
1115
1159
  ).rejects.toThrow();
1116
1160
  expect4(fx.accessEvents.length).toBe(before);
1117
- if (fx.watermarks) await expect4(fx.watermarks.get(ALICE4)).resolves.toBeNull();
1161
+ await expect4(fx.watermarks.get(ALICE4)).resolves.toBeNull();
1118
1162
  });
1119
1163
  it4("leaves an audit trail proving the erasure ran, carrying no content", async () => {
1120
- if (!fx.accessEvents) return;
1121
1164
  await seedSupportedFact(ALICE4, "s1", "zeta");
1122
1165
  await fx.erasure.erase(ALICE4, { kind: "all" });
1123
1166
  const erasures = fx.accessEvents.filter((e) => e.action === "delete");
@@ -1324,9 +1367,8 @@ function describeTurnStoreContract(name, factory) {
1324
1367
  await expect6(store.acquire(ALICE6, "s1", NEVER_EXPIRES)).resolves.not.toBeNull();
1325
1368
  });
1326
1369
  it6("ignores a STALE release instead of freeing the current holder's session", async () => {
1327
- const stale = await store.acquire(ALICE6, "s1", { ttlMs: 20, waitMs: 0 });
1328
- await new Promise((r) => setTimeout(r, 60));
1329
- const current = await store.acquire(ALICE6, "s1", { ttlMs: 6e4, waitMs: 200 });
1370
+ const stale = await store.acquire(ALICE6, "s1", { ttlMs: 100, waitMs: 0 });
1371
+ const current = await store.acquire(ALICE6, "s1", { ttlMs: 6e4, waitMs: 2e3 });
1330
1372
  expect6(current.token).not.toBe(stale.token);
1331
1373
  await store.release(ALICE6, "s1", stale);
1332
1374
  await expect6(store.acquire(ALICE6, "s1", NEVER_EXPIRES)).resolves.toBeNull();
@@ -1334,9 +1376,8 @@ function describeTurnStoreContract(name, factory) {
1334
1376
  await expect6(store.acquire(ALICE6, "s1", NEVER_EXPIRES)).resolves.not.toBeNull();
1335
1377
  });
1336
1378
  it6("lets a later caller take a lease whose TTL expired \u2014 a crashed holder never blocks a session forever", async () => {
1337
- await store.acquire(ALICE6, "s1", { ttlMs: 20, waitMs: 0 });
1338
- await new Promise((r) => setTimeout(r, 60));
1339
- await expect6(store.acquire(ALICE6, "s1", NEVER_EXPIRES)).resolves.not.toBeNull();
1379
+ await store.acquire(ALICE6, "s1", { ttlMs: 100, waitMs: 0 });
1380
+ await expect6(store.acquire(ALICE6, "s1", { ttlMs: 6e4, waitMs: 2e3 })).resolves.not.toBeNull();
1340
1381
  });
1341
1382
  it6("WAITS for a busy session and takes it when the holder releases inside the window", async () => {
1342
1383
  const lease = await store.acquire(ALICE6, "s1", { ttlMs: 6e4, waitMs: 0 });
@@ -1435,16 +1476,18 @@ function describeTurnStoreContract(name, factory) {
1435
1476
  await store.complete(k, completedTurn());
1436
1477
  await expect6(store.claim(k)).resolves.toEqual({ status: "fresh" });
1437
1478
  });
1438
- it6("erases one session's claims and lease, leaving its siblings alone", async () => {
1479
+ it6("erases one session's claims and leaves its lease to the holder, so a turn in flight is not interleaved", async () => {
1439
1480
  const target = key(ALICE6, "s1", "wamid-3");
1440
1481
  const sibling = key(ALICE6, "s2", "wamid-3");
1441
1482
  for (const k of [target, sibling]) {
1442
1483
  await store.claim(k);
1443
1484
  await store.complete(k, completedTurn());
1444
1485
  }
1445
- await store.acquire(ALICE6, "s1", NEVER_EXPIRES);
1486
+ const held = await store.acquire(ALICE6, "s1", NEVER_EXPIRES);
1446
1487
  await store.erase(ALICE6, "s1");
1447
1488
  await expect6(store.claim(target)).resolves.toEqual({ status: "fresh" });
1489
+ await expect6(store.acquire(ALICE6, "s1", NEVER_EXPIRES)).resolves.toBeNull();
1490
+ await store.release(ALICE6, "s1", held);
1448
1491
  await expect6(store.acquire(ALICE6, "s1", NEVER_EXPIRES)).resolves.not.toBeNull();
1449
1492
  const siblingClaim = await store.claim(sibling);
1450
1493
  expect6(siblingClaim.status).toBe("replay");
@@ -1499,10 +1542,189 @@ function describeTurnStoreContract(name, factory) {
1499
1542
  });
1500
1543
  });
1501
1544
  }
1545
+
1546
+ // src/routine-run-store-contract.ts
1547
+ import { afterEach as afterEach7, beforeEach as beforeEach7, describe as describe7, expect as expect7, it as it7 } from "vitest";
1548
+ var ALICE7 = { org: "org-a", uid: "user-alice" };
1549
+ var BOB7 = { org: "org-a", uid: "user-bob" };
1550
+ var OTHER_ORG5 = { org: "org-b", uid: "user-alice" };
1551
+ var T4 = {
1552
+ early: "2026-08-24T08:00:00.000Z",
1553
+ late: "2026-08-24T20:00:00.000Z",
1554
+ nextDay: "2026-08-25T09:00:00.000Z",
1555
+ /** 01:00+02:00 on the 25th IS 23:00Z on the 24th. */
1556
+ lateByOffset: "2026-08-25T01:00:00+02:00"
1557
+ };
1558
+ function run(over) {
1559
+ return { routineId: "briefing", scope: ALICE7, outcome: "delivered", costUsd: 0.01, ...over };
1560
+ }
1561
+ function describeRoutineRunStoreContract(name, factory) {
1562
+ describe7(`RoutineRunStore contract: ${name}`, () => {
1563
+ let store;
1564
+ beforeEach7(async () => {
1565
+ store = await factory.create();
1566
+ });
1567
+ afterEach7(async () => {
1568
+ await factory.destroy?.(store);
1569
+ });
1570
+ it7("round-trips a run whole, optional fields present when set and absent when not", async () => {
1571
+ const full = run({
1572
+ id: "r1",
1573
+ startedAt: T4.early,
1574
+ finishedAt: T4.late,
1575
+ outcome: "submitted",
1576
+ reason: "why",
1577
+ costUsd: 2e-6,
1578
+ sessionId: "s1",
1579
+ turnId: "t1",
1580
+ handle: { provider: "openai", id: "batch_1", model: { provider: "openai", id: "m" } },
1581
+ deliveryHash: "abc"
1582
+ });
1583
+ const bare = run({ id: "r2", startedAt: T4.late });
1584
+ await store.record(full);
1585
+ await store.record(bare);
1586
+ expect7(await store.get(ALICE7, "briefing", "r1")).toEqual(full);
1587
+ expect7(await store.get(ALICE7, "briefing", "r2")).toEqual(bare);
1588
+ });
1589
+ it7("upserts by (scope, routine, run): a second record replaces the first", async () => {
1590
+ await store.record(run({ id: "r1", startedAt: T4.early, outcome: "submitted" }));
1591
+ await store.record(run({ id: "r1", startedAt: T4.early, outcome: "delivered", finishedAt: T4.late, deliveryHash: "h" }));
1592
+ expect7(await store.get(ALICE7, "briefing", "r1")).toMatchObject({ outcome: "delivered", deliveryHash: "h" });
1593
+ expect7(await store.list(ALICE7, "briefing")).toHaveLength(1);
1594
+ });
1595
+ it7("resolves an unknown run to null", async () => {
1596
+ await expect7(store.get(ALICE7, "briefing", "never")).resolves.toBeNull();
1597
+ });
1598
+ it7("lists newest first; since is inclusive and compares instants; outcome filters; limit caps", async () => {
1599
+ await store.record(run({ id: "early", startedAt: T4.early }));
1600
+ await store.record(run({ id: "late", startedAt: T4.late, outcome: "duplicate" }));
1601
+ await store.record(run({ id: "offset", startedAt: T4.lateByOffset }));
1602
+ await store.record(run({ id: "next", startedAt: T4.nextDay, outcome: "refused" }));
1603
+ expect7((await store.list(ALICE7, "briefing")).map((r) => r.id)).toEqual(["next", "offset", "late", "early"]);
1604
+ expect7((await store.list(ALICE7, "briefing", { since: T4.late })).map((r) => r.id)).toEqual(["next", "offset", "late"]);
1605
+ expect7((await store.list(ALICE7, "briefing", { since: "2026-08-24T00:00:00.000Z" })).map((r) => r.id)).toEqual(["next", "offset", "late", "early"]);
1606
+ expect7((await store.list(ALICE7, "briefing", { since: "2026-08-25T00:00:00.000Z" })).map((r) => r.id)).toEqual(["next"]);
1607
+ expect7((await store.list(ALICE7, "briefing", { outcome: "delivered", limit: 1 })).map((r) => r.id)).toEqual(["offset"]);
1608
+ expect7((await store.list(ALICE7, "briefing", { outcome: "refused" })).map((r) => r.id)).toEqual(["next"]);
1609
+ expect7(await store.list(ALICE7, "briefing", { limit: 0 })).toEqual([]);
1610
+ });
1611
+ it7("orders two runs at the same instant by id, on every backend", async () => {
1612
+ await store.record(run({ id: "b", startedAt: T4.late }));
1613
+ await store.record(run({ id: "a", startedAt: T4.late }));
1614
+ await store.record(run({ id: "c", startedAt: T4.early }));
1615
+ expect7((await store.list(ALICE7, "briefing")).map((r) => r.id)).toEqual(["a", "b", "c"]);
1616
+ expect7((await store.list(ALICE7, "briefing", { limit: 1 })).map((r) => r.id)).toEqual(["a"]);
1617
+ });
1618
+ it7("isolates runs by routine, uid and org", async () => {
1619
+ await store.record(run({ id: "r1", startedAt: T4.early }));
1620
+ await store.record(run({ id: "r1", startedAt: T4.early, routineId: "closing" }));
1621
+ await store.record(run({ id: "r1", startedAt: T4.early, scope: BOB7 }));
1622
+ await store.record(run({ id: "r1", startedAt: T4.early, scope: OTHER_ORG5 }));
1623
+ expect7(await store.list(ALICE7, "briefing")).toHaveLength(1);
1624
+ expect7(await store.list(ALICE7, "closing")).toHaveLength(1);
1625
+ expect7(await store.list(BOB7, "briefing")).toHaveLength(1);
1626
+ expect7(await store.list(OTHER_ORG5, "briefing")).toHaveLength(1);
1627
+ await expect7(store.get(ALICE7, "nothing", "r1")).resolves.toBeNull();
1628
+ });
1629
+ it7("rejects an invalid scope on every surface", async () => {
1630
+ const bad = { org: "../evil", uid: "user-1" };
1631
+ await expect7(store.record(run({ id: "r1", startedAt: T4.early, scope: bad }))).rejects.toThrow();
1632
+ await expect7(store.get(bad, "briefing", "r1")).rejects.toThrow();
1633
+ await expect7(store.list(bad, "briefing")).rejects.toThrow();
1634
+ });
1635
+ it7("rejects an unparseable since or startedAt rather than sorting it to the epoch", async () => {
1636
+ await expect7(store.record(run({ id: "r1", startedAt: "not a date" }))).rejects.toThrow();
1637
+ await store.record(run({ id: "r2", startedAt: T4.early }));
1638
+ await expect7(store.list(ALICE7, "briefing", { since: "not a date" })).rejects.toThrow();
1639
+ });
1640
+ });
1641
+ }
1642
+
1643
+ // src/routine-store-contract.ts
1644
+ import { afterEach as afterEach8, beforeEach as beforeEach8, describe as describe8, expect as expect8, it as it8 } from "vitest";
1645
+ var ALICE8 = { org: "org-a", uid: "user-alice" };
1646
+ var BOB8 = { org: "org-a", uid: "user-bob" };
1647
+ var OTHER_ORG6 = { org: "org-b", uid: "user-alice" };
1648
+ function routine(over) {
1649
+ return {
1650
+ scope: ALICE8,
1651
+ schedule: { cron: "0 8 * * 1-5", tz: "America/Sao_Paulo" },
1652
+ goal: "prepare the briefing",
1653
+ intent: { tier: "mechanical", sensitivity: "internal" },
1654
+ toolProfile: "briefing",
1655
+ budget: { perTurnUsd: 0.2 },
1656
+ outputSink: "inbox",
1657
+ maxRunsPerDay: 2,
1658
+ ...over
1659
+ };
1660
+ }
1661
+ function describeRoutineStoreContract(name, factory) {
1662
+ describe8(`RoutineStore contract: ${name}`, () => {
1663
+ let store;
1664
+ beforeEach8(async () => {
1665
+ store = await factory.create();
1666
+ });
1667
+ afterEach8(async () => {
1668
+ await factory.destroy?.(store);
1669
+ });
1670
+ it8("registers a routine stamped with the time, round-trips it whole, and keeps the anchor on re-registration", async () => {
1671
+ const before = Date.now();
1672
+ await store.register(routine({ id: "briefing" }));
1673
+ const stored = await store.get(ALICE8, "briefing");
1674
+ expect8(stored).toMatchObject(routine({ id: "briefing" }));
1675
+ const at = Date.parse(stored.registeredAt);
1676
+ expect8(Number.isNaN(at)).toBe(false);
1677
+ expect8(at).toBeGreaterThanOrEqual(before - 1e3);
1678
+ await store.register(routine({ id: "briefing", goal: "prepare the briefing, shorter" }));
1679
+ const edited = await store.get(ALICE8, "briefing");
1680
+ expect8(edited).toMatchObject({ goal: "prepare the briefing, shorter", registeredAt: stored.registeredAt });
1681
+ });
1682
+ it8("keeps a supplied registration stamp on first registration \u2014 a migration keeps its anchors \u2014 and ignores it on re-registration", async () => {
1683
+ await store.register({ ...routine({ id: "moved" }), registeredAt: "2026-01-01T00:00:00.000Z" });
1684
+ expect8((await store.get(ALICE8, "moved"))?.registeredAt).toBe("2026-01-01T00:00:00.000Z");
1685
+ await store.register({ ...routine({ id: "moved" }), registeredAt: "2026-06-01T00:00:00.000Z" });
1686
+ expect8((await store.get(ALICE8, "moved"))?.registeredAt).toBe("2026-01-01T00:00:00.000Z");
1687
+ await expect8(store.register({ ...routine({ id: "bad" }), registeredAt: "not a date" })).rejects.toThrow();
1688
+ });
1689
+ it8("cancels a routine, after which get is null and list omits it", async () => {
1690
+ await store.register(routine({ id: "briefing" }));
1691
+ await store.register(routine({ id: "closing", execution: "job", toolProfile: void 0 }));
1692
+ await store.cancel(ALICE8, "briefing");
1693
+ await expect8(store.get(ALICE8, "briefing")).resolves.toBeNull();
1694
+ expect8((await store.list()).map((r) => r.id)).toEqual(["closing"]);
1695
+ await expect8(store.cancel(ALICE8, "briefing")).resolves.toBeUndefined();
1696
+ });
1697
+ it8("lists every routine of every scope \u2014 the tick is a deployment read", async () => {
1698
+ await store.register(routine({ id: "briefing" }));
1699
+ await store.register(routine({ id: "briefing", scope: BOB8 }));
1700
+ await store.register(routine({ id: "briefing", scope: OTHER_ORG6 }));
1701
+ const all = await store.list();
1702
+ expect8(all).toHaveLength(3);
1703
+ expect8(all.map((r) => `${r.scope.org}/${r.scope.uid}`).sort()).toEqual(["org-a/user-alice", "org-a/user-bob", "org-b/user-alice"]);
1704
+ });
1705
+ it8("isolates get and cancel by scope and id", async () => {
1706
+ await store.register(routine({ id: "briefing" }));
1707
+ await store.register(routine({ id: "briefing", scope: BOB8, goal: "bob's" }));
1708
+ await store.cancel(OTHER_ORG6, "briefing");
1709
+ await store.cancel(ALICE8, "closing");
1710
+ expect8((await store.get(ALICE8, "briefing"))?.goal).toBe("prepare the briefing");
1711
+ expect8((await store.get(BOB8, "briefing"))?.goal).toBe("bob's");
1712
+ await expect8(store.get(OTHER_ORG6, "briefing")).resolves.toBeNull();
1713
+ });
1714
+ it8("rejects an invalid scope on every surface", async () => {
1715
+ const bad = { org: "../evil", uid: "user-1" };
1716
+ await expect8(store.register(routine({ id: "r", scope: bad }))).rejects.toThrow();
1717
+ await expect8(store.get(bad, "r")).rejects.toThrow();
1718
+ await expect8(store.cancel(bad, "r")).rejects.toThrow();
1719
+ });
1720
+ });
1721
+ }
1502
1722
  export {
1503
1723
  describeEpisodeStoreContract,
1504
1724
  describeMemoryErasureContract,
1505
1725
  describeProfileStoreContract,
1726
+ describeRoutineRunStoreContract,
1727
+ describeRoutineStoreContract,
1506
1728
  describeSessionStoreContract,
1507
1729
  describeSpendStoreContract,
1508
1730
  describeTurnStoreContract