@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 +13 -2
- package/dist/index.d.ts +42 -5
- package/dist/index.js +238 -16
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
1328
|
-
|
|
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:
|
|
1338
|
-
await
|
|
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,
|
|
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
|