@b4run/testing 0.8.28

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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/aimock-runner.d.ts +48 -0
  4. package/dist/aimock-runner.d.ts.map +1 -0
  5. package/dist/aimock-runner.js +81 -0
  6. package/dist/checkpointer-conformance.d.ts +43 -0
  7. package/dist/checkpointer-conformance.d.ts.map +1 -0
  8. package/dist/checkpointer-conformance.js +381 -0
  9. package/dist/fake-embedder.d.ts +9 -0
  10. package/dist/fake-embedder.d.ts.map +1 -0
  11. package/dist/fake-embedder.js +38 -0
  12. package/dist/fixture-builder.d.ts +29 -0
  13. package/dist/fixture-builder.d.ts.map +1 -0
  14. package/dist/fixture-builder.js +43 -0
  15. package/dist/fixture-file.d.ts +14 -0
  16. package/dist/fixture-file.d.ts.map +1 -0
  17. package/dist/fixture-file.js +49 -0
  18. package/dist/harness.d.ts +35 -0
  19. package/dist/harness.d.ts.map +1 -0
  20. package/dist/harness.js +211 -0
  21. package/dist/http-inject.d.ts +22 -0
  22. package/dist/http-inject.d.ts.map +1 -0
  23. package/dist/http-inject.js +55 -0
  24. package/dist/index.d.ts +20 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +19 -0
  27. package/dist/matchers.d.ts +56 -0
  28. package/dist/matchers.d.ts.map +1 -0
  29. package/dist/matchers.js +262 -0
  30. package/dist/memory-conformance.d.ts +13 -0
  31. package/dist/memory-conformance.d.ts.map +1 -0
  32. package/dist/memory-conformance.js +1333 -0
  33. package/dist/memory.d.ts +11 -0
  34. package/dist/memory.d.ts.map +1 -0
  35. package/dist/memory.js +20 -0
  36. package/dist/middleware-harness.d.ts +11 -0
  37. package/dist/middleware-harness.d.ts.map +1 -0
  38. package/dist/middleware-harness.js +35 -0
  39. package/dist/permissions-conformance.d.ts +24 -0
  40. package/dist/permissions-conformance.d.ts.map +1 -0
  41. package/dist/permissions-conformance.js +156 -0
  42. package/dist/record-fixtures.d.ts +21 -0
  43. package/dist/record-fixtures.d.ts.map +1 -0
  44. package/dist/record-fixtures.js +31 -0
  45. package/dist/record.d.ts +12 -0
  46. package/dist/record.d.ts.map +1 -0
  47. package/dist/record.js +22 -0
  48. package/dist/regex-safety.d.ts +2 -0
  49. package/dist/regex-safety.d.ts.map +1 -0
  50. package/dist/regex-safety.js +66 -0
  51. package/dist/run-result.d.ts +111 -0
  52. package/dist/run-result.d.ts.map +1 -0
  53. package/dist/run-result.js +287 -0
  54. package/dist/subprocess.d.ts +25 -0
  55. package/dist/subprocess.d.ts.map +1 -0
  56. package/dist/subprocess.js +223 -0
  57. package/dist/thread-access-harness.d.ts +40 -0
  58. package/dist/thread-access-harness.d.ts.map +1 -0
  59. package/dist/thread-access-harness.js +42 -0
  60. package/dist/threads-conformance.d.ts +18 -0
  61. package/dist/threads-conformance.d.ts.map +1 -0
  62. package/dist/threads-conformance.js +315 -0
  63. package/dist/tool-harness.d.ts +16 -0
  64. package/dist/tool-harness.d.ts.map +1 -0
  65. package/dist/tool-harness.js +25 -0
  66. package/dist/tsconfig.tsbuildinfo +1 -0
  67. package/dist/workspace-harness.d.ts +15 -0
  68. package/dist/workspace-harness.d.ts.map +1 -0
  69. package/dist/workspace-harness.js +43 -0
  70. package/package.json +71 -0
@@ -0,0 +1,1333 @@
1
+ import { approveWithReconcile, BrowseQueryError, } from "@b4run/memory";
2
+ import { expect, test } from "vitest";
3
+ /** Asserted on `.code`, not on the message: the route branches on the code. */
4
+ async function expectContinuationInvalid(run) {
5
+ try {
6
+ await run();
7
+ }
8
+ catch (error) {
9
+ expect(error).toBeInstanceOf(BrowseQueryError);
10
+ expect(error.code).toBe("continuation-invalid");
11
+ return;
12
+ }
13
+ expect.unreachable("should have rejected");
14
+ }
15
+ // A store whose keyset does not ADVANCE hands back the same full window forever.
16
+ // Unbounded, every walk below would spin to the suite timeout with no diagnosis; the
17
+ // bound plus the post-loop null assertion makes that a named failure instead. Third
18
+ // parties run this suite against their own stores, so only they can hit it.
19
+ const MAX_WALK_PAGES = 20;
20
+ function rec(over) {
21
+ return {
22
+ kind: "semantic",
23
+ data: {},
24
+ source: { type: "eval", id: "seed" },
25
+ confidence: 1,
26
+ tags: [],
27
+ status: "active",
28
+ createdAt: "2026-01-01T00:00:00.000Z",
29
+ updatedAt: "2026-01-01T00:00:00.000Z",
30
+ ...over,
31
+ };
32
+ }
33
+ const vec = (...xs) => new Float32Array(xs);
34
+ const D = (day) => `2026-08-${String(day).padStart(2, "0")}T00:00:00.000Z`;
35
+ /** Record fixture with episodic defaults (kind overridable, e.g. for prune's
36
+ * non-episodic immunity cases). */
37
+ function ep(over) {
38
+ return rec({
39
+ namespace: "route=/n",
40
+ content: over.id,
41
+ kind: "episodic",
42
+ source: { type: "run", id: "r" },
43
+ createdAt: D(1),
44
+ updatedAt: D(1),
45
+ ...over,
46
+ });
47
+ }
48
+ /**
49
+ * The contract every MemoryStore must satisfy. Run against sqlite (in-process,
50
+ * always) and pgvector (real Postgres, gated) so backends cannot drift. Pass
51
+ * vitest's `describe`; `makeStore` returns a FRESH empty store per call.
52
+ */
53
+ export function runMemoryStoreConformance(opts) {
54
+ const { name, makeStore, describe, close } = opts;
55
+ describe(`MemoryStore conformance: ${name}`, () => {
56
+ test("put + get round-trips", async () => {
57
+ const s = await makeStore();
58
+ try {
59
+ await s.put(rec({ id: "a", namespace: "ns", content: "hello billing" }));
60
+ expect((await s.get("a"))?.content).toBe("hello billing");
61
+ }
62
+ finally {
63
+ await close?.(s);
64
+ }
65
+ });
66
+ test("search is namespace-isolated", async () => {
67
+ const s = await makeStore();
68
+ try {
69
+ await s.put(rec({ id: "a", namespace: "ns1", content: "billing escalation" }));
70
+ await s.put(rec({ id: "b", namespace: "ns2", content: "billing escalation" }));
71
+ expect((await s.search({ namespace: "ns1", query: "billing" })).map((r) => r.id)).toEqual([
72
+ "a",
73
+ ]);
74
+ }
75
+ finally {
76
+ await close?.(s);
77
+ }
78
+ });
79
+ test("query-less search is pure recency order", async () => {
80
+ const s = await makeStore();
81
+ try {
82
+ await s.put(rec({ id: "old", namespace: "ns", content: "x", updatedAt: "2026-07-01T00:00:00.000Z" }));
83
+ await s.put(rec({ id: "new", namespace: "ns", content: "y", updatedAt: "2026-07-04T00:00:00.000Z" }));
84
+ expect((await s.search({ namespace: "ns" })).map((r) => r.id)).toEqual(["new", "old"]);
85
+ }
86
+ finally {
87
+ await close?.(s);
88
+ }
89
+ });
90
+ test("query-less search equal updated_at rows order by codepoint id (C collation)", async () => {
91
+ const s = await makeStore();
92
+ try {
93
+ // Same updatedAt on every row: id ASC is the sole tiebreak, and both backends
94
+ // must agree byte-for-byte. Mixed case separates codepoint order ("B10" < "a9")
95
+ // from a locale collation, which folds case and puts "a9" first.
96
+ for (const id of ["b2", "B10", "a9"]) {
97
+ await s.put(rec({ id, namespace: "ns", content: id, updatedAt: D(2) }));
98
+ }
99
+ expect((await s.search({ namespace: "ns" })).map((r) => r.id)).toEqual(["B10", "a9", "b2"]);
100
+ }
101
+ finally {
102
+ await close?.(s);
103
+ }
104
+ });
105
+ test("supersede: old→superseded, new active, link recorded", async () => {
106
+ const s = await makeStore();
107
+ try {
108
+ await s.put(rec({ id: "old", namespace: "ns", content: "v1" }));
109
+ await s.put(rec({ id: "new", namespace: "ns", content: "v2" }));
110
+ await s.supersede("old", "new");
111
+ expect((await s.get("old"))?.status).toBe("superseded");
112
+ expect((await s.get("new"))?.supersedes).toContain("old");
113
+ }
114
+ finally {
115
+ await close?.(s);
116
+ }
117
+ });
118
+ test("candidate listing + delete", async () => {
119
+ const s = await makeStore();
120
+ try {
121
+ await s.put(rec({ id: "c", namespace: "ns", content: "cand", status: "candidate" }));
122
+ expect((await s.listCandidates("")).map((r) => r.id)).toContain("c");
123
+ await s.delete("c");
124
+ expect(await s.get("c")).toBeNull();
125
+ }
126
+ finally {
127
+ await close?.(s);
128
+ }
129
+ });
130
+ test("update preserves the stored embedding (vector recall still finds it)", async () => {
131
+ const s = await makeStore();
132
+ try {
133
+ await s.put(rec({ id: "e", namespace: "ns", content: "faster shipping" }), {
134
+ embedding: vec(1, 0, 0),
135
+ embeddingModel: "fake:test",
136
+ });
137
+ await s.update("e", { confidence: 0.5 });
138
+ const out = await s.search({
139
+ namespace: "ns",
140
+ query: "expedite delivery",
141
+ queryEmbedding: vec(1, 0, 0),
142
+ embedderId: "fake:test",
143
+ now: "2026-07-05T00:00:00.000Z",
144
+ });
145
+ expect(out.map((r) => r.id)).toContain("e");
146
+ }
147
+ finally {
148
+ await close?.(s);
149
+ }
150
+ });
151
+ test("hybrid: a 0-shared-token semantic match is recalled via the vector list", async () => {
152
+ const s = await makeStore();
153
+ try {
154
+ await s.put(rec({ id: "sem", namespace: "ns", content: "faster shipping preferred" }), {
155
+ embedding: vec(1, 0, 0),
156
+ embeddingModel: "fake:test",
157
+ });
158
+ await s.put(rec({ id: "kw", namespace: "ns", content: "acme billing" }), {
159
+ embedding: vec(0, 1, 0),
160
+ embeddingModel: "fake:test",
161
+ });
162
+ const out = await s.search({
163
+ namespace: "ns",
164
+ query: "expedite delivery",
165
+ queryEmbedding: vec(0.95, 0.05, 0),
166
+ embedderId: "fake:test",
167
+ now: "2026-07-05T00:00:00.000Z",
168
+ });
169
+ expect(out.map((r) => r.id)).toContain("sem");
170
+ expect(out[0]?.id).toBe("sem");
171
+ }
172
+ finally {
173
+ await close?.(s);
174
+ }
175
+ });
176
+ test("browse lists across namespaces and statuses, newest first, with total", async () => {
177
+ const s = await makeStore();
178
+ try {
179
+ await s.put(rec({
180
+ id: "a",
181
+ namespace: "route=/x",
182
+ content: "a",
183
+ updatedAt: "2026-07-03T00:00:00.000Z",
184
+ }));
185
+ await s.put(rec({
186
+ id: "b",
187
+ namespace: "route=/y",
188
+ content: "b",
189
+ status: "candidate",
190
+ updatedAt: "2026-07-02T00:00:00.000Z",
191
+ }));
192
+ await s.put(rec({
193
+ id: "c",
194
+ namespace: "route=/x",
195
+ content: "c",
196
+ status: "superseded",
197
+ updatedAt: "2026-07-01T00:00:00.000Z",
198
+ }));
199
+ const page = await s.browse();
200
+ expect(page.total).toBe(3);
201
+ expect(page.records.map((r) => r.id)).toEqual(["a", "b", "c"]);
202
+ }
203
+ finally {
204
+ await close?.(s);
205
+ }
206
+ });
207
+ test("browse orders equal-updated_at rows by id ASC", async () => {
208
+ const s = await makeStore();
209
+ try {
210
+ // Same updatedAt on every row: id ASC is the sole tiebreak, and both
211
+ // backends must agree byte-for-byte (BINARY / C collation). Mixed case
212
+ // distinguishes codepoint order ("B10" < "a9") from locale collation.
213
+ for (const id of ["b2", "B10", "a9"]) {
214
+ await s.put(rec({ id, namespace: "ns", content: id }));
215
+ }
216
+ expect((await s.browse()).records.map((r) => r.id)).toEqual(["B10", "a9", "b2"]);
217
+ }
218
+ finally {
219
+ await close?.(s);
220
+ }
221
+ });
222
+ test("browse filters by namespacePrefix, status, kind, and sourceType individually", async () => {
223
+ const s = await makeStore();
224
+ try {
225
+ await s.put(rec({ id: "a", namespace: "route=/x", content: "a" }));
226
+ await s.put(rec({ id: "b", namespace: "route=/y", content: "b", status: "candidate" }));
227
+ await s.put(rec({ id: "h", namespace: "route=/x", content: "h", source: { type: "human", id: "u" } }));
228
+ await s.put(rec({ id: "e", namespace: "other", content: "e", kind: "episodic" }));
229
+ expect((await s.browse({ namespacePrefix: "route=/x" })).records.map((r) => r.id).sort()).toEqual(["a", "h"]);
230
+ expect((await s.browse({ status: "candidate" })).records.map((r) => r.id)).toEqual(["b"]);
231
+ expect((await s.browse({ kind: "episodic" })).records.map((r) => r.id)).toEqual(["e"]);
232
+ expect((await s.browse({ sourceType: "human" })).records.map((r) => r.id)).toEqual(["h"]);
233
+ }
234
+ finally {
235
+ await close?.(s);
236
+ }
237
+ });
238
+ test("browse accepts a set of statuses or kinds, matching any of them", async () => {
239
+ const s = await makeStore();
240
+ try {
241
+ await s.put(rec({ id: "act", namespace: "route=/x", content: "act" }));
242
+ await s.put(rec({ id: "cand", namespace: "route=/x", content: "cand", status: "candidate" }));
243
+ await s.put(rec({ id: "sup", namespace: "route=/x", content: "sup", status: "superseded" }));
244
+ await s.put(rec({ id: "ep", namespace: "route=/x", content: "ep", kind: "episodic" }));
245
+ await s.put(rec({ id: "proc", namespace: "route=/x", content: "proc", kind: "procedural" }));
246
+ expect((await s.browse({ status: ["candidate", "superseded"] })).records.map((r) => r.id).sort()).toEqual(["cand", "sup"]);
247
+ expect((await s.browse({ kind: ["episodic", "procedural"] })).records.map((r) => r.id).sort()).toEqual(["ep", "proc"]);
248
+ }
249
+ finally {
250
+ await close?.(s);
251
+ }
252
+ });
253
+ test("browse treats a one-element set exactly like the bare value", async () => {
254
+ const s = await makeStore();
255
+ try {
256
+ await s.put(rec({ id: "a", namespace: "route=/x", content: "a" }));
257
+ await s.put(rec({ id: "b", namespace: "route=/x", content: "b", status: "candidate" }));
258
+ const bare = await s.browse({ status: "candidate" });
259
+ const set = await s.browse({ status: ["candidate"] });
260
+ expect(set.records.map((r) => r.id)).toEqual(bare.records.map((r) => r.id));
261
+ expect(set.total).toBe(bare.total);
262
+ }
263
+ finally {
264
+ await close?.(s);
265
+ }
266
+ });
267
+ test("browse matches nothing for an empty set — not everything", async () => {
268
+ const s = await makeStore();
269
+ try {
270
+ // "none of these" is an OR over zero options, which is false. Reading it
271
+ // as "no filter" would silently show every row to a UI that had just
272
+ // unticked its last box.
273
+ await s.put(rec({ id: "a", namespace: "route=/x", content: "a" }));
274
+ await s.put(rec({ id: "b", namespace: "route=/x", content: "b", status: "candidate" }));
275
+ const page = await s.browse({ status: [] });
276
+ expect(page.records).toEqual([]);
277
+ expect(page.total).toBe(0);
278
+ }
279
+ finally {
280
+ await close?.(s);
281
+ }
282
+ });
283
+ test("browse counts a set with the same clause it selects with", async () => {
284
+ const s = await makeStore();
285
+ try {
286
+ for (let i = 0; i < 5; i += 1) {
287
+ await s.put(rec({ id: `c${i}`, namespace: "route=/x", content: `c${i}`, status: "candidate" }));
288
+ }
289
+ await s.put(rec({ id: "keep", namespace: "route=/x", content: "keep" }));
290
+ const page = await s.browse({ status: ["candidate", "superseded"], limit: 2 });
291
+ expect(page.records).toHaveLength(2);
292
+ // total reflects the whole matching set, not the page.
293
+ expect(page.total).toBe(5);
294
+ }
295
+ finally {
296
+ await close?.(s);
297
+ }
298
+ });
299
+ test("browse ANDs a status set with the other filters", async () => {
300
+ const s = await makeStore();
301
+ try {
302
+ await s.put(rec({ id: "hit", namespace: "route=/x", content: "hit", status: "candidate" }));
303
+ await s.put(rec({ id: "wrongNs", namespace: "route=/y", content: "n", status: "candidate" }));
304
+ await s.put(rec({ id: "wrongStatus", namespace: "route=/x", content: "s" }));
305
+ const page = await s.browse({
306
+ namespacePrefix: "route=/x",
307
+ status: ["candidate", "superseded"],
308
+ });
309
+ expect(page.records.map((r) => r.id)).toEqual(["hit"]);
310
+ expect(page.total).toBe(1);
311
+ }
312
+ finally {
313
+ await close?.(s);
314
+ }
315
+ });
316
+ test("browse combines multiple filters with AND (COUNT shares the clause)", async () => {
317
+ const s = await makeStore();
318
+ try {
319
+ // Strict-subset construction: each of b/h/y passes two of the three
320
+ // clauses but fails the third, so only "a" survives — and total:1
321
+ // proves the COUNT query applies the same WHERE as the row query.
322
+ await s.put(rec({ id: "a", namespace: "route=/x", content: "a" }));
323
+ await s.put(rec({ id: "b", namespace: "route=/x", content: "b", status: "candidate" }));
324
+ await s.put(rec({ id: "h", namespace: "route=/x", content: "h", source: { type: "human", id: "u" } }));
325
+ await s.put(rec({ id: "y", namespace: "route=/y", content: "y" }));
326
+ const page = await s.browse({
327
+ namespacePrefix: "route=/x",
328
+ status: "active",
329
+ sourceType: "eval",
330
+ });
331
+ expect(page.records.map((r) => r.id)).toEqual(["a"]);
332
+ expect(page.total).toBe(1);
333
+ }
334
+ finally {
335
+ await close?.(s);
336
+ }
337
+ });
338
+ test("browse pages with limit/offset while total stays full", async () => {
339
+ const s = await makeStore();
340
+ try {
341
+ for (let i = 0; i < 5; i++) {
342
+ await s.put(rec({
343
+ id: `r${i}`,
344
+ namespace: "ns",
345
+ content: `r${i}`,
346
+ updatedAt: `2026-07-0${i + 1}T00:00:00.000Z`,
347
+ }));
348
+ }
349
+ const page = await s.browse({ limit: 2, offset: 2 });
350
+ expect(page.total).toBe(5);
351
+ expect(page.records.map((r) => r.id)).toEqual(["r2", "r1"]);
352
+ }
353
+ finally {
354
+ await close?.(s);
355
+ }
356
+ });
357
+ test("browse returns an empty page on an empty store", async () => {
358
+ const s = await makeStore();
359
+ try {
360
+ expect(await s.browse()).toEqual({ records: [], total: 0, continuation: null });
361
+ }
362
+ finally {
363
+ await close?.(s);
364
+ }
365
+ });
366
+ test("browse returns empty records but full total when offset exceeds total", async () => {
367
+ const s = await makeStore();
368
+ try {
369
+ await s.put(rec({ id: "a", namespace: "ns", content: "a" }));
370
+ const page = await s.browse({ limit: 10, offset: 99 });
371
+ expect(page.records).toEqual([]);
372
+ expect(page.total).toBe(1);
373
+ }
374
+ finally {
375
+ await close?.(s);
376
+ }
377
+ });
378
+ test("browse/stats namespacePrefix treats LIKE metacharacters literally", async () => {
379
+ const s = await makeStore();
380
+ try {
381
+ // Underscore: "route=/foo_" must NOT also match "route=/fooXbar".
382
+ await s.put(rec({ id: "u", namespace: "route=/foo_bar", content: "u" }));
383
+ await s.put(rec({ id: "x", namespace: "route=/fooXbar", content: "x" }));
384
+ // Percent + trailing backslash: "50%_off\\" must match only the
385
+ // literal namespace, never wildcard-expand.
386
+ await s.put(rec({ id: "p", namespace: "50%_off\\x", content: "p" }));
387
+ await s.put(rec({ id: "q", namespace: "50Y_offx", content: "q" }));
388
+ const page = await s.browse({ namespacePrefix: "route=/foo_" });
389
+ expect(page.records.map((r) => r.id)).toEqual(["u"]);
390
+ expect(page.total).toBe(1);
391
+ expect((await s.stats({ namespacePrefix: "route=/foo_" })).total).toBe(1);
392
+ const pct = await s.browse({ namespacePrefix: "50%" });
393
+ expect(pct.records.map((r) => r.id)).toEqual(["p"]);
394
+ expect(pct.total).toBe(1);
395
+ const bs = await s.browse({ namespacePrefix: "50%_off\\" });
396
+ expect(bs.records.map((r) => r.id)).toEqual(["p"]);
397
+ expect(bs.total).toBe(1);
398
+ expect((await s.stats({ namespacePrefix: "50%_off\\" })).total).toBe(1);
399
+ }
400
+ finally {
401
+ await close?.(s);
402
+ }
403
+ });
404
+ test("browse namespacePrefix is case-sensitive", async () => {
405
+ const s = await makeStore();
406
+ try {
407
+ await s.put(rec({ id: "a", namespace: "route=/x", content: "a" }));
408
+ expect(await s.browse({ namespacePrefix: "Route=/X" })).toEqual({
409
+ records: [],
410
+ total: 0,
411
+ continuation: null,
412
+ });
413
+ expect((await s.stats({ namespacePrefix: "Route=/X" })).total).toBe(0);
414
+ }
415
+ finally {
416
+ await close?.(s);
417
+ }
418
+ });
419
+ test("browse rejects an invalid query instead of silently matching zero rows", async () => {
420
+ const s = await makeStore();
421
+ try {
422
+ // A store that quietly returns [] for a malformed filter teaches the caller
423
+ // that its query was fine and the data was empty. Both are lies.
424
+ // Identity, not wording: the HTTP boundary maps a rejection to 400 by `name`, so
425
+ // a store that caught and rethrew a plain Error satisfies every regex below and
426
+ // still 500s. Asserted by name rather than `instanceof`, which is false across the
427
+ // two module copies a bundled route and a node_modules store resolve to.
428
+ await expect(s.browse({ status: "bogus" })).rejects.toMatchObject({
429
+ name: "BrowseQueryError",
430
+ code: "invalid-query",
431
+ });
432
+ await expect(s.browse({ status: "bogus" })).rejects.toThrow(/invalid status/);
433
+ await expect(s.browse({ filters: [{ field: "tags", op: "in", values: ["x"] }] })).rejects.toThrow(/unknown filter field/);
434
+ await expect(s.browse({ filters: [{ field: "status", op: "in", values: [] }] })).rejects.toThrow(/must not be empty/);
435
+ await expect(s.browse({ orderBy: [{ field: "content", dir: "asc" }] })).rejects.toThrow(/unknown sort field/);
436
+ await expect(s.browse({ limit: 0 })).rejects.toThrow(/limit must be an integer >= 1/);
437
+ await expect(s.browse({ since: "2026-08-09" })).rejects.toThrow(/full ISO-8601/);
438
+ }
439
+ finally {
440
+ await close?.(s);
441
+ }
442
+ });
443
+ test("browse does NOT impose the HTTP limit ceiling on in-process callers", async () => {
444
+ const s = await makeStore();
445
+ try {
446
+ // The CLI's consolidation scan browses with limit 10_000, so no store may pass
447
+ // `maxLimit` into the validator. Only that rejection is visible here: with one row
448
+ // seeded, a store that also clamped 10_000 down to the ceiling reads identically.
449
+ await s.put(rec({ id: "a", namespace: "ns", content: "a" }));
450
+ const page = await s.browse({ limit: 10_000 });
451
+ expect(page.total).toBe(1);
452
+ expect(page.records.map((r) => r.id)).toEqual(["a"]);
453
+ }
454
+ finally {
455
+ await close?.(s);
456
+ }
457
+ });
458
+ test("browse filters[] narrows by status/kind set, ANDed with everything else", async () => {
459
+ const s = await makeStore();
460
+ try {
461
+ await s.put(rec({ id: "a", namespace: "route=/x", content: "a" }));
462
+ await s.put(rec({ id: "b", namespace: "route=/x", content: "b", status: "candidate" }));
463
+ await s.put(rec({ id: "e", namespace: "route=/y", content: "e", kind: "episodic" }));
464
+ const inSet = await s.browse({
465
+ filters: [{ field: "status", op: "in", values: ["candidate", "superseded"] }],
466
+ });
467
+ expect(inSet.records.map((r) => r.id)).toEqual(["b"]);
468
+ expect(inSet.total).toBe(1);
469
+ const notIn = await s.browse({
470
+ filters: [{ field: "kind", op: "notIn", values: ["episodic"] }],
471
+ });
472
+ expect(notIn.records.map((r) => r.id).sort()).toEqual(["a", "b"]);
473
+ expect(notIn.total).toBe(2);
474
+ const anded = await s.browse({
475
+ namespacePrefix: "route=/x",
476
+ filters: [{ field: "status", op: "in", values: ["candidate"] }],
477
+ });
478
+ expect(anded.records.map((r) => r.id)).toEqual(["b"]);
479
+ expect(anded.total).toBe(1);
480
+ }
481
+ finally {
482
+ await close?.(s);
483
+ }
484
+ });
485
+ test("browse returns identical ordered ids for a composed filter on every backend", async () => {
486
+ const s = await makeStore();
487
+ try {
488
+ for (let index = 0; index < 40; index += 1) {
489
+ await s.put(rec({
490
+ id: `cmp-${String(index).padStart(3, "0")}`,
491
+ kind: index % 2 === 0 ? "semantic" : "episodic",
492
+ namespace: index % 5 === 0 ? "route=/a-archive" : "route=/a",
493
+ content: index % 3 === 0 ? "needle body" : "filler body",
494
+ confidence: (index % 4) / 4,
495
+ status: index % 3 === 0 ? "active" : "candidate",
496
+ // Ten records per stamp so the id tie-break decides order INSIDE the
497
+ // window, not only at a page seam this single window never reaches.
498
+ updatedAt: new Date(Date.UTC(2026, 0, 1) + Math.floor(index / 10) * 60_000).toISOString(),
499
+ }));
500
+ }
501
+ const page = await s.browse({
502
+ namespace: "route=/a",
503
+ limit: 10,
504
+ filters: [
505
+ { field: "status", op: "in", values: ["active"] },
506
+ { field: "kind", op: "in", values: ["semantic"] },
507
+ { field: "content", op: "contains", value: "needle" },
508
+ ],
509
+ orderBy: [{ field: "updatedAt", dir: "desc" }],
510
+ });
511
+ // Byte-exact expectation, computed the way both stores must compute it: no
512
+ // archive rows, active ∧ semantic ∧ needle, updatedAt DESC then id ASC.
513
+ //
514
+ // The survivors are the indices divisible by 6 — `% 2` picks semantic and
515
+ // `% 3` picks active-and-needle together — minus those divisible by 5, which
516
+ // the exact `namespace` puts in the archive: 6, 12, 18, 24, 36. Two of them
517
+ // (12 and 18) share a minute stamp, so their relative order is decided by the
518
+ // id terminator rather than by the sort key — which is the half of this a
519
+ // backend can silently get wrong while looking right.
520
+ expect(page.records.map((record) => record.id)).toEqual([
521
+ "cmp-036",
522
+ "cmp-024",
523
+ "cmp-012",
524
+ "cmp-018",
525
+ "cmp-006",
526
+ ]);
527
+ expect(page.total).toBe(5);
528
+ expect(page.continuation).toBeNull();
529
+ }
530
+ finally {
531
+ await close?.(s);
532
+ }
533
+ });
534
+ test("browse content filters are case-insensitive substring matches, not LIKE patterns", async () => {
535
+ const s = await makeStore();
536
+ try {
537
+ await s.put(rec({ id: "a", namespace: "ns", content: "Acme threshold is 500" }));
538
+ await s.put(rec({ id: "b", namespace: "ns", content: "zed color is blue" }));
539
+ await s.put(rec({ id: "pct", namespace: "ns", content: "50% off today" }));
540
+ await s.put(rec({ id: "und", namespace: "ns", content: "50Xoff today" }));
541
+ const contains = await s.browse({
542
+ filters: [{ field: "content", op: "contains", value: "ACME" }],
543
+ });
544
+ expect(contains.records.map((r) => r.id)).toEqual(["a"]);
545
+ expect(contains.total).toBe(1);
546
+ expect((await s.browse({ filters: [{ field: "content", op: "notContains", value: "acme" }] })).records
547
+ .map((r) => r.id)
548
+ .sort()).toEqual(["b", "pct", "und"]);
549
+ expect((await s.browse({ filters: [{ field: "content", op: "startsWith", value: "acme " }] })).records.map((r) => r.id)).toEqual(["a"]);
550
+ expect((await s.browse({ filters: [{ field: "content", op: "endsWith", value: "IS BLUE" }] })).records.map((r) => r.id)).toEqual(["b"]);
551
+ expect((await s.browse({
552
+ filters: [{ field: "content", op: "equals", value: "zed color is blue" }],
553
+ })).records.map((r) => r.id)).toEqual(["b"]);
554
+ expect((await s.browse({
555
+ filters: [{ field: "content", op: "notEquals", value: "zed color is blue" }],
556
+ })).total).toBe(3);
557
+ // "%" and "_" are literal characters, not wildcards: this is why the stores
558
+ // use instr/position instead of LIKE. Both needles SEPARATE the two readings —
559
+ // under LIKE, "%" is "anything" and "_" is "any one character", so both would
560
+ // additionally admit "500" and "50Xoff". A needle either reading accepts (say
561
+ // "50% o") asserts nothing here.
562
+ expect((await s.browse({ filters: [{ field: "content", op: "contains", value: "50%" }] })).records.map((r) => r.id)).toEqual(["pct"]);
563
+ expect((await s.browse({ filters: [{ field: "content", op: "contains", value: "50_" }] })).records.map((r) => r.id)).toEqual([]);
564
+ }
565
+ finally {
566
+ await close?.(s);
567
+ }
568
+ });
569
+ test("browse namespace is EXACT while namespacePrefix stays a prefix", async () => {
570
+ const s = await makeStore();
571
+ try {
572
+ await s.put(rec({ id: "exact", namespace: "route=/a", content: "exact" }));
573
+ await s.put(rec({ id: "child", namespace: "route=/ab", content: "child" }));
574
+ const byPrefix = await s.browse({ namespacePrefix: "route=/a" });
575
+ expect(byPrefix.records.map((r) => r.id).sort()).toEqual(["child", "exact"]);
576
+ expect(byPrefix.total).toBe(2);
577
+ // The exact field is what kills the Inspector's client-side narrowing, where
578
+ // the server counted the prefix and the client displayed the equality.
579
+ const byExact = await s.browse({ namespace: "route=/a" });
580
+ expect(byExact.records.map((r) => r.id)).toEqual(["exact"]);
581
+ expect(byExact.total).toBe(1);
582
+ const byFilter = await s.browse({
583
+ filters: [{ field: "namespace", op: "equals", value: "route=/ab" }],
584
+ });
585
+ expect(byFilter.records.map((r) => r.id)).toEqual(["child"]);
586
+ expect(byFilter.total).toBe(1);
587
+ }
588
+ finally {
589
+ await close?.(s);
590
+ }
591
+ });
592
+ test("browse namespace startsWith keeps byte-exact, case-sensitive, metachar-literal semantics", async () => {
593
+ const s = await makeStore();
594
+ try {
595
+ await s.put(rec({ id: "u", namespace: "route=/foo_bar", content: "u" }));
596
+ await s.put(rec({ id: "x", namespace: "route=/fooXbar", content: "x" }));
597
+ await s.put(rec({ id: "unicode", namespace: "route=/日本語", content: "unicode" }));
598
+ const underscore = await s.browse({
599
+ filters: [{ field: "namespace", op: "startsWith", value: "route=/foo_" }],
600
+ });
601
+ expect(underscore.records.map((r) => r.id)).toEqual(["u"]);
602
+ expect(underscore.total).toBe(1);
603
+ expect((await s.browse({
604
+ filters: [{ field: "namespace", op: "startsWith", value: "ROUTE=/foo" }],
605
+ })).total).toBe(0);
606
+ // Multi-byte prefixes must not fall outside the computed range.
607
+ const unicode = await s.browse({
608
+ filters: [{ field: "namespace", op: "startsWith", value: "route=/日" }],
609
+ });
610
+ expect(unicode.records.map((r) => r.id)).toEqual(["unicode"]);
611
+ expect(unicode.total).toBe(1);
612
+ }
613
+ finally {
614
+ await close?.(s);
615
+ }
616
+ });
617
+ test("browse ANDs namespace with namespacePrefix instead of letting one win", async () => {
618
+ const s = await makeStore();
619
+ try {
620
+ await s.put(rec({ id: "exact", namespace: "route=/a", content: "exact" }));
621
+ await s.put(rec({ id: "child", namespace: "route=/ab", content: "child" }));
622
+ // The only query that binds both clauses at once, so it is the only one where
623
+ // a parameter pushed out of step with its clause shows up as a wrong answer
624
+ // rather than a bind error.
625
+ const both = await s.browse({ namespace: "route=/ab", namespacePrefix: "route=/a" });
626
+ expect(both.records.map((r) => r.id)).toEqual(["child"]);
627
+ expect(both.total).toBe(1);
628
+ const disjoint = await s.browse({ namespace: "route=/a", namespacePrefix: "route=/ab" });
629
+ expect(disjoint.records).toEqual([]);
630
+ expect(disjoint.total).toBe(0);
631
+ }
632
+ finally {
633
+ await close?.(s);
634
+ }
635
+ });
636
+ test("browse namespacePrefix above the last code point keeps only a lower bound", async () => {
637
+ const s = await makeStore();
638
+ try {
639
+ await s.put(rec({ id: "below", namespace: "route=/a", content: "below" }));
640
+ await s.put(rec({ id: "top", namespace: "\u{10FFFF}top", content: "top" }));
641
+ // Nothing sorts above U+10FFFF, so the prefix has no successor and the upper
642
+ // bound must be OMITTED — binding an absent one matches nothing at all.
643
+ const top = await s.browse({ namespacePrefix: "\u{10FFFF}" });
644
+ expect(top.records.map((r) => r.id)).toEqual(["top"]);
645
+ expect(top.total).toBe(1);
646
+ const viaFilter = await s.browse({
647
+ filters: [{ field: "namespace", op: "startsWith", value: "\u{10FFFF}" }],
648
+ });
649
+ expect(viaFilter.records.map((r) => r.id)).toEqual(["top"]);
650
+ expect(viaFilter.total).toBe(1);
651
+ }
652
+ finally {
653
+ await close?.(s);
654
+ }
655
+ });
656
+ test("browse filters by confidence, with between inclusive on both ends", async () => {
657
+ const s = await makeStore();
658
+ try {
659
+ // 0.9 is chosen deliberately: it is not representable in float4, so `eq`
660
+ // is asserted against the value READ BACK, which is the only one a backend
661
+ // that narrows on write can still match.
662
+ await s.put(rec({ id: "low", namespace: "ns", content: "low", confidence: 0.2 }));
663
+ await s.put(rec({ id: "mid", namespace: "ns", content: "mid", confidence: 0.5 }));
664
+ await s.put(rec({ id: "high", namespace: "ns", content: "high", confidence: 0.9 }));
665
+ const stored = (await s.get("high"))?.confidence;
666
+ expect((await s.browse({ filters: [{ field: "confidence", op: "eq", value: stored }] })).records.map((r) => r.id)).toEqual(["high"]);
667
+ expect((await s.browse({ filters: [{ field: "confidence", op: "gt", value: 0.5 }] })).records.map((r) => r.id)).toEqual(["high"]);
668
+ // Ids, not counts: at this spread `gte 0.5` and `lte 0.5` both match two rows,
669
+ // so a count-only assertion passes with the two operators transposed.
670
+ const gte = await s.browse({ filters: [{ field: "confidence", op: "gte", value: 0.5 }] });
671
+ expect(gte.records.map((r) => r.id).sort()).toEqual(["high", "mid"]);
672
+ expect(gte.total).toBe(2);
673
+ expect((await s.browse({ filters: [{ field: "confidence", op: "lt", value: 0.5 }] })).records.map((r) => r.id)).toEqual(["low"]);
674
+ const lte = await s.browse({ filters: [{ field: "confidence", op: "lte", value: 0.5 }] });
675
+ expect(lte.records.map((r) => r.id).sort()).toEqual(["low", "mid"]);
676
+ expect(lte.total).toBe(2);
677
+ const neq = await s.browse({ filters: [{ field: "confidence", op: "neq", value: 0.5 }] });
678
+ expect(neq.records.map((r) => r.id).sort()).toEqual(["high", "low"]);
679
+ expect(neq.total).toBe(2);
680
+ const between = await s.browse({
681
+ filters: [{ field: "confidence", op: "between", min: 0.2, max: 0.5 }],
682
+ });
683
+ expect(between.records.map((r) => r.id).sort()).toEqual(["low", "mid"]);
684
+ expect(between.total).toBe(2);
685
+ }
686
+ finally {
687
+ await close?.(s);
688
+ }
689
+ });
690
+ test("browse filters updatedAt by UTC day buckets", async () => {
691
+ const s = await makeStore();
692
+ try {
693
+ await s.put(rec({ id: "d1", namespace: "ns", content: "d1", updatedAt: "2026-08-01T23:59:59.999Z" }));
694
+ await s.put(rec({ id: "d2", namespace: "ns", content: "d2", updatedAt: "2026-08-02T00:00:00.000Z" }));
695
+ await s.put(rec({ id: "d3", namespace: "ns", content: "d3", updatedAt: "2026-08-03T12:00:00.000Z" }));
696
+ const onDay = await s.browse({
697
+ filters: [{ field: "updatedAt", op: "onDay", day: "2026-08-02" }],
698
+ });
699
+ expect(onDay.records.map((r) => r.id)).toEqual(["d2"]);
700
+ expect(onDay.total).toBe(1);
701
+ expect((await s.browse({
702
+ filters: [{ field: "updatedAt", op: "beforeDay", day: "2026-08-02" }],
703
+ })).records.map((r) => r.id)).toEqual(["d1"]);
704
+ expect((await s.browse({ filters: [{ field: "updatedAt", op: "afterDay", day: "2026-08-02" }] })).records.map((r) => r.id)).toEqual(["d3"]);
705
+ const span = await s.browse({
706
+ filters: [
707
+ {
708
+ field: "updatedAt",
709
+ op: "betweenDays",
710
+ fromDay: "2026-08-01",
711
+ untilDay: "2026-08-02",
712
+ },
713
+ ],
714
+ });
715
+ expect(span.records.map((r) => r.id).sort()).toEqual(["d1", "d2"]);
716
+ expect(span.total).toBe(2);
717
+ }
718
+ finally {
719
+ await close?.(s);
720
+ }
721
+ });
722
+ test("stats returns count maps by status/kind/namespace/sourceType plus total", async () => {
723
+ const s = await makeStore();
724
+ try {
725
+ await s.put(rec({ id: "a", namespace: "route=/x", content: "a" }));
726
+ await s.put(rec({ id: "b", namespace: "route=/y", content: "b", status: "candidate" }));
727
+ await s.put(rec({ id: "h", namespace: "route=/x", content: "h", source: { type: "human", id: "u" } }));
728
+ const st = await s.stats();
729
+ expect(st.total).toBe(3);
730
+ expect(st.byStatus).toEqual({ active: 2, candidate: 1 });
731
+ expect(st.byKind).toEqual({ semantic: 3 });
732
+ expect(st.byNamespace).toEqual({ "route=/x": 2, "route=/y": 1 });
733
+ expect(st.bySourceType).toEqual({ eval: 2, human: 1 });
734
+ }
735
+ finally {
736
+ await close?.(s);
737
+ }
738
+ });
739
+ test("stats honors namespacePrefix", async () => {
740
+ const s = await makeStore();
741
+ try {
742
+ await s.put(rec({ id: "a", namespace: "route=/x", content: "a" }));
743
+ await s.put(rec({ id: "b", namespace: "route=/y", content: "b" }));
744
+ expect((await s.stats({ namespacePrefix: "route=/x" })).total).toBe(1);
745
+ }
746
+ finally {
747
+ await close?.(s);
748
+ }
749
+ });
750
+ test("stats returns zero total and empty maps on an empty store", async () => {
751
+ const s = await makeStore();
752
+ try {
753
+ expect(await s.stats()).toEqual({
754
+ total: 0,
755
+ byStatus: {},
756
+ byKind: {},
757
+ byNamespace: {},
758
+ bySourceType: {},
759
+ });
760
+ }
761
+ finally {
762
+ await close?.(s);
763
+ }
764
+ });
765
+ test("hybrid: mismatched embedder tag is excluded from the vector list", async () => {
766
+ const s = await makeStore();
767
+ try {
768
+ await s.put(rec({ id: "stale", namespace: "ns", content: "faster shipping" }), {
769
+ embedding: vec(1, 0, 0),
770
+ embeddingModel: "old:model",
771
+ });
772
+ const out = await s.search({
773
+ namespace: "ns",
774
+ query: "expedite delivery",
775
+ queryEmbedding: vec(1, 0, 0),
776
+ embedderId: "fake:test",
777
+ now: "2026-07-05T00:00:00.000Z",
778
+ });
779
+ expect(out.map((r) => r.id)).not.toContain("stale");
780
+ }
781
+ finally {
782
+ await close?.(s);
783
+ }
784
+ });
785
+ test("search window: since is inclusive, until is exclusive on effectiveAt", async () => {
786
+ const s = await makeStore();
787
+ try {
788
+ await s.put(ep({ id: "d1", effectiveAt: D(1) }));
789
+ await s.put(ep({ id: "d2", effectiveAt: D(2) }));
790
+ await s.put(ep({ id: "d3", effectiveAt: D(3) }));
791
+ const out = await s.search({ namespace: "route=/n", since: D(2), until: D(3) });
792
+ expect(out.map((r) => r.id)).toEqual(["d2"]);
793
+ }
794
+ finally {
795
+ await close?.(s);
796
+ }
797
+ });
798
+ test("search window falls back to createdAt for legacy rows without effectiveAt", async () => {
799
+ const s = await makeStore();
800
+ try {
801
+ await s.put(ep({ id: "legacy", createdAt: D(2), updatedAt: D(2) })); // no effectiveAt
802
+ await s.put(ep({ id: "outside", createdAt: D(5), updatedAt: D(5) }));
803
+ const out = await s.search({ namespace: "route=/n", since: D(1), until: D(3) });
804
+ expect(out.map((r) => r.id)).toEqual(["legacy"]);
805
+ }
806
+ finally {
807
+ await close?.(s);
808
+ }
809
+ });
810
+ test("windowed query-less search orders by event time, not update time", async () => {
811
+ const s = await makeStore();
812
+ try {
813
+ // updatedAt order is DELIBERATELY the reverse of effectiveAt order —
814
+ // proves windowed ordering uses event time, not update time.
815
+ await s.put(ep({ id: "b", effectiveAt: D(3), updatedAt: D(1) }));
816
+ await s.put(ep({ id: "a", effectiveAt: D(1), updatedAt: D(3) }));
817
+ await s.put(ep({ id: "c", effectiveAt: D(3), updatedAt: D(2) }));
818
+ const out = await s.search({ namespace: "route=/n", since: D(1) });
819
+ expect(out.map((r) => r.id)).toEqual(["b", "c", "a"]); // D3 pair id-ASC, then D1
820
+ }
821
+ finally {
822
+ await close?.(s);
823
+ }
824
+ });
825
+ test("windowed query-less equal event times order by codepoint id (C collation)", async () => {
826
+ const s = await makeStore();
827
+ try {
828
+ // Same effectiveAt on every row: id ASC is the sole tiebreak, and both
829
+ // backends must agree byte-for-byte (BINARY / C collation). Mixed case
830
+ // distinguishes codepoint order ("B10" < "a9") from locale collation.
831
+ for (const id of ["b2", "B10", "a9"]) {
832
+ await s.put(ep({ id, effectiveAt: D(2) }));
833
+ }
834
+ const out = await s.search({ namespace: "route=/n", since: D(1) });
835
+ expect(out.map((r) => r.id)).toEqual(["B10", "a9", "b2"]);
836
+ }
837
+ finally {
838
+ await close?.(s);
839
+ }
840
+ });
841
+ test("search with now excludes expired rows; without now shows everything", async () => {
842
+ const s = await makeStore();
843
+ try {
844
+ await s.put(ep({ id: "live", expiresAt: D(9) }));
845
+ await s.put(ep({ id: "dead", expiresAt: D(2) }));
846
+ const withNow = await s.search({ namespace: "route=/n", now: D(5) });
847
+ expect(withNow.map((r) => r.id)).toEqual(["live"]);
848
+ const withoutNow = await s.search({ namespace: "route=/n" });
849
+ expect(withoutNow.map((r) => r.id).sort()).toEqual(["dead", "live"]);
850
+ }
851
+ finally {
852
+ await close?.(s);
853
+ }
854
+ });
855
+ test("expiry boundary: expiresAt exactly equal to now is excluded", async () => {
856
+ const s = await makeStore();
857
+ try {
858
+ await s.put(ep({ id: "edge", expiresAt: D(5) }));
859
+ expect((await s.search({ namespace: "route=/n", now: D(5) })).length).toBe(0);
860
+ }
861
+ finally {
862
+ await close?.(s);
863
+ }
864
+ });
865
+ test("ranked (query) search shares the window + expiry clause", async () => {
866
+ const s = await makeStore();
867
+ try {
868
+ await s.put(ep({
869
+ id: "hit",
870
+ content: "deploy failed on staging",
871
+ effectiveAt: D(2),
872
+ expiresAt: D(9),
873
+ }));
874
+ await s.put(ep({
875
+ id: "expired",
876
+ content: "deploy failed on prod",
877
+ effectiveAt: D(2),
878
+ expiresAt: D(3),
879
+ }));
880
+ await s.put(ep({ id: "outside", content: "deploy failed early", effectiveAt: D(1) }));
881
+ const out = await s.search({
882
+ namespace: "route=/n",
883
+ query: "deploy failed",
884
+ since: D(2),
885
+ now: D(5),
886
+ });
887
+ expect(out.map((r) => r.id)).toEqual(["hit"]);
888
+ }
889
+ finally {
890
+ await close?.(s);
891
+ }
892
+ });
893
+ test("browse honors since/until + now, with total sharing the clause", async () => {
894
+ const s = await makeStore();
895
+ try {
896
+ await s.put(ep({ id: "live", effectiveAt: D(2), expiresAt: D(9) }));
897
+ await s.put(ep({ id: "dead", effectiveAt: D(2), expiresAt: D(3) }));
898
+ await s.put(ep({ id: "early", effectiveAt: D(1), expiresAt: D(9) }));
899
+ const page = await s.browse({ since: D(2), until: D(4), now: D(5) });
900
+ expect(page.records.map((r) => r.id)).toEqual(["live"]);
901
+ expect(page.total).toBe(1); // COUNT shares the full WHERE incl. expiry+window
902
+ }
903
+ finally {
904
+ await close?.(s);
905
+ }
906
+ });
907
+ test("prune deletes expired rows of any kind and reports the count", async () => {
908
+ const s = await makeStore();
909
+ try {
910
+ await s.put(ep({ id: "sem-dead", kind: "semantic", expiresAt: D(2) }));
911
+ await s.put(ep({ id: "ep-dead", expiresAt: D(2) }));
912
+ await s.put(ep({ id: "ep-live", expiresAt: D(9) }));
913
+ const res = await s.prune({ now: D(5) });
914
+ expect(res).toEqual({ deletedExpired: 2, deletedOverCap: 0 });
915
+ expect(await s.get("sem-dead")).toBeNull();
916
+ expect((await s.get("ep-live"))?.id).toBe("ep-live");
917
+ }
918
+ finally {
919
+ await close?.(s);
920
+ }
921
+ });
922
+ test("prune caps episodic rows per namespace, keeping the newest by event time", async () => {
923
+ const s = await makeStore();
924
+ try {
925
+ for (let i = 1; i <= 5; i++)
926
+ await s.put(ep({ id: `e${i}`, effectiveAt: D(i) }));
927
+ const res = await s.prune({ now: D(9), cap: 3 });
928
+ expect(res.deletedOverCap).toBe(2);
929
+ expect((await s.browse({ kind: "episodic" })).records.map((r) => r.id).sort()).toEqual([
930
+ "e3",
931
+ "e4",
932
+ "e5",
933
+ ]);
934
+ }
935
+ finally {
936
+ await close?.(s);
937
+ }
938
+ });
939
+ test("prune cap is PER namespace and never touches non-episodic rows", async () => {
940
+ const s = await makeStore();
941
+ try {
942
+ await s.put(ep({ id: "sem", kind: "semantic" }));
943
+ for (let i = 1; i <= 3; i++) {
944
+ await s.put(ep({ id: `a${i}`, namespace: "route=/a", effectiveAt: D(i) }));
945
+ }
946
+ for (let i = 1; i <= 3; i++) {
947
+ await s.put(ep({ id: `b${i}`, namespace: "route=/b", effectiveAt: D(i) }));
948
+ }
949
+ const res = await s.prune({ now: D(9), cap: 2 });
950
+ expect(res.deletedOverCap).toBe(2); // one from each namespace
951
+ expect(await s.get("a1")).toBeNull();
952
+ expect(await s.get("b1")).toBeNull();
953
+ expect((await s.get("sem"))?.id).toBe("sem");
954
+ }
955
+ finally {
956
+ await close?.(s);
957
+ }
958
+ });
959
+ test("prune cap ranks episodic rows regardless of status (superseded counts)", async () => {
960
+ const s = await makeStore();
961
+ try {
962
+ // A superseded episodic row sits between two actives by event time. The
963
+ // cap pass ranks ALL episodic rows — status never exempts a row from the
964
+ // budget — so cap=2 keeps the two newest (one of them superseded) and
965
+ // evicts the oldest ACTIVE row.
966
+ await s.put(ep({ id: "new-active", effectiveAt: D(3) }));
967
+ await s.put(ep({ id: "mid-superseded", effectiveAt: D(2), status: "superseded" }));
968
+ await s.put(ep({ id: "old-active", effectiveAt: D(1) }));
969
+ const res = await s.prune({ now: D(9), cap: 2 });
970
+ expect(res.deletedOverCap).toBe(1);
971
+ expect(await s.get("old-active")).toBeNull();
972
+ expect((await s.get("mid-superseded"))?.id).toBe("mid-superseded");
973
+ expect((await s.get("new-active"))?.id).toBe("new-active");
974
+ }
975
+ finally {
976
+ await close?.(s);
977
+ }
978
+ });
979
+ test("prune equal event times evict by codepoint id order (deterministic tiebreak)", async () => {
980
+ const s = await makeStore();
981
+ try {
982
+ // Same effectiveAt; codepoint id order decides. Keep the NEWEST 2 under
983
+ // (effective DESC, id ASC) ordering ⇒ evict the last in that ordering:
984
+ // "b2" (0x62 highest codepoint sorts last among equals).
985
+ await s.put(ep({ id: "B10", effectiveAt: D(2) }));
986
+ await s.put(ep({ id: "a9", effectiveAt: D(2) }));
987
+ await s.put(ep({ id: "b2", effectiveAt: D(2) }));
988
+ await s.prune({ now: D(9), cap: 2 });
989
+ expect(await s.get("b2")).toBeNull();
990
+ expect((await s.get("B10"))?.id).toBe("B10");
991
+ expect((await s.get("a9"))?.id).toBe("a9");
992
+ }
993
+ finally {
994
+ await close?.(s);
995
+ }
996
+ });
997
+ test("prune namespacePrefix narrows both TTL and cap passes", async () => {
998
+ const s = await makeStore();
999
+ try {
1000
+ await s.put(ep({ id: "in", namespace: "route=/a", expiresAt: D(2) }));
1001
+ await s.put(ep({ id: "out", namespace: "route=/b", expiresAt: D(2) }));
1002
+ for (let i = 1; i <= 3; i++) {
1003
+ await s.put(ep({ id: `ka${i}`, namespace: "route=/a", effectiveAt: D(i + 2) }));
1004
+ }
1005
+ for (let i = 1; i <= 3; i++) {
1006
+ await s.put(ep({ id: `kb${i}`, namespace: "route=/b", effectiveAt: D(i + 2) }));
1007
+ }
1008
+ const res = await s.prune({ now: D(5), cap: 2, namespacePrefix: "route=/a" });
1009
+ expect(res.deletedExpired).toBe(1);
1010
+ expect(res.deletedOverCap).toBe(1);
1011
+ expect((await s.get("out"))?.id).toBe("out");
1012
+ expect(await s.get("ka1")).toBeNull();
1013
+ expect((await s.get("kb1"))?.id).toBe("kb1");
1014
+ }
1015
+ finally {
1016
+ await close?.(s);
1017
+ }
1018
+ });
1019
+ test("prune is idempotent — a second identical prune deletes nothing", async () => {
1020
+ const s = await makeStore();
1021
+ try {
1022
+ for (let i = 1; i <= 4; i++) {
1023
+ await s.put(ep({ id: `e${i}`, effectiveAt: D(i), ...(i === 1 ? { expiresAt: D(2) } : {}) }));
1024
+ }
1025
+ await s.prune({ now: D(5), cap: 2 });
1026
+ const second = await s.prune({ now: D(5), cap: 2 });
1027
+ expect(second).toEqual({ deletedExpired: 0, deletedOverCap: 0 });
1028
+ }
1029
+ finally {
1030
+ await close?.(s);
1031
+ }
1032
+ });
1033
+ test("episodic candidate approval is plain activation — no reconcile against actives", async () => {
1034
+ const s = await makeStore();
1035
+ try {
1036
+ // Identical data to an existing active row: a reconcile policy would
1037
+ // dedupe (delete the candidate); the episodic append policy activates it.
1038
+ await s.put(ep({ id: "prior", data: { event: "deploy" }, effectiveAt: D(1) }));
1039
+ await s.put(ep({ id: "cand", data: { event: "deploy" }, status: "candidate", effectiveAt: D(2) }));
1040
+ const res = await approveWithReconcile(s, "cand", {
1041
+ identityKeys: ["event"],
1042
+ now: D(3),
1043
+ });
1044
+ expect(res.action).toBe("activated");
1045
+ expect(res.superseded).toEqual([]);
1046
+ expect((await s.get("cand"))?.status).toBe("active");
1047
+ expect((await s.get("prior"))?.status).toBe("active");
1048
+ }
1049
+ finally {
1050
+ await close?.(s);
1051
+ }
1052
+ });
1053
+ test("supersede links and demotes episodic records (consolidation's dependency)", async () => {
1054
+ const s = await makeStore();
1055
+ try {
1056
+ // Consolidation writes a summary then supersedes each source episode.
1057
+ // Both backends must fan MANY sources into ONE summary's link list, and
1058
+ // active-only browse must stop surfacing the sources — otherwise recall
1059
+ // double-counts the events the summary already covers.
1060
+ await s.put(ep({ id: "src1", namespace: "ns", content: "run one" }));
1061
+ await s.put(ep({ id: "src2", namespace: "ns", content: "run two" }));
1062
+ await s.put(ep({ id: "sum", namespace: "ns", content: "summary", tags: ["consolidated"] }));
1063
+ await s.supersede("src1", "sum");
1064
+ await s.supersede("src2", "sum");
1065
+ await s.supersede("src1", "sum"); // repeat: links merge through a Set, never duplicate
1066
+ expect((await s.get("src1"))?.status).toBe("superseded");
1067
+ expect((await s.get("src2"))?.status).toBe("superseded");
1068
+ const links = (await s.get("sum"))?.supersedes;
1069
+ expect(links).toEqual(expect.arrayContaining(["src1", "src2"]));
1070
+ expect(links).toHaveLength(2);
1071
+ // active-only browse no longer surfaces the sources
1072
+ expect((await s.browse({ status: "active", kind: "episodic" })).records.map((r) => r.id)).toEqual(["sum"]);
1073
+ }
1074
+ finally {
1075
+ await close?.(s);
1076
+ }
1077
+ });
1078
+ test("browse applies orderBy in order and always terminates with the id tie-break", async () => {
1079
+ const s = await makeStore();
1080
+ try {
1081
+ // Deliberately tied on the leading key so the tie-break is the ONLY thing
1082
+ // deciding the order — and mixed-case ids so a locale collation would
1083
+ // disagree with byte order if the tie-break were not pinned.
1084
+ await s.put(rec({ id: "B", namespace: "ns", content: "B", confidence: 0.5, updatedAt: D(1) }));
1085
+ await s.put(rec({ id: "a", namespace: "ns", content: "a", confidence: 0.5, updatedAt: D(1) }));
1086
+ await s.put(rec({ id: "C", namespace: "ns", content: "C", confidence: 0.5, updatedAt: D(1) }));
1087
+ await s.put(rec({ id: "z", namespace: "ns", content: "z", confidence: 0.9, updatedAt: D(2) }));
1088
+ expect((await s.browse({ orderBy: [{ field: "confidence", dir: "desc" }] })).records.map((r) => r.id)).toEqual(["z", "B", "C", "a"]);
1089
+ expect((await s.browse({ orderBy: [{ field: "confidence", dir: "asc" }] })).records.map((r) => r.id)).toEqual(["B", "C", "a", "z"]);
1090
+ }
1091
+ finally {
1092
+ await close?.(s);
1093
+ }
1094
+ });
1095
+ test("browse honors a multi-key orderBy with mixed directions", async () => {
1096
+ const s = await makeStore();
1097
+ try {
1098
+ await s.put(rec({ id: "1", namespace: "ns=b", content: "1", confidence: 0.1 }));
1099
+ await s.put(rec({ id: "2", namespace: "ns=a", content: "2", confidence: 0.9 }));
1100
+ // "4" and "3" tie on BOTH keys, so the appended tie-break is the only thing
1101
+ // separating them — and they go in reverse id order so insertion order alone
1102
+ // cannot produce the expected sequence.
1103
+ await s.put(rec({ id: "4", namespace: "ns=a", content: "4", confidence: 0.1 }));
1104
+ await s.put(rec({ id: "3", namespace: "ns=a", content: "3", confidence: 0.1 }));
1105
+ expect((await s.browse({
1106
+ orderBy: [
1107
+ { field: "namespace", dir: "asc" },
1108
+ { field: "confidence", dir: "desc" },
1109
+ ],
1110
+ })).records.map((r) => r.id)).toEqual(["2", "3", "4", "1"]);
1111
+ }
1112
+ finally {
1113
+ await close?.(s);
1114
+ }
1115
+ });
1116
+ test("browse orders by the enum columns kind and status, uncollated on both backends", async () => {
1117
+ const s = await makeStore();
1118
+ try {
1119
+ // kind/status are the only sort columns Postgres resolves WITHOUT COLLATE "C",
1120
+ // safe only because both are closed lowercase-ASCII enums. Ids are deliberately
1121
+ // NOT in enum order, so a store that ignored the key and fell through to the
1122
+ // default `updated_at DESC, id ASC` would return "1","2","3","4" and fail.
1123
+ const ids = async (field, dir) => (await s.browse({ orderBy: [{ field, dir }] })).records.map((r) => r.id);
1124
+ await s.put(rec({ id: "1", namespace: "ns", content: "1", kind: "semantic", status: "candidate" }));
1125
+ await s.put(rec({ id: "2", namespace: "ns", content: "2", kind: "episodic", status: "superseded" }));
1126
+ await s.put(rec({ id: "3", namespace: "ns", content: "3", kind: "reflection", status: "active" }));
1127
+ await s.put(rec({ id: "4", namespace: "ns", content: "4", kind: "procedural", status: "active" }));
1128
+ expect(await ids("kind", "asc")).toEqual(["2", "4", "3", "1"]);
1129
+ expect(await ids("kind", "desc")).toEqual(["1", "3", "4", "2"]);
1130
+ expect(await ids("status", "asc")).toEqual(["3", "4", "1", "2"]);
1131
+ }
1132
+ finally {
1133
+ await close?.(s);
1134
+ }
1135
+ });
1136
+ test("browse with an empty orderBy is the documented default order", async () => {
1137
+ const s = await makeStore();
1138
+ try {
1139
+ await s.put(rec({ id: "old", namespace: "ns", content: "old", updatedAt: D(1) }));
1140
+ await s.put(rec({ id: "new", namespace: "ns", content: "new", updatedAt: D(2) }));
1141
+ expect((await s.browse({ orderBy: [] })).records.map((r) => r.id)).toEqual(["new", "old"]);
1142
+ expect((await s.browse()).records.map((r) => r.id)).toEqual(["new", "old"]);
1143
+ }
1144
+ finally {
1145
+ await close?.(s);
1146
+ }
1147
+ });
1148
+ test("browse walks the whole dataset through continuations under a fixed now", async () => {
1149
+ const s = await makeStore();
1150
+ try {
1151
+ // Odd days, so the expired row below can land BETWEEN two live ones.
1152
+ for (let i = 0; i < 7; i += 1) {
1153
+ await s.put(rec({ id: `r${i}`, namespace: "ns", content: `r${i}`, updatedAt: D(2 * i + 1) }));
1154
+ }
1155
+ // Sorts between r4 and r3, i.e. inside the SECOND window: a store that applies
1156
+ // the expiry clause to the first page and drops it on continuations leaks it here.
1157
+ await s.put(rec({ id: "dead", namespace: "ns", content: "dead", updatedAt: D(8), expiresAt: D(2) }));
1158
+ // Holding one `now` across the walk is the POSITIVE half of the cursor's `now`
1159
+ // contract; rejection of a re-stamped one is pinned separately. Without this,
1160
+ // a store that rejected every cursor carrying a `now` would still pass.
1161
+ const query = { limit: 3, now: D(20) };
1162
+ const seen = [];
1163
+ let page = await s.browse(query);
1164
+ expect(page.total).toBe(7);
1165
+ expect(page.continuation).not.toBeNull();
1166
+ seen.push(...page.records.map((r) => r.id));
1167
+ for (let walked = 0; page.continuation && walked < MAX_WALK_PAGES; walked += 1) {
1168
+ page = await s.browse({ ...query, cursor: page.continuation });
1169
+ // total is the WHOLE matching set on every window, never what remains.
1170
+ expect(page.total).toBe(7);
1171
+ seen.push(...page.records.map((r) => r.id));
1172
+ }
1173
+ expect(page.continuation).toBeNull();
1174
+ expect(seen).toEqual(["r6", "r5", "r4", "r3", "r2", "r1", "r0"]);
1175
+ expect(new Set(seen).size).toBe(7);
1176
+ }
1177
+ finally {
1178
+ await close?.(s);
1179
+ }
1180
+ });
1181
+ test("browse returns a null continuation when the window did not fill", async () => {
1182
+ const s = await makeStore();
1183
+ try {
1184
+ await s.put(rec({ id: "a", namespace: "ns", content: "a" }));
1185
+ expect((await s.browse({ limit: 10 })).continuation).toBeNull();
1186
+ expect((await s.browse()).continuation).toBeNull();
1187
+ // A page that fills EXACTLY still issues one; following it is a legal
1188
+ // zero-row window, not an error.
1189
+ const full = await s.browse({ limit: 1 });
1190
+ expect(full.continuation).not.toBeNull();
1191
+ const after = await s.browse({ limit: 1, cursor: full.continuation });
1192
+ expect(after.records).toEqual([]);
1193
+ expect(after.total).toBe(1);
1194
+ expect(after.continuation).toBeNull();
1195
+ }
1196
+ finally {
1197
+ await close?.(s);
1198
+ }
1199
+ });
1200
+ test("browse continuations survive tied sort keys by falling through to id", async () => {
1201
+ const s = await makeStore();
1202
+ try {
1203
+ for (const id of ["a", "B", "c", "D"]) {
1204
+ await s.put(rec({ id, namespace: "ns", content: id, updatedAt: D(1) }));
1205
+ }
1206
+ const seen = [];
1207
+ let page = await s.browse({ limit: 2 });
1208
+ seen.push(...page.records.map((r) => r.id));
1209
+ for (let walked = 0; page.continuation && walked < MAX_WALK_PAGES; walked += 1) {
1210
+ page = await s.browse({ limit: 2, cursor: page.continuation });
1211
+ seen.push(...page.records.map((r) => r.id));
1212
+ }
1213
+ expect(page.continuation).toBeNull();
1214
+ expect(seen).toEqual(["B", "D", "a", "c"]);
1215
+ }
1216
+ finally {
1217
+ await close?.(s);
1218
+ }
1219
+ });
1220
+ test("browse continuations round-trip a float confidence key exactly", async () => {
1221
+ const s = await makeStore();
1222
+ try {
1223
+ // Postgres stores confidence as float4; a cursor key that is not cast back to
1224
+ // ::real compares false against the row it came from and the walk stalls or
1225
+ // repeats. These values are all inexact in float4.
1226
+ for (const [id, confidence] of [
1227
+ ["a", 0.1],
1228
+ ["b", 0.2],
1229
+ ["c", 0.3],
1230
+ ]) {
1231
+ await s.put(rec({ id, namespace: "ns", content: id, confidence }));
1232
+ }
1233
+ const seen = [];
1234
+ let page = await s.browse({ limit: 1, orderBy: [{ field: "confidence", dir: "desc" }] });
1235
+ seen.push(...page.records.map((r) => r.id));
1236
+ for (let walked = 0; page.continuation && walked < MAX_WALK_PAGES; walked += 1) {
1237
+ page = await s.browse({
1238
+ limit: 1,
1239
+ orderBy: [{ field: "confidence", dir: "desc" }],
1240
+ cursor: page.continuation,
1241
+ });
1242
+ seen.push(...page.records.map((r) => r.id));
1243
+ }
1244
+ expect(page.continuation).toBeNull();
1245
+ expect(seen).toEqual(["c", "b", "a"]);
1246
+ }
1247
+ finally {
1248
+ await close?.(s);
1249
+ }
1250
+ });
1251
+ test("browse rejects a continuation issued for a different query", async () => {
1252
+ const s = await makeStore();
1253
+ try {
1254
+ for (let i = 0; i < 3; i += 1) {
1255
+ await s.put(rec({ id: `r${i}`, namespace: "ns", content: `r${i}`, updatedAt: D(i + 1) }));
1256
+ }
1257
+ const page = await s.browse({ limit: 1 });
1258
+ const cursor = page.continuation;
1259
+ // A cursor carries its query's fingerprint, so it can never be replayed
1260
+ // against a different filter/sort and silently answer the wrong question.
1261
+ await expectContinuationInvalid(() => s.browse({ limit: 1, cursor, status: "active" }));
1262
+ await expectContinuationInvalid(() => s.browse({ limit: 1, cursor, orderBy: [{ field: "confidence", dir: "asc" }] }));
1263
+ await expectContinuationInvalid(() => s.browse({ limit: 1, cursor: "not-a-cursor" }));
1264
+ // `now` decides which rows are expired, so it is dataset identity too. A caller
1265
+ // that re-stamps it per request — the natural shape of an HTTP boundary — kills
1266
+ // its own walk at page two; it must hold `now` fixed for the whole walk.
1267
+ await expectContinuationInvalid(() => s.browse({ limit: 1, cursor, now: D(9) }));
1268
+ }
1269
+ finally {
1270
+ await close?.(s);
1271
+ }
1272
+ });
1273
+ test("browse continuations compose with filters, and total stays the filtered set", async () => {
1274
+ const s = await makeStore();
1275
+ try {
1276
+ for (let i = 0; i < 5; i += 1) {
1277
+ await s.put(rec({
1278
+ id: `c${i}`,
1279
+ namespace: "ns",
1280
+ content: `c${i}`,
1281
+ status: "candidate",
1282
+ updatedAt: D(i + 1),
1283
+ }));
1284
+ }
1285
+ await s.put(rec({ id: "keep", namespace: "ns", content: "keep", updatedAt: D(9) }));
1286
+ const query = {
1287
+ limit: 2,
1288
+ filters: [{ field: "status", op: "in", values: ["candidate"] }],
1289
+ };
1290
+ const seen = [];
1291
+ let page = await s.browse(query);
1292
+ expect(page.total).toBe(5);
1293
+ seen.push(...page.records.map((r) => r.id));
1294
+ for (let walked = 0; page.continuation && walked < MAX_WALK_PAGES; walked += 1) {
1295
+ page = await s.browse({ ...query, cursor: page.continuation });
1296
+ expect(page.total).toBe(5);
1297
+ seen.push(...page.records.map((r) => r.id));
1298
+ }
1299
+ expect(page.continuation).toBeNull();
1300
+ expect(seen).toEqual(["c4", "c3", "c2", "c1", "c0"]);
1301
+ }
1302
+ finally {
1303
+ await close?.(s);
1304
+ }
1305
+ });
1306
+ test("browse reads records and total from ONE snapshot, even under concurrent writes", async () => {
1307
+ const s = await makeStore();
1308
+ try {
1309
+ for (let i = 0; i < 60; i += 1) {
1310
+ await s.put(rec({ id: `r${String(i).padStart(2, "0")}`, namespace: "ns", content: `r${i}` }));
1311
+ }
1312
+ // Two non-transactional statements can count 30 and return 60 (or the
1313
+ // reverse) when a delete lands between them, and the UI then renders
1314
+ // "60 loaded of 30 matching". Inside one snapshot that is unrepresentable.
1315
+ const [page] = await Promise.all([
1316
+ s.browse({ limit: 1000 }),
1317
+ (async () => {
1318
+ for (let i = 0; i < 30; i += 1)
1319
+ await s.delete(`r${String(i).padStart(2, "0")}`);
1320
+ })(),
1321
+ ]);
1322
+ expect(page.records.length).toBe(page.total);
1323
+ // And the model converges on the next read.
1324
+ const after = await s.browse({ limit: 1000 });
1325
+ expect(after.total).toBe(30);
1326
+ expect(after.records.length).toBe(30);
1327
+ }
1328
+ finally {
1329
+ await close?.(s);
1330
+ }
1331
+ });
1332
+ });
1333
+ }