@nanobpm/agentic 0.1.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 +22 -0
- package/dist/blackboard/family.d.ts +40 -0
- package/dist/blackboard/family.js +151 -0
- package/dist/blackboard/index.d.ts +21 -0
- package/dist/blackboard/index.js +19 -0
- package/dist/blackboard/schema.d.ts +30 -0
- package/dist/blackboard/schema.js +42 -0
- package/dist/blackboard/store.d.ts +138 -0
- package/dist/blackboard/store.js +216 -0
- package/dist/blackboard/test-db.d.ts +5 -0
- package/dist/blackboard/test-db.js +42 -0
- package/dist/channel/auth.d.ts +41 -0
- package/dist/channel/auth.js +67 -0
- package/dist/channel/clock.d.ts +11 -0
- package/dist/channel/clock.js +4 -0
- package/dist/channel/connection.d.ts +75 -0
- package/dist/channel/connection.js +14 -0
- package/dist/channel/dispatch.d.ts +46 -0
- package/dist/channel/dispatch.js +86 -0
- package/dist/channel/hub.d.ts +78 -0
- package/dist/channel/hub.js +157 -0
- package/dist/channel/index.d.ts +27 -0
- package/dist/channel/index.js +20 -0
- package/dist/channel/registry.d.ts +68 -0
- package/dist/channel/registry.js +84 -0
- package/dist/channel/ws-transport.d.ts +23 -0
- package/dist/channel/ws-transport.js +178 -0
- package/dist/cockpit/boot.d.ts +68 -0
- package/dist/cockpit/boot.js +202 -0
- package/dist/cockpit/fake-dom.d.ts +37 -0
- package/dist/cockpit/fake-dom.js +73 -0
- package/dist/cockpit/index.d.ts +27 -0
- package/dist/cockpit/index.js +27 -0
- package/dist/cockpit/relay-client.d.ts +52 -0
- package/dist/cockpit/relay-client.js +192 -0
- package/dist/cockpit/render.d.ts +58 -0
- package/dist/cockpit/render.js +122 -0
- package/dist/cockpit/terminal-session.d.ts +95 -0
- package/dist/cockpit/terminal-session.js +123 -0
- package/dist/cockpit/view.d.ts +79 -0
- package/dist/cockpit/view.js +58 -0
- package/dist/demand/c8-rest.d.ts +77 -0
- package/dist/demand/c8-rest.js +123 -0
- package/dist/demand/index.d.ts +24 -0
- package/dist/demand/index.js +24 -0
- package/dist/demand/model.d.ts +68 -0
- package/dist/demand/model.js +118 -0
- package/dist/demand/taskdef.d.ts +40 -0
- package/dist/demand/taskdef.js +67 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/presence/family.d.ts +40 -0
- package/dist/presence/family.js +166 -0
- package/dist/presence/index.d.ts +19 -0
- package/dist/presence/index.js +17 -0
- package/dist/presence/schema.d.ts +20 -0
- package/dist/presence/schema.js +32 -0
- package/dist/presence/store.d.ts +130 -0
- package/dist/presence/store.js +191 -0
- package/dist/presence/test-db.d.ts +5 -0
- package/dist/presence/test-db.js +42 -0
- package/dist/protocol/conformance/frames.d.ts +24 -0
- package/dist/protocol/conformance/frames.js +116 -0
- package/dist/protocol/conformance/index.d.ts +13 -0
- package/dist/protocol/conformance/index.js +13 -0
- package/dist/protocol/conformance/malformed.d.ts +14 -0
- package/dist/protocol/conformance/malformed.js +44 -0
- package/dist/protocol/conformance/tokens.d.ts +19 -0
- package/dist/protocol/conformance/tokens.js +49 -0
- package/dist/protocol/conformance/vocab.d.ts +23 -0
- package/dist/protocol/conformance/vocab.js +97 -0
- package/dist/protocol/families.d.ts +32 -0
- package/dist/protocol/families.js +45 -0
- package/dist/protocol/frame.d.ts +45 -0
- package/dist/protocol/frame.js +114 -0
- package/dist/protocol/hex.d.ts +7 -0
- package/dist/protocol/hex.js +26 -0
- package/dist/protocol/index.d.ts +23 -0
- package/dist/protocol/index.js +23 -0
- package/dist/protocol/lanes.d.ts +36 -0
- package/dist/protocol/lanes.js +40 -0
- package/dist/protocol/payloads.d.ts +64 -0
- package/dist/protocol/payloads.js +122 -0
- package/dist/protocol/token.d.ts +34 -0
- package/dist/protocol/token.js +81 -0
- package/dist/protocol/vocab/schema.d.ts +51 -0
- package/dist/protocol/vocab/schema.js +218 -0
- package/dist/relay/incarnation.d.ts +17 -0
- package/dist/relay/incarnation.js +50 -0
- package/dist/relay/index.d.ts +25 -0
- package/dist/relay/index.js +22 -0
- package/dist/relay/relay-family.d.ts +68 -0
- package/dist/relay/relay-family.js +272 -0
- package/dist/relay/ring.d.ts +49 -0
- package/dist/relay/ring.js +105 -0
- package/dist/relay/scheduler.d.ts +72 -0
- package/dist/relay/scheduler.js +180 -0
- package/dist/relay/validate.d.ts +29 -0
- package/dist/relay/validate.js +39 -0
- package/dist/transcript/index.d.ts +18 -0
- package/dist/transcript/index.js +17 -0
- package/dist/transcript/schema.d.ts +32 -0
- package/dist/transcript/schema.js +48 -0
- package/dist/transcript/store.d.ts +192 -0
- package/dist/transcript/store.js +347 -0
- package/dist/transcript/test-db.d.ts +5 -0
- package/dist/transcript/test-db.js +41 -0
- package/dist/vocab/core-vocab.d.ts +26 -0
- package/dist/vocab/core-vocab.js +67 -0
- package/dist/vocab/diversity.d.ts +78 -0
- package/dist/vocab/diversity.js +89 -0
- package/dist/vocab/index.d.ts +22 -0
- package/dist/vocab/index.js +22 -0
- package/dist/vocab/merge.d.ts +9 -0
- package/dist/vocab/merge.js +104 -0
- package/dist/vocab/requires.d.ts +49 -0
- package/dist/vocab/requires.js +107 -0
- package/dist/vocab/resolver.d.ts +62 -0
- package/dist/vocab/resolver.js +149 -0
- package/dist/vocab/serve.d.ts +39 -0
- package/dist/vocab/serve.js +36 -0
- package/package.json +108 -0
- package/page/cockpit.css +114 -0
- package/page/cockpit.page.json +33 -0
- package/page/embed.html +40 -0
- package/page/mount.js +78 -0
- package/page/standalone.html +43 -0
- package/src/blackboard/family.test.ts +280 -0
- package/src/blackboard/family.ts +208 -0
- package/src/blackboard/index.ts +42 -0
- package/src/blackboard/schema.test.ts +60 -0
- package/src/blackboard/schema.ts +44 -0
- package/src/blackboard/store.test.ts +189 -0
- package/src/blackboard/store.ts +331 -0
- package/src/blackboard/test-db.ts +47 -0
- package/src/channel/auth.test.ts +64 -0
- package/src/channel/auth.ts +101 -0
- package/src/channel/clock.ts +14 -0
- package/src/channel/connection.ts +77 -0
- package/src/channel/dispatch.test.ts +83 -0
- package/src/channel/dispatch.ts +102 -0
- package/src/channel/hub.test.ts +335 -0
- package/src/channel/hub.ts +222 -0
- package/src/channel/index.ts +55 -0
- package/src/channel/registry.test.ts +73 -0
- package/src/channel/registry.ts +137 -0
- package/src/channel/ws-transport.test.ts +234 -0
- package/src/channel/ws-transport.ts +212 -0
- package/src/cockpit/boot.test.ts +374 -0
- package/src/cockpit/boot.ts +280 -0
- package/src/cockpit/fake-dom.ts +90 -0
- package/src/cockpit/index.ts +63 -0
- package/src/cockpit/relay-client.test.ts +359 -0
- package/src/cockpit/relay-client.ts +234 -0
- package/src/cockpit/render.test.ts +149 -0
- package/src/cockpit/render.ts +194 -0
- package/src/cockpit/terminal-session.test.ts +252 -0
- package/src/cockpit/terminal-session.ts +194 -0
- package/src/cockpit/view.test.ts +117 -0
- package/src/cockpit/view.ts +140 -0
- package/src/demand/c8-rest.test.ts +140 -0
- package/src/demand/c8-rest.ts +167 -0
- package/src/demand/index.ts +42 -0
- package/src/demand/model.test.ts +197 -0
- package/src/demand/model.ts +183 -0
- package/src/demand/taskdef.test.ts +85 -0
- package/src/demand/taskdef.ts +78 -0
- package/src/index.ts +17 -0
- package/src/presence/family.test.ts +252 -0
- package/src/presence/family.ts +205 -0
- package/src/presence/index.ts +26 -0
- package/src/presence/schema.test.ts +53 -0
- package/src/presence/schema.ts +34 -0
- package/src/presence/store.test.ts +190 -0
- package/src/presence/store.ts +287 -0
- package/src/presence/test-db.test.ts +57 -0
- package/src/presence/test-db.ts +47 -0
- package/src/protocol/conformance/corpus.test.ts +66 -0
- package/src/protocol/conformance/frames.ts +142 -0
- package/src/protocol/conformance/index.ts +29 -0
- package/src/protocol/conformance/malformed.ts +59 -0
- package/src/protocol/conformance/tokens.ts +70 -0
- package/src/protocol/conformance/vocab.ts +122 -0
- package/src/protocol/families.ts +54 -0
- package/src/protocol/frame.test.ts +116 -0
- package/src/protocol/frame.ts +171 -0
- package/src/protocol/hex.ts +28 -0
- package/src/protocol/index.ts +84 -0
- package/src/protocol/lanes.test.ts +82 -0
- package/src/protocol/lanes.ts +54 -0
- package/src/protocol/payloads.test.ts +91 -0
- package/src/protocol/payloads.ts +201 -0
- package/src/protocol/token.test.ts +57 -0
- package/src/protocol/token.ts +123 -0
- package/src/protocol/vocab/schema.test.ts +67 -0
- package/src/protocol/vocab/schema.ts +281 -0
- package/src/relay/incarnation.test.ts +53 -0
- package/src/relay/incarnation.ts +54 -0
- package/src/relay/index.ts +34 -0
- package/src/relay/integration.test.ts +135 -0
- package/src/relay/relay-family.test.ts +236 -0
- package/src/relay/relay-family.ts +336 -0
- package/src/relay/ring.test.ts +138 -0
- package/src/relay/ring.ts +136 -0
- package/src/relay/scheduler.test.ts +233 -0
- package/src/relay/scheduler.ts +208 -0
- package/src/relay/validate.test.ts +43 -0
- package/src/relay/validate.ts +44 -0
- package/src/transcript/index.ts +33 -0
- package/src/transcript/integration.test.ts +108 -0
- package/src/transcript/schema.test.ts +69 -0
- package/src/transcript/schema.ts +51 -0
- package/src/transcript/store.test.ts +285 -0
- package/src/transcript/store.ts +530 -0
- package/src/transcript/test-db.ts +46 -0
- package/src/vocab/core-vocab.test.ts +34 -0
- package/src/vocab/core-vocab.ts +88 -0
- package/src/vocab/diversity.test.ts +153 -0
- package/src/vocab/diversity.ts +169 -0
- package/src/vocab/index.ts +55 -0
- package/src/vocab/merge.test.ts +73 -0
- package/src/vocab/merge.ts +117 -0
- package/src/vocab/requires.test.ts +69 -0
- package/src/vocab/requires.ts +155 -0
- package/src/vocab/resolver.test.ts +118 -0
- package/src/vocab/resolver.ts +187 -0
- package/src/vocab/serve.test.ts +64 -0
- package/src/vocab/serve.ts +66 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { afterEach, beforeEach, test } from "node:test";
|
|
3
|
+
import { TranscriptLifecycleError, TranscriptStore } from "./store.ts";
|
|
4
|
+
import { TRANSCRIPT_STREAM_TABLE } from "./schema.ts";
|
|
5
|
+
import { openTestDb, type TestDb } from "./test-db.ts";
|
|
6
|
+
|
|
7
|
+
/** A hand-driven clock so retention/timestamps are deterministic. */
|
|
8
|
+
function fakeClock(start = 1_000): { now(): number; set(t: number): void } {
|
|
9
|
+
let t = start;
|
|
10
|
+
return { now: () => t, set: (v: number) => (t = v) };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let db: TestDb;
|
|
14
|
+
afterEach(() => db.close());
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
db = openTestDb();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function newStore(opts: { retentionMs?: number; clock?: { now(): number } } = {}): TranscriptStore {
|
|
20
|
+
const store = new TranscriptStore(db, {
|
|
21
|
+
ephemeralRetentionMs: opts.retentionMs,
|
|
22
|
+
clock: opts.clock,
|
|
23
|
+
});
|
|
24
|
+
store.ensureSchema();
|
|
25
|
+
return store;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const chunks = (n: number, base = 0): { offset: number; chunk: string }[] =>
|
|
29
|
+
Array.from({ length: n }, (_, i) => ({ offset: base + i, chunk: `c${base + i}` }));
|
|
30
|
+
|
|
31
|
+
test("open is idempotent and first-wins on lifecycle", () => {
|
|
32
|
+
const store = newStore();
|
|
33
|
+
const a = store.open("s", "ephemeral");
|
|
34
|
+
assert.equal(a.lifecycle, "ephemeral");
|
|
35
|
+
assert.equal(a.status, "open");
|
|
36
|
+
assert.equal(a.nextOffset, 0);
|
|
37
|
+
assert.equal(a.firstOffset, undefined);
|
|
38
|
+
// A second open with a different lifecycle must not mutate the first.
|
|
39
|
+
const b = store.open("s", "long-lived");
|
|
40
|
+
assert.equal(b.lifecycle, "ephemeral");
|
|
41
|
+
assert.equal(store.count(), 1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("record persists chunks and tracks the offset window", () => {
|
|
45
|
+
const store = newStore();
|
|
46
|
+
const written = store.record("s", chunks(3), "long-lived");
|
|
47
|
+
assert.equal(written, 3);
|
|
48
|
+
const meta = store.get("s");
|
|
49
|
+
assert.equal(meta?.firstOffset, 0);
|
|
50
|
+
assert.equal(meta?.nextOffset, 3);
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
store.read("s").map((c) => c.chunk),
|
|
53
|
+
["c0", "c1", "c2"],
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("record is idempotent by (stream, offset) — re-recording writes nothing", () => {
|
|
58
|
+
const store = newStore();
|
|
59
|
+
store.record("s", chunks(3));
|
|
60
|
+
const again = store.record("s", chunks(3));
|
|
61
|
+
assert.equal(again, 0);
|
|
62
|
+
assert.equal(store.read("s").length, 3);
|
|
63
|
+
// Overlapping resume: offsets 2,3,4 — only 3,4 are new.
|
|
64
|
+
const overlap = store.record("s", chunks(3, 2));
|
|
65
|
+
assert.equal(overlap, 2);
|
|
66
|
+
assert.equal(store.get("s")?.nextOffset, 5);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("record rejects a non-integer / negative offset", () => {
|
|
70
|
+
const store = newStore();
|
|
71
|
+
assert.throws(() => store.record("s", [{ offset: -1, chunk: "x" }]), RangeError);
|
|
72
|
+
assert.throws(() => store.record("s", [{ offset: 1.5, chunk: "x" }]), RangeError);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("record rejects an offset at MAX_SAFE_INTEGER (nextOffset would overflow safe range)", () => {
|
|
76
|
+
const store = newStore();
|
|
77
|
+
assert.throws(
|
|
78
|
+
() => store.record("s", [{ offset: Number.MAX_SAFE_INTEGER, chunk: "x" }]),
|
|
79
|
+
RangeError,
|
|
80
|
+
);
|
|
81
|
+
// The rejection is before any write — the stream stays empty.
|
|
82
|
+
assert.equal(store.read("s").length, 0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("record is atomic — an invalid offset after a valid one persists nothing", () => {
|
|
86
|
+
const store = newStore();
|
|
87
|
+
// A later invalid entry must roll back the whole batch (SAVEPOINT), so the
|
|
88
|
+
// earlier valid chunk is never partially persisted and the offset window
|
|
89
|
+
// metadata (first/next-offset) stays pristine.
|
|
90
|
+
assert.throws(
|
|
91
|
+
() =>
|
|
92
|
+
store.record("s", [
|
|
93
|
+
{ offset: 0, chunk: "c0" },
|
|
94
|
+
{ offset: Number.MAX_SAFE_INTEGER, chunk: "bad" },
|
|
95
|
+
]),
|
|
96
|
+
RangeError,
|
|
97
|
+
);
|
|
98
|
+
assert.equal(store.read("s").length, 0, "no chunk persisted from a batch that later throws");
|
|
99
|
+
const meta = store.get("s");
|
|
100
|
+
assert.equal(meta?.firstOffset, undefined);
|
|
101
|
+
assert.equal(meta?.nextOffset, 0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("record atomicity holds on an already-populated stream", () => {
|
|
105
|
+
const store = newStore();
|
|
106
|
+
store.record("s", chunks(2)); // offsets 0,1
|
|
107
|
+
assert.throws(
|
|
108
|
+
() =>
|
|
109
|
+
store.record("s", [
|
|
110
|
+
{ offset: 2, chunk: "c2" },
|
|
111
|
+
{ offset: -1, chunk: "bad" },
|
|
112
|
+
]),
|
|
113
|
+
RangeError,
|
|
114
|
+
);
|
|
115
|
+
// The new valid chunk (offset 2) is rolled back with the failed batch; the
|
|
116
|
+
// stream is left exactly as it was before the call.
|
|
117
|
+
assert.deepEqual(
|
|
118
|
+
store.read("s").map((c) => c.offset),
|
|
119
|
+
[0, 1],
|
|
120
|
+
);
|
|
121
|
+
assert.equal(store.get("s")?.nextOffset, 2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("constructor rejects a non-finite / negative ephemeralRetentionMs", () => {
|
|
125
|
+
assert.throws(() => new TranscriptStore(db, { ephemeralRetentionMs: Number.NaN }), RangeError);
|
|
126
|
+
assert.throws(
|
|
127
|
+
() => new TranscriptStore(db, { ephemeralRetentionMs: Number.POSITIVE_INFINITY }),
|
|
128
|
+
RangeError,
|
|
129
|
+
);
|
|
130
|
+
assert.throws(() => new TranscriptStore(db, { ephemeralRetentionMs: -1 }), RangeError);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("record refuses a lifecycle that mismatches an already-open stream, before writing", () => {
|
|
134
|
+
const store = newStore();
|
|
135
|
+
store.record("s", chunks(2), "long-lived");
|
|
136
|
+
// A mismatched lifecycle must fail fast and persist nothing new.
|
|
137
|
+
assert.throws(
|
|
138
|
+
() => store.record("s", chunks(2, 2), "ephemeral"),
|
|
139
|
+
TranscriptLifecycleError,
|
|
140
|
+
);
|
|
141
|
+
assert.equal(store.read("s").length, 2, "no chunks written on lifecycle mismatch");
|
|
142
|
+
assert.equal(store.get("s")?.lifecycle, "long-lived");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("reading a stream row with a corrupt lifecycle/status fails fast", () => {
|
|
146
|
+
const store = newStore();
|
|
147
|
+
db.run(
|
|
148
|
+
`INSERT INTO ${TRANSCRIPT_STREAM_TABLE} (stream, lifecycle, status, created_at, next_offset)
|
|
149
|
+
VALUES (?, ?, ?, ?, 0)`,
|
|
150
|
+
["bad", "bogus-lifecycle", "open", "2020-01-01T00:00:00.000Z"],
|
|
151
|
+
);
|
|
152
|
+
assert.throws(() => store.get("bad"), /lifecycle/);
|
|
153
|
+
db.run(
|
|
154
|
+
`INSERT INTO ${TRANSCRIPT_STREAM_TABLE} (stream, lifecycle, status, created_at, next_offset)
|
|
155
|
+
VALUES (?, ?, ?, ?, 0)`,
|
|
156
|
+
["bad2", "ephemeral", "bogus-status", "2020-01-01T00:00:00.000Z"],
|
|
157
|
+
);
|
|
158
|
+
assert.throws(() => store.get("bad2"), /status/);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("since reattaches from an offset with the live nextOffset", () => {
|
|
162
|
+
const store = newStore();
|
|
163
|
+
store.record("s", chunks(5), "long-lived");
|
|
164
|
+
const slice = store.since("s", 2);
|
|
165
|
+
assert.deepEqual(
|
|
166
|
+
slice.entries.map((e) => e.offset),
|
|
167
|
+
[2, 3, 4],
|
|
168
|
+
);
|
|
169
|
+
assert.equal(slice.gap, false);
|
|
170
|
+
assert.equal(slice.nextOffset, 5);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("since on an unknown stream is empty and gap-free", () => {
|
|
174
|
+
const store = newStore();
|
|
175
|
+
const slice = store.since("nope", 0);
|
|
176
|
+
assert.deepEqual(slice.entries, []);
|
|
177
|
+
assert.equal(slice.gap, false);
|
|
178
|
+
assert.equal(slice.nextOffset, 0);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("since past the head is empty (consumer already has everything), no gap", () => {
|
|
182
|
+
const store = newStore();
|
|
183
|
+
store.record("s", chunks(3));
|
|
184
|
+
const slice = store.since("s", 3);
|
|
185
|
+
assert.deepEqual(slice.entries, []);
|
|
186
|
+
assert.equal(slice.gap, false);
|
|
187
|
+
assert.equal(slice.nextOffset, 3);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("truncateBefore drops the head and a reattach before it reports a gap", () => {
|
|
191
|
+
const store = newStore();
|
|
192
|
+
store.record("s", chunks(6), "long-lived");
|
|
193
|
+
const dropped = store.truncateBefore("s", 3);
|
|
194
|
+
assert.equal(dropped, 3);
|
|
195
|
+
const meta = store.get("s");
|
|
196
|
+
assert.equal(meta?.firstOffset, 3);
|
|
197
|
+
assert.equal(meta?.nextOffset, 6, "nextOffset is a monotonic high-water mark, unaffected by head truncation");
|
|
198
|
+
// Reattach from an evicted offset: best-effort tail + gap.
|
|
199
|
+
const slice = store.since("s", 1);
|
|
200
|
+
assert.equal(slice.gap, true);
|
|
201
|
+
assert.deepEqual(
|
|
202
|
+
slice.entries.map((e) => e.offset),
|
|
203
|
+
[3, 4, 5],
|
|
204
|
+
);
|
|
205
|
+
// Reattach from a still-retained offset: no gap.
|
|
206
|
+
assert.equal(store.since("s", 4).gap, false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("truncateBefore refuses to touch an ephemeral transcript", () => {
|
|
210
|
+
const store = newStore();
|
|
211
|
+
store.record("s", chunks(3), "ephemeral");
|
|
212
|
+
assert.throws(() => store.truncateBefore("s", 1), TranscriptLifecycleError);
|
|
213
|
+
assert.equal(store.read("s").length, 3, "ephemeral transcript is left whole");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("sweep retires completed ephemeral transcripts past the retention window, keeps long-lived", () => {
|
|
217
|
+
const clock = fakeClock(10_000);
|
|
218
|
+
const store = newStore({ retentionMs: 1_000, clock });
|
|
219
|
+
// An ephemeral run: record + complete via flush.
|
|
220
|
+
store.record("eph", chunks(2), "ephemeral");
|
|
221
|
+
store.flush("eph", { since: () => ({ entries: [] }), nextOffset: 2 }, "ephemeral");
|
|
222
|
+
assert.equal(store.get("eph")?.status, "completed");
|
|
223
|
+
// A long-lived stream: open, never completes.
|
|
224
|
+
store.record("live", chunks(2), "long-lived");
|
|
225
|
+
|
|
226
|
+
// Not yet expired (completed_at == 10_000, cutoff = now - 1_000).
|
|
227
|
+
clock.set(10_500);
|
|
228
|
+
assert.deepEqual(store.sweep(), []);
|
|
229
|
+
assert.equal(store.count(), 2);
|
|
230
|
+
|
|
231
|
+
// Past the window: the ephemeral transcript is retired, the long-lived one stays.
|
|
232
|
+
clock.set(11_001);
|
|
233
|
+
assert.deepEqual(store.sweep(), ["eph"]);
|
|
234
|
+
assert.equal(store.get("eph"), undefined);
|
|
235
|
+
assert.equal(store.read("eph").length, 0);
|
|
236
|
+
assert.equal(store.get("live")?.lifecycle, "long-lived");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("sweep is atomic — a mid-sweep delete failure rolls the whole sweep back", () => {
|
|
240
|
+
const clock = fakeClock(10_000);
|
|
241
|
+
// Two ephemeral streams both eligible for the sweep.
|
|
242
|
+
const base = newStore({ retentionMs: 0, clock });
|
|
243
|
+
base.flush("a", { since: () => ({ entries: chunks(2) }), nextOffset: 2 }, "ephemeral");
|
|
244
|
+
base.flush("b", { since: () => ({ entries: chunks(2) }), nextOffset: 2 }, "ephemeral");
|
|
245
|
+
assert.equal(base.count(), 2);
|
|
246
|
+
|
|
247
|
+
// Wrap the shared connection so the stream-table DELETE throws once (a
|
|
248
|
+
// simulated mid-sweep failure). Everything else delegates to the real db, so
|
|
249
|
+
// the SAVEPOINT still runs against the same underlying connection.
|
|
250
|
+
let boom = true;
|
|
251
|
+
const faulting: TestDb = {
|
|
252
|
+
exec: (sql) => db.exec(sql),
|
|
253
|
+
run: (sql, params) => {
|
|
254
|
+
if (boom && sql.includes(`DELETE FROM ${TRANSCRIPT_STREAM_TABLE}`)) {
|
|
255
|
+
boom = false;
|
|
256
|
+
throw new Error("injected mid-sweep delete failure");
|
|
257
|
+
}
|
|
258
|
+
return db.run(sql, params);
|
|
259
|
+
},
|
|
260
|
+
all: <T>(sql: string, params?: unknown[]): T[] => db.all<T>(sql, params),
|
|
261
|
+
close: () => db.close(),
|
|
262
|
+
};
|
|
263
|
+
const store = new TranscriptStore(faulting, { ephemeralRetentionMs: 0, clock });
|
|
264
|
+
|
|
265
|
+
clock.set(10_001);
|
|
266
|
+
assert.throws(() => store.sweep(), /injected mid-sweep delete failure/);
|
|
267
|
+
|
|
268
|
+
// Nothing was swept: both streams and all their chunks survive intact, so the
|
|
269
|
+
// returned list can never claim a deletion that did not actually happen.
|
|
270
|
+
const check = newStore({ retentionMs: 0, clock });
|
|
271
|
+
assert.equal(check.count(), 2);
|
|
272
|
+
assert.equal(check.read("a").length, 2);
|
|
273
|
+
assert.equal(check.read("b").length, 2);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("a fully-swept reattach is empty and gap-free (nothing was lost)", () => {
|
|
277
|
+
const clock = fakeClock(0);
|
|
278
|
+
const store = newStore({ retentionMs: 0, clock });
|
|
279
|
+
store.flush("eph", { since: () => ({ entries: chunks(2) }), nextOffset: 2 }, "ephemeral");
|
|
280
|
+
clock.set(1);
|
|
281
|
+
store.sweep();
|
|
282
|
+
const slice = store.since("eph", 0);
|
|
283
|
+
assert.deepEqual(slice.entries, []);
|
|
284
|
+
assert.equal(slice.gap, false);
|
|
285
|
+
});
|