@ny4n/durable 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +711 -2
- package/dist/documents.d.ts +66 -0
- package/dist/documents.d.ts.map +1 -0
- package/dist/documents.js +98 -0
- package/dist/documents.js.map +1 -0
- package/dist/entries.d.ts +31 -0
- package/dist/entries.d.ts.map +1 -0
- package/dist/entries.js +31 -0
- package/dist/entries.js.map +1 -0
- package/dist/env/decode.d.ts +16 -0
- package/dist/env/decode.d.ts.map +1 -0
- package/dist/env/decode.js +28 -0
- package/dist/env/decode.js.map +1 -0
- package/dist/env/index.d.ts +263 -0
- package/dist/env/index.d.ts.map +1 -0
- package/dist/env/index.js +49 -0
- package/dist/env/index.js.map +1 -0
- package/dist/env/line-scan.d.ts +13 -0
- package/dist/env/line-scan.d.ts.map +1 -0
- package/dist/env/line-scan.js +147 -0
- package/dist/env/line-scan.js.map +1 -0
- package/dist/env/node-watch.d.ts +29 -0
- package/dist/env/node-watch.d.ts.map +1 -0
- package/dist/env/node-watch.js +485 -0
- package/dist/env/node-watch.js.map +1 -0
- package/dist/env/node.d.ts +56 -0
- package/dist/env/node.d.ts.map +1 -0
- package/dist/env/node.js +1166 -0
- package/dist/env/node.js.map +1 -0
- package/dist/errors.d.ts +32 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +53 -0
- package/dist/errors.js.map +1 -0
- package/dist/harness/agent.d.ts +33 -0
- package/dist/harness/agent.d.ts.map +1 -0
- package/dist/harness/agent.js +235 -0
- package/dist/harness/agent.js.map +1 -0
- package/dist/harness/assistant.d.ts +15 -0
- package/dist/harness/assistant.d.ts.map +1 -0
- package/dist/harness/assistant.js +23 -0
- package/dist/harness/assistant.js.map +1 -0
- package/dist/harness/compaction.d.ts +56 -0
- package/dist/harness/compaction.d.ts.map +1 -0
- package/dist/harness/compaction.js +384 -0
- package/dist/harness/compaction.js.map +1 -0
- package/dist/harness/context.d.ts +63 -0
- package/dist/harness/context.d.ts.map +1 -0
- package/dist/harness/context.js +221 -0
- package/dist/harness/context.js.map +1 -0
- package/dist/harness/define.d.ts +18 -0
- package/dist/harness/define.d.ts.map +1 -0
- package/dist/harness/define.js +25 -0
- package/dist/harness/define.js.map +1 -0
- package/dist/harness/events.d.ts +180 -0
- package/dist/harness/events.d.ts.map +1 -0
- package/dist/harness/events.js +370 -0
- package/dist/harness/events.js.map +1 -0
- package/dist/harness/generation.d.ts +54 -0
- package/dist/harness/generation.d.ts.map +1 -0
- package/dist/harness/generation.js +514 -0
- package/dist/harness/generation.js.map +1 -0
- package/dist/harness/harness.d.ts +17 -0
- package/dist/harness/harness.d.ts.map +1 -0
- package/dist/harness/harness.js +332 -0
- package/dist/harness/harness.js.map +1 -0
- package/dist/harness/inbox.d.ts +59 -0
- package/dist/harness/inbox.d.ts.map +1 -0
- package/dist/harness/inbox.js +97 -0
- package/dist/harness/inbox.js.map +1 -0
- package/dist/harness/json.d.ts +10 -0
- package/dist/harness/json.d.ts.map +1 -0
- package/dist/harness/json.js +33 -0
- package/dist/harness/json.js.map +1 -0
- package/dist/harness/live.d.ts +142 -0
- package/dist/harness/live.d.ts.map +1 -0
- package/dist/harness/live.js +149 -0
- package/dist/harness/live.js.map +1 -0
- package/dist/harness/output.d.ts +69 -0
- package/dist/harness/output.d.ts.map +1 -0
- package/dist/harness/output.js +318 -0
- package/dist/harness/output.js.map +1 -0
- package/dist/harness/prompt.d.ts +27 -0
- package/dist/harness/prompt.d.ts.map +1 -0
- package/dist/harness/prompt.js +131 -0
- package/dist/harness/prompt.js.map +1 -0
- package/dist/harness/provider.d.ts +14 -0
- package/dist/harness/provider.d.ts.map +1 -0
- package/dist/harness/provider.js +30 -0
- package/dist/harness/provider.js.map +1 -0
- package/dist/harness/registry.d.ts +6 -0
- package/dist/harness/registry.d.ts.map +1 -0
- package/dist/harness/registry.js +101 -0
- package/dist/harness/registry.js.map +1 -0
- package/dist/harness/run.d.ts +17 -0
- package/dist/harness/run.d.ts.map +1 -0
- package/dist/harness/run.js +26 -0
- package/dist/harness/run.js.map +1 -0
- package/dist/harness/scheduler.d.ts +106 -0
- package/dist/harness/scheduler.d.ts.map +1 -0
- package/dist/harness/scheduler.js +1743 -0
- package/dist/harness/scheduler.js.map +1 -0
- package/dist/harness/submissions.d.ts +29 -0
- package/dist/harness/submissions.d.ts.map +1 -0
- package/dist/harness/submissions.js +175 -0
- package/dist/harness/submissions.js.map +1 -0
- package/dist/harness/task-graph.d.ts +49 -0
- package/dist/harness/task-graph.d.ts.map +1 -0
- package/dist/harness/task-graph.js +168 -0
- package/dist/harness/task-graph.js.map +1 -0
- package/dist/harness/tool.d.ts +76 -0
- package/dist/harness/tool.d.ts.map +1 -0
- package/dist/harness/tool.js +736 -0
- package/dist/harness/tool.js.map +1 -0
- package/dist/harness/types.d.ts +713 -0
- package/dist/harness/types.d.ts.map +1 -0
- package/dist/harness/types.js +2 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/harness/usage.d.ts +18 -0
- package/dist/harness/usage.d.ts.map +1 -0
- package/dist/harness/usage.js +57 -0
- package/dist/harness/usage.js.map +1 -0
- package/dist/harness/util.d.ts +19 -0
- package/dist/harness/util.d.ts.map +1 -0
- package/dist/harness/util.js +58 -0
- package/dist/harness/util.js.map +1 -0
- package/dist/harness/view.d.ts +47 -0
- package/dist/harness/view.d.ts.map +1 -0
- package/dist/harness/view.js +206 -0
- package/dist/harness/view.js.map +1 -0
- package/dist/ids.d.ts +6 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +9 -0
- package/dist/ids.js.map +1 -0
- package/dist/images/cloudflare.d.ts +4 -0
- package/dist/images/cloudflare.d.ts.map +1 -0
- package/dist/images/cloudflare.js +9 -0
- package/dist/images/cloudflare.js.map +1 -0
- package/dist/images/exif.d.ts +3 -0
- package/dist/images/exif.d.ts.map +1 -0
- package/dist/images/exif.js +71 -0
- package/dist/images/exif.js.map +1 -0
- package/dist/images/index.d.ts +18 -0
- package/dist/images/index.d.ts.map +1 -0
- package/dist/images/index.js +125 -0
- package/dist/images/index.js.map +1 -0
- package/dist/images/node-worker.d.ts +17 -0
- package/dist/images/node-worker.d.ts.map +1 -0
- package/dist/images/node-worker.js +16 -0
- package/dist/images/node-worker.js.map +1 -0
- package/dist/images/node.d.ts +21 -0
- package/dist/images/node.d.ts.map +1 -0
- package/dist/images/node.js +121 -0
- package/dist/images/node.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/session/forks.d.ts +10 -0
- package/dist/session/forks.d.ts.map +1 -0
- package/dist/session/forks.js +42 -0
- package/dist/session/forks.js.map +1 -0
- package/dist/session/observation.d.ts +42 -0
- package/dist/session/observation.d.ts.map +1 -0
- package/dist/session/observation.js +281 -0
- package/dist/session/observation.js.map +1 -0
- package/dist/session/session.d.ts +133 -0
- package/dist/session/session.d.ts.map +1 -0
- package/dist/session/session.js +811 -0
- package/dist/session/session.js.map +1 -0
- package/dist/session/transaction.d.ts +119 -0
- package/dist/session/transaction.d.ts.map +1 -0
- package/dist/session/transaction.js +795 -0
- package/dist/session/transaction.js.map +1 -0
- package/dist/storage/jsonl/index.d.ts +2 -0
- package/dist/storage/jsonl/index.d.ts.map +1 -0
- package/dist/storage/jsonl/index.js +2 -0
- package/dist/storage/jsonl/index.js.map +1 -0
- package/dist/storage/jsonl/node.d.ts +6 -0
- package/dist/storage/jsonl/node.d.ts.map +1 -0
- package/dist/storage/jsonl/node.js +8 -0
- package/dist/storage/jsonl/node.js.map +1 -0
- package/dist/storage/jsonl/storage.d.ts +143 -0
- package/dist/storage/jsonl/storage.d.ts.map +1 -0
- package/dist/storage/jsonl/storage.js +682 -0
- package/dist/storage/jsonl/storage.js.map +1 -0
- package/dist/storage/memory.d.ts +62 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +733 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/scan.d.ts +18 -0
- package/dist/storage/scan.d.ts.map +1 -0
- package/dist/storage/scan.js +29 -0
- package/dist/storage/scan.js.map +1 -0
- package/dist/storage/sqlite/cloudflare.d.ts +46 -0
- package/dist/storage/sqlite/cloudflare.d.ts.map +1 -0
- package/dist/storage/sqlite/cloudflare.js +106 -0
- package/dist/storage/sqlite/cloudflare.js.map +1 -0
- package/dist/storage/sqlite/database.d.ts +37 -0
- package/dist/storage/sqlite/database.d.ts.map +1 -0
- package/dist/storage/sqlite/database.js +2 -0
- package/dist/storage/sqlite/database.js.map +1 -0
- package/dist/storage/sqlite/index.d.ts +4 -0
- package/dist/storage/sqlite/index.d.ts.map +1 -0
- package/dist/storage/sqlite/index.js +3 -0
- package/dist/storage/sqlite/index.js.map +1 -0
- package/dist/storage/sqlite/migrations.d.ts +11 -0
- package/dist/storage/sqlite/migrations.d.ts.map +1 -0
- package/dist/storage/sqlite/migrations.js +111 -0
- package/dist/storage/sqlite/migrations.js.map +1 -0
- package/dist/storage/sqlite/node.d.ts +41 -0
- package/dist/storage/sqlite/node.d.ts.map +1 -0
- package/dist/storage/sqlite/node.js +179 -0
- package/dist/storage/sqlite/node.js.map +1 -0
- package/dist/storage/sqlite/storage.d.ts +65 -0
- package/dist/storage/sqlite/storage.d.ts.map +1 -0
- package/dist/storage/sqlite/storage.js +670 -0
- package/dist/storage/sqlite/storage.js.map +1 -0
- package/dist/tasks.d.ts +6 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +5 -0
- package/dist/tasks.js.map +1 -0
- package/dist/testing/assertions.d.ts +5 -0
- package/dist/testing/assertions.d.ts.map +1 -0
- package/dist/testing/assertions.js +25 -0
- package/dist/testing/assertions.js.map +1 -0
- package/dist/testing/env-conformance.d.ts +7 -0
- package/dist/testing/env-conformance.d.ts.map +1 -0
- package/dist/testing/env-conformance.js +431 -0
- package/dist/testing/env-conformance.js.map +1 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +6 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/runner.d.ts +12 -0
- package/dist/testing/runner.d.ts.map +1 -0
- package/dist/testing/runner.js +23 -0
- package/dist/testing/runner.js.map +1 -0
- package/dist/testing/storage-benchmark.d.ts +41 -0
- package/dist/testing/storage-benchmark.d.ts.map +1 -0
- package/dist/testing/storage-benchmark.js +350 -0
- package/dist/testing/storage-benchmark.js.map +1 -0
- package/dist/testing/storage-conformance.d.ts +4 -0
- package/dist/testing/storage-conformance.d.ts.map +1 -0
- package/dist/testing/storage-conformance.js +1213 -0
- package/dist/testing/storage-conformance.js.map +1 -0
- package/dist/testing/types.d.ts +37 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing/types.js +2 -0
- package/dist/testing/types.js.map +1 -0
- package/dist/tools/bash.d.ts +55 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +138 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/edit-diff.d.ts +89 -0
- package/dist/tools/edit-diff.d.ts.map +1 -0
- package/dist/tools/edit-diff.js +386 -0
- package/dist/tools/edit-diff.js.map +1 -0
- package/dist/tools/edit.d.ts +18 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +117 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/env.d.ts +5 -0
- package/dist/tools/env.d.ts.map +1 -0
- package/dist/tools/env.js +7 -0
- package/dist/tools/env.js.map +1 -0
- package/dist/tools/file-mutation-queue.d.ts +9 -0
- package/dist/tools/file-mutation-queue.d.ts.map +1 -0
- package/dist/tools/file-mutation-queue.js +52 -0
- package/dist/tools/file-mutation-queue.js.map +1 -0
- package/dist/tools/image-processor.d.ts +47 -0
- package/dist/tools/image-processor.d.ts.map +1 -0
- package/dist/tools/image-processor.js +30 -0
- package/dist/tools/image-processor.js.map +1 -0
- package/dist/tools/image.d.ts +20 -0
- package/dist/tools/image.d.ts.map +1 -0
- package/dist/tools/image.js +178 -0
- package/dist/tools/image.js.map +1 -0
- package/dist/tools/index.d.ts +46 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/path-utils.d.ts +5 -0
- package/dist/tools/path-utils.d.ts.map +1 -0
- package/dist/tools/path-utils.js +26 -0
- package/dist/tools/path-utils.js.map +1 -0
- package/dist/tools/read.d.ts +25 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +235 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/write.d.ts +10 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +31 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/truncate.d.ts +64 -0
- package/dist/truncate.d.ts.map +1 -0
- package/dist/truncate.js +154 -0
- package/dist/truncate.js.map +1 -0
- package/dist/types.d.ts +932 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +140 -5
|
@@ -0,0 +1,1213 @@
|
|
|
1
|
+
import { BACKGROUND_CONTEXT } from "@ny4n/chord/context";
|
|
2
|
+
import { idFromNumber } from "../ids.js";
|
|
3
|
+
import { ROOT_CONVERSATION_ID, } from "../types.js";
|
|
4
|
+
const context = BACKGROUND_CONTEXT;
|
|
5
|
+
async function createRoot(storage) {
|
|
6
|
+
await storage.commit([{ type: "conversation", value: { id: ROOT_CONVERSATION_ID } }], context);
|
|
7
|
+
return ROOT_CONVERSATION_ID;
|
|
8
|
+
}
|
|
9
|
+
function pendingTask(id, conversationId, phase = "ready") {
|
|
10
|
+
return {
|
|
11
|
+
id,
|
|
12
|
+
conversationId,
|
|
13
|
+
kind: "test.task",
|
|
14
|
+
version: 1,
|
|
15
|
+
input: { value: id },
|
|
16
|
+
state: { status: "pending", checkpoint: { phase } },
|
|
17
|
+
background: false,
|
|
18
|
+
abortRequested: false,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function entry(id, conversationId, kind = "message", extra = {}) {
|
|
22
|
+
return { id, conversationId, kind, ...extra };
|
|
23
|
+
}
|
|
24
|
+
function assertionFacade(assertions) {
|
|
25
|
+
return (actual) => ({
|
|
26
|
+
toBe: (expected) => assertions.strictEqual(actual, expected),
|
|
27
|
+
toBeDefined: () => assertions.ok(actual !== undefined, "Expected value to be defined"),
|
|
28
|
+
toBeGreaterThan: (expected) => assertions.greaterThan(actual, expected),
|
|
29
|
+
toBeUndefined: () => assertions.strictEqual(actual, undefined),
|
|
30
|
+
toEqual: (expected) => assertions.deepEqual(actual, expected),
|
|
31
|
+
toHaveLength: (expected) => assertions.strictEqual(actual.length, expected),
|
|
32
|
+
toMatchObject: (expected) => assertions.partialDeepEqual(actual, expected),
|
|
33
|
+
rejects: {
|
|
34
|
+
toThrow: (messageIncludes) => assertions.rejects(Promise.resolve(actual), messageIncludes),
|
|
35
|
+
},
|
|
36
|
+
resolves: {
|
|
37
|
+
toBe: async (expected) => assertions.strictEqual(await Promise.resolve(actual), expected),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function createCase(options, name, test) {
|
|
42
|
+
return { name, run: () => options.withStorage(test) };
|
|
43
|
+
}
|
|
44
|
+
/** Creates runner-independent cases. `withStorage` must call and await its callback exactly once per case. */
|
|
45
|
+
export function createStorageConformance(options) {
|
|
46
|
+
const expect = assertionFacade(options.assertions);
|
|
47
|
+
return [
|
|
48
|
+
createCase(options, "reserves ID 1 for the immutable root conversation", async (storage) => {
|
|
49
|
+
expect(await storage.mintId()).toBe(2);
|
|
50
|
+
await expect(createRoot(storage)).resolves.toBe(ROOT_CONVERSATION_ID);
|
|
51
|
+
expect(await storage.conversation(ROOT_CONVERSATION_ID, context)).toEqual({ id: ROOT_CONVERSATION_ID });
|
|
52
|
+
await expect(storage.commit([{ type: "conversation", value: { id: ROOT_CONVERSATION_ID } }], context)).rejects.toThrow(`ID ${ROOT_CONVERSATION_ID} already belongs to conversation`);
|
|
53
|
+
}),
|
|
54
|
+
createCase(options, "commits mixed table writes atomically and rolls all of them back on failure", async (storage) => {
|
|
55
|
+
const rootId = await createRoot(storage);
|
|
56
|
+
const entryId = await storage.mintId();
|
|
57
|
+
const taskId = await storage.mintId();
|
|
58
|
+
const submissionId = await storage.mintId();
|
|
59
|
+
const task = pendingTask(taskId, rootId);
|
|
60
|
+
const input = {
|
|
61
|
+
id: submissionId,
|
|
62
|
+
conversationId: rootId,
|
|
63
|
+
requestId: "request-1",
|
|
64
|
+
type: "input",
|
|
65
|
+
status: "placed",
|
|
66
|
+
entry: entryId,
|
|
67
|
+
};
|
|
68
|
+
const initialSeq = await storage.commit([
|
|
69
|
+
{ type: "entry", value: entry(entryId, rootId, "user", { data: { text: "hello" } }) },
|
|
70
|
+
{ type: "task", value: task },
|
|
71
|
+
{ type: "submission", value: input },
|
|
72
|
+
], context);
|
|
73
|
+
expect(await storage.entry(entryId, context)).toEqual({
|
|
74
|
+
entry: entry(entryId, rootId, "user", { data: { text: "hello" } }),
|
|
75
|
+
commitSeq: initialSeq,
|
|
76
|
+
});
|
|
77
|
+
expect(await storage.task(taskId, context)).toEqual(task);
|
|
78
|
+
expect(await storage.submission(submissionId, context)).toEqual(input);
|
|
79
|
+
const transientEntryId = await storage.mintId();
|
|
80
|
+
const runningTask = {
|
|
81
|
+
...task,
|
|
82
|
+
state: { status: "running", checkpoint: { phase: "effect" } },
|
|
83
|
+
};
|
|
84
|
+
const doneInput = { ...input, status: "done", answer: transientEntryId };
|
|
85
|
+
await expect(storage.commit([
|
|
86
|
+
{ type: "task", value: runningTask },
|
|
87
|
+
{ type: "submission", value: doneInput },
|
|
88
|
+
{ type: "entry", value: entry(transientEntryId, rootId, "assistant") },
|
|
89
|
+
{ type: "conversation", value: { id: rootId } },
|
|
90
|
+
], context)).rejects.toThrow(`ID ${rootId} already belongs to conversation`);
|
|
91
|
+
expect(await storage.task(taskId, context)).toEqual(task);
|
|
92
|
+
expect(await storage.submission(submissionId, context)).toEqual(input);
|
|
93
|
+
expect(await storage.entry(transientEntryId, context)).toBeUndefined();
|
|
94
|
+
const afterRollbackSeq = await storage.commit([{ type: "entry", value: entry(await storage.mintId(), rootId, "after-rollback") }], context);
|
|
95
|
+
expect(afterRollbackSeq).toBeGreaterThan(initialSeq);
|
|
96
|
+
}),
|
|
97
|
+
createCase(options, "detaches retained writes and every returned record", async (storage) => {
|
|
98
|
+
const rootId = await createRoot(storage);
|
|
99
|
+
const entryId = await storage.mintId();
|
|
100
|
+
const taskId = await storage.mintId();
|
|
101
|
+
const submissionId = await storage.mintId();
|
|
102
|
+
const entryData = { nested: [1, 2] };
|
|
103
|
+
const checkpoint = { phase: "ready", nested: { count: 1 } };
|
|
104
|
+
const detail = { codes: ["initial"] };
|
|
105
|
+
const storedEntry = entry(entryId, rootId, "note", { data: entryData });
|
|
106
|
+
const storedTask = {
|
|
107
|
+
...pendingTask(taskId, rootId),
|
|
108
|
+
state: { status: "pending", checkpoint },
|
|
109
|
+
};
|
|
110
|
+
const storedInput = {
|
|
111
|
+
id: submissionId,
|
|
112
|
+
conversationId: rootId,
|
|
113
|
+
type: "input",
|
|
114
|
+
status: "unanswered",
|
|
115
|
+
reason: "failed",
|
|
116
|
+
detail,
|
|
117
|
+
};
|
|
118
|
+
await storage.commit([
|
|
119
|
+
{ type: "entry", value: storedEntry },
|
|
120
|
+
{ type: "task", value: storedTask },
|
|
121
|
+
{ type: "submission", value: storedInput },
|
|
122
|
+
], context);
|
|
123
|
+
entryData.nested.push(3);
|
|
124
|
+
checkpoint.nested.count = 2;
|
|
125
|
+
detail.codes.push("mutated");
|
|
126
|
+
expect((await storage.entry(entryId, context))?.entry.data).toEqual({ nested: [1, 2] });
|
|
127
|
+
expect((await storage.task(taskId, context))?.state).toEqual({
|
|
128
|
+
status: "pending",
|
|
129
|
+
checkpoint: { phase: "ready", nested: { count: 1 } },
|
|
130
|
+
});
|
|
131
|
+
expect((await storage.submission(submissionId, context))?.detail).toEqual({ codes: ["initial"] });
|
|
132
|
+
const readEntry = (await storage.entry(entryId, context)).entry;
|
|
133
|
+
readEntry.data.nested.push(9);
|
|
134
|
+
const readTask = (await storage.task(taskId, context));
|
|
135
|
+
if (readTask.state.status !== "terminal") {
|
|
136
|
+
readTask.state.checkpoint.nested.count = 9;
|
|
137
|
+
}
|
|
138
|
+
const readInput = (await storage.submission(submissionId, context));
|
|
139
|
+
readInput.detail.codes.push("read mutation");
|
|
140
|
+
expect((await storage.entry(entryId, context))?.entry.data).toEqual({ nested: [1, 2] });
|
|
141
|
+
expect((await storage.task(taskId, context))?.state).toEqual({
|
|
142
|
+
status: "pending",
|
|
143
|
+
checkpoint: { phase: "ready", nested: { count: 1 } },
|
|
144
|
+
});
|
|
145
|
+
expect((await storage.submission(submissionId, context))?.detail).toEqual({ codes: ["initial"] });
|
|
146
|
+
}),
|
|
147
|
+
createCase(options, "detaches prototype-like JSON keys without changing object prototypes", async (storage) => {
|
|
148
|
+
const rootId = await createRoot(storage);
|
|
149
|
+
const entryId = await storage.mintId();
|
|
150
|
+
const data = JSON.parse('{"__proto__":{"polluted":false},"constructor":{"label":"stored"},"toString":"value"}');
|
|
151
|
+
await storage.commit([{ type: "entry", value: entry(entryId, rootId, "note", { data }) }], context);
|
|
152
|
+
Reflect.get(data, "__proto__").polluted = true;
|
|
153
|
+
Reflect.get(data, "constructor").label = "mutated";
|
|
154
|
+
const firstRead = (await storage.entry(entryId, context)).entry.data;
|
|
155
|
+
expect(Object.getPrototypeOf(firstRead)).toBe(Object.prototype);
|
|
156
|
+
expect(Object.hasOwn(firstRead, "__proto__")).toBe(true);
|
|
157
|
+
expect(Reflect.get(firstRead, "__proto__")).toEqual({ polluted: false });
|
|
158
|
+
expect(Reflect.get(firstRead, "constructor")).toEqual({ label: "stored" });
|
|
159
|
+
expect(Reflect.get(firstRead, "toString")).toBe("value");
|
|
160
|
+
expect({}.polluted).toBeUndefined();
|
|
161
|
+
Reflect.get(firstRead, "__proto__").polluted = true;
|
|
162
|
+
const secondRead = (await storage.entry(entryId, context)).entry.data;
|
|
163
|
+
expect(Reflect.get(secondRead, "__proto__")).toEqual({ polluted: false });
|
|
164
|
+
expect(Reflect.get(secondRead, "constructor")).toEqual({ label: "stored" });
|
|
165
|
+
expect(Reflect.get(secondRead, "toString")).toBe("value");
|
|
166
|
+
}),
|
|
167
|
+
createCase(options, "indexes entries committed out of ID order", async (storage) => {
|
|
168
|
+
const rootId = await createRoot(storage);
|
|
169
|
+
await storage.commit([
|
|
170
|
+
{ type: "entry", value: entry(idFromNumber(30), rootId) },
|
|
171
|
+
{ type: "entry", value: entry(idFromNumber(10), rootId) },
|
|
172
|
+
{
|
|
173
|
+
type: "entry",
|
|
174
|
+
value: entry(idFromNumber(20), rootId, "marker", {
|
|
175
|
+
head: idFromNumber(10),
|
|
176
|
+
}),
|
|
177
|
+
},
|
|
178
|
+
], context);
|
|
179
|
+
expect((await storage.scanEntries({ conversationId: rootId }, 10, undefined, context)).items.map(({ id }) => id)).toEqual([30, 20, 10]);
|
|
180
|
+
expect((await storage.findLatestHeadMarker(rootId, undefined, context))?.id).toBe(20);
|
|
181
|
+
}),
|
|
182
|
+
createCase(options, "continues an entry cursor below its last item after a newer commit", async (storage) => {
|
|
183
|
+
const rootId = await createRoot(storage);
|
|
184
|
+
const oldestId = await storage.mintId();
|
|
185
|
+
const middleId = await storage.mintId();
|
|
186
|
+
const newestId = await storage.mintId();
|
|
187
|
+
await storage.commit([
|
|
188
|
+
{ type: "entry", value: entry(oldestId, rootId) },
|
|
189
|
+
{ type: "entry", value: entry(middleId, rootId) },
|
|
190
|
+
{ type: "entry", value: entry(newestId, rootId) },
|
|
191
|
+
], context);
|
|
192
|
+
const first = await storage.scanEntries({ conversationId: rootId }, 2, undefined, context);
|
|
193
|
+
expect(first.items.map(({ id }) => id)).toEqual([newestId, middleId]);
|
|
194
|
+
const appendedId = await storage.mintId();
|
|
195
|
+
await storage.commit([{ type: "entry", value: entry(appendedId, rootId) }], context);
|
|
196
|
+
const second = await storage.scanEntries({ conversationId: rootId }, 2, first.next, context);
|
|
197
|
+
expect(second.items.map(({ id }) => id)).toEqual([oldestId]);
|
|
198
|
+
expect(second.next).toBeUndefined();
|
|
199
|
+
}),
|
|
200
|
+
createCase(options, "paginates conversations by opaque cursor in ascending ID order", async (storage) => {
|
|
201
|
+
const rootId = await createRoot(storage);
|
|
202
|
+
const secondId = await storage.mintId();
|
|
203
|
+
const thirdId = await storage.mintId();
|
|
204
|
+
await storage.commit([
|
|
205
|
+
{ type: "conversation", value: { id: thirdId } },
|
|
206
|
+
{ type: "conversation", value: { id: secondId } },
|
|
207
|
+
], context);
|
|
208
|
+
const first = await storage.scanConversations({}, 2, undefined, context);
|
|
209
|
+
expect(first.items.map(({ id }) => id)).toEqual([rootId, secondId]);
|
|
210
|
+
expect(first.next).toBeDefined();
|
|
211
|
+
const roundTrippedCursor = JSON.parse(JSON.stringify(first.next));
|
|
212
|
+
const second = await storage.scanConversations({}, 2, roundTrippedCursor, context);
|
|
213
|
+
expect(second.items.map(({ id }) => id)).toEqual([thirdId]);
|
|
214
|
+
expect(second.next).toBeUndefined();
|
|
215
|
+
}),
|
|
216
|
+
createCase(options, "filters and pages conversations by durable owner edges", async (storage) => {
|
|
217
|
+
const rootId = await createRoot(storage);
|
|
218
|
+
const otherOwnerId = await storage.mintId();
|
|
219
|
+
const firstTaskId = await storage.mintId();
|
|
220
|
+
const secondTaskId = await storage.mintId();
|
|
221
|
+
const firstId = await storage.mintId();
|
|
222
|
+
const secondId = await storage.mintId();
|
|
223
|
+
const thirdId = await storage.mintId();
|
|
224
|
+
await storage.commit([
|
|
225
|
+
{ type: "conversation", value: { id: otherOwnerId } },
|
|
226
|
+
{
|
|
227
|
+
type: "conversation",
|
|
228
|
+
value: { id: firstId, owner: { conversationId: rootId, taskId: firstTaskId } },
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
type: "conversation",
|
|
232
|
+
value: { id: secondId, owner: { conversationId: rootId, taskId: secondTaskId } },
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
type: "conversation",
|
|
236
|
+
value: { id: thirdId, owner: { conversationId: otherOwnerId, taskId: firstTaskId } },
|
|
237
|
+
},
|
|
238
|
+
], context);
|
|
239
|
+
const first = await storage.scanConversations({ ownerConversationId: rootId }, 1, undefined, context);
|
|
240
|
+
expect(first.items.map(({ id }) => id)).toEqual([firstId]);
|
|
241
|
+
expect(first.next).toBeDefined();
|
|
242
|
+
const second = await storage.scanConversations({ ownerConversationId: rootId }, 1, first.next, context);
|
|
243
|
+
expect(second.items.map(({ id }) => id)).toEqual([secondId]);
|
|
244
|
+
expect(second.next).toBeUndefined();
|
|
245
|
+
expect((await storage.scanConversations({ ownerTaskId: firstTaskId }, 10, undefined, context)).items.map(({ id }) => id)).toEqual([firstId, thirdId]);
|
|
246
|
+
expect((await storage.scanConversations({ ownerConversationId: rootId, ownerTaskId: firstTaskId }, 10, undefined, context)).items.map(({ id }) => id)).toEqual([firstId]);
|
|
247
|
+
}),
|
|
248
|
+
createCase(options, "scans deep fork history newest-first through every ancestor cap", async (storage) => {
|
|
249
|
+
const rootId = await createRoot(storage);
|
|
250
|
+
const rootFirst = await storage.mintId();
|
|
251
|
+
const rootForkPoint = await storage.mintId();
|
|
252
|
+
const rootExcludedSameCommit = await storage.mintId();
|
|
253
|
+
const rootEntriesSeq = await storage.commit([
|
|
254
|
+
{ type: "entry", value: entry(rootFirst, rootId) },
|
|
255
|
+
{
|
|
256
|
+
type: "entry",
|
|
257
|
+
value: entry(rootForkPoint, rootId, "marker", { head: rootFirst }),
|
|
258
|
+
},
|
|
259
|
+
{ type: "entry", value: entry(rootExcludedSameCommit, rootId) },
|
|
260
|
+
], context);
|
|
261
|
+
const childId = await storage.mintId();
|
|
262
|
+
await storage.commit([
|
|
263
|
+
{
|
|
264
|
+
type: "conversation",
|
|
265
|
+
value: { id: childId, parent: { conversationId: rootId, at: rootForkPoint } },
|
|
266
|
+
},
|
|
267
|
+
], context);
|
|
268
|
+
const childForkPoint = await storage.mintId();
|
|
269
|
+
const childExcluded = await storage.mintId();
|
|
270
|
+
await storage.commit([
|
|
271
|
+
{ type: "entry", value: entry(childForkPoint, childId, "note") },
|
|
272
|
+
{ type: "entry", value: entry(childExcluded, childId) },
|
|
273
|
+
], context);
|
|
274
|
+
const rootExcludedLater = await storage.mintId();
|
|
275
|
+
await storage.commit([{ type: "entry", value: entry(rootExcludedLater, rootId) }], context);
|
|
276
|
+
const grandchildId = await storage.mintId();
|
|
277
|
+
await storage.commit([
|
|
278
|
+
{
|
|
279
|
+
type: "conversation",
|
|
280
|
+
value: { id: grandchildId, parent: { conversationId: childId, at: childForkPoint } },
|
|
281
|
+
},
|
|
282
|
+
], context);
|
|
283
|
+
const grandchildHead = await storage.mintId();
|
|
284
|
+
const grandchildTail = await storage.mintId();
|
|
285
|
+
const grandchildEntriesSeq = await storage.commit([
|
|
286
|
+
{
|
|
287
|
+
type: "entry",
|
|
288
|
+
value: entry(grandchildHead, grandchildId, "marker", { head: grandchildHead }),
|
|
289
|
+
},
|
|
290
|
+
{ type: "entry", value: entry(grandchildTail, grandchildId) },
|
|
291
|
+
], context);
|
|
292
|
+
const childExcludedLater = await storage.mintId();
|
|
293
|
+
await storage.commit([{ type: "entry", value: entry(childExcludedLater, childId) }], context);
|
|
294
|
+
const first = await storage.scanEntries({ conversationId: grandchildId }, 2, undefined, context);
|
|
295
|
+
expect(first.items.map(({ id }) => id)).toEqual([grandchildTail, grandchildHead]);
|
|
296
|
+
const second = await storage.scanEntries({ conversationId: grandchildId }, 2, first.next, context);
|
|
297
|
+
expect(second.items.map(({ id }) => id)).toEqual([childForkPoint, rootForkPoint]);
|
|
298
|
+
const third = await storage.scanEntries({ conversationId: grandchildId }, 2, second.next, context);
|
|
299
|
+
expect(third.items.map(({ id }) => id)).toEqual([rootFirst]);
|
|
300
|
+
expect(third.next).toBeUndefined();
|
|
301
|
+
// Oldest first: the root's segment, then each fork's, each capped at the next fork point.
|
|
302
|
+
const ascending = { conversationId: grandchildId, order: "ascending" };
|
|
303
|
+
const firstUp = await storage.scanEntries(ascending, 2, undefined, context);
|
|
304
|
+
expect(firstUp.items.map(({ id }) => id)).toEqual([rootFirst, rootForkPoint]);
|
|
305
|
+
const secondUp = await storage.scanEntries(ascending, 2, firstUp.next, context);
|
|
306
|
+
expect(secondUp.items.map(({ id }) => id)).toEqual([childForkPoint, grandchildHead]);
|
|
307
|
+
const thirdUp = await storage.scanEntries({ conversationId: grandchildId }, 2, secondUp.next, context);
|
|
308
|
+
expect(thirdUp.items.map(({ id }) => id)).toEqual([grandchildTail]);
|
|
309
|
+
expect(thirdUp.next).toBeUndefined();
|
|
310
|
+
expect((await storage.scanEntries({ ...ascending, minEntryId: rootForkPoint, maxEntryId: childForkPoint }, 10, undefined, context)).items.map(({ id }) => id)).toEqual([rootForkPoint, childForkPoint]);
|
|
311
|
+
await expect(storage.scanEntries({ conversationId: grandchildId, order: "descending" }, 2, firstUp.next, context)).rejects.toThrow("cursor");
|
|
312
|
+
const currentMarker = await storage.findLatestHeadMarker(grandchildId, undefined, context);
|
|
313
|
+
expect(currentMarker?.id).toBe(grandchildHead);
|
|
314
|
+
expect(currentMarker?.head).toBe(grandchildHead);
|
|
315
|
+
const historicalMarker = await storage.findLatestHeadMarker(grandchildId, childForkPoint, context);
|
|
316
|
+
expect(historicalMarker?.id).toBe(rootForkPoint);
|
|
317
|
+
expect(historicalMarker?.head).toBe(rootFirst);
|
|
318
|
+
expect(await storage.findLatestHeadMarker(grandchildId, rootFirst, context)).toBeUndefined();
|
|
319
|
+
const activeFirst = await storage.scanEntries({ conversationId: grandchildId, minEntryId: currentMarker?.head }, 1, undefined, context);
|
|
320
|
+
expect(activeFirst.items.map(({ id }) => id)).toEqual([grandchildTail]);
|
|
321
|
+
expect(activeFirst.next).toBeDefined();
|
|
322
|
+
const activeSecond = await storage.scanEntries({ conversationId: grandchildId, minEntryId: currentMarker?.head }, 1, activeFirst.next, context);
|
|
323
|
+
expect(activeSecond.items.map(({ id }) => id)).toEqual([grandchildHead]);
|
|
324
|
+
expect(activeSecond.next).toBeUndefined();
|
|
325
|
+
expect((await storage.scanEntries({
|
|
326
|
+
conversationId: grandchildId,
|
|
327
|
+
minEntryId: historicalMarker?.head,
|
|
328
|
+
maxEntryId: childForkPoint,
|
|
329
|
+
}, 10, undefined, context)).items.map(({ id }) => id)).toEqual([childForkPoint, rootForkPoint, rootFirst]);
|
|
330
|
+
expect(await storage.entry(rootFirst, context)).toEqual({
|
|
331
|
+
entry: entry(rootFirst, rootId),
|
|
332
|
+
commitSeq: rootEntriesSeq,
|
|
333
|
+
});
|
|
334
|
+
expect((await storage.entry(rootForkPoint, context))?.commitSeq).toBe(rootEntriesSeq);
|
|
335
|
+
expect((await storage.entry(grandchildHead, context))?.commitSeq).toBe(grandchildEntriesSeq);
|
|
336
|
+
expect((await storage.entry(grandchildTail, context))?.commitSeq).toBe(grandchildEntriesSeq);
|
|
337
|
+
expect(await storage.entry(idFromNumber(999_999), context)).toBeUndefined();
|
|
338
|
+
expect(await storage.entry(grandchildId, rootFirst, context)).toEqual({
|
|
339
|
+
entry: entry(rootFirst, rootId),
|
|
340
|
+
commitSeq: rootEntriesSeq,
|
|
341
|
+
});
|
|
342
|
+
expect((await storage.entry(grandchildId, childForkPoint, context))?.entry.conversationId).toBe(childId);
|
|
343
|
+
expect((await storage.entry(grandchildId, grandchildTail, context))?.commitSeq).toBe(grandchildEntriesSeq);
|
|
344
|
+
expect(await storage.entry(grandchildId, rootExcludedSameCommit, context)).toBeUndefined();
|
|
345
|
+
expect(await storage.entry(grandchildId, rootExcludedLater, context)).toBeUndefined();
|
|
346
|
+
expect(await storage.entry(grandchildId, childExcluded, context)).toBeUndefined();
|
|
347
|
+
expect(await storage.entry(grandchildId, childExcludedLater, context)).toBeUndefined();
|
|
348
|
+
expect(await storage.entry(rootId, grandchildHead, context)).toBeUndefined();
|
|
349
|
+
expect(await storage.entry(grandchildId, idFromNumber(999_999), context)).toBeUndefined();
|
|
350
|
+
await expect(storage.entry(idFromNumber(999_999), rootFirst, context)).rejects.toThrow("Unknown conversation");
|
|
351
|
+
await expect(storage.scanEntries({ conversationId: idFromNumber(999_999) }, 10, undefined, context)).rejects.toThrow("Unknown conversation");
|
|
352
|
+
}),
|
|
353
|
+
createCase(options, "scans tables in either ID order and continues a cursor in its order", async (storage) => {
|
|
354
|
+
const rootId = await createRoot(storage);
|
|
355
|
+
const conversationIds = [rootId];
|
|
356
|
+
const taskIds = [];
|
|
357
|
+
const submissionIds = [];
|
|
358
|
+
for (let index = 0; index < 3; index++) {
|
|
359
|
+
const conversationId = await storage.mintId();
|
|
360
|
+
const taskId = await storage.mintId();
|
|
361
|
+
const submissionId = await storage.mintId();
|
|
362
|
+
await storage.commit([
|
|
363
|
+
{ type: "conversation", value: { id: conversationId } },
|
|
364
|
+
{ type: "task", value: pendingTask(taskId, rootId) },
|
|
365
|
+
{
|
|
366
|
+
type: "submission",
|
|
367
|
+
value: { id: submissionId, conversationId: rootId, type: "input", status: "queued" },
|
|
368
|
+
},
|
|
369
|
+
], context);
|
|
370
|
+
conversationIds.push(conversationId);
|
|
371
|
+
taskIds.push(taskId);
|
|
372
|
+
submissionIds.push(submissionId);
|
|
373
|
+
}
|
|
374
|
+
const scans = [
|
|
375
|
+
[(order, cursor) => storage.scanConversations({ order }, 2, cursor, context), conversationIds],
|
|
376
|
+
[(order, cursor) => storage.scanTasks({ order }, 2, cursor, context), taskIds],
|
|
377
|
+
[(order, cursor) => storage.scanSubmissions({ order }, 2, cursor, context), submissionIds],
|
|
378
|
+
];
|
|
379
|
+
const all = async (scan, order) => {
|
|
380
|
+
const found = [];
|
|
381
|
+
let page = await scan(order, undefined);
|
|
382
|
+
found.push(...page.items.map(({ id }) => id));
|
|
383
|
+
while (page.next !== undefined) {
|
|
384
|
+
// A cursor carries its order; the query may omit it.
|
|
385
|
+
page = await scan(undefined, JSON.parse(JSON.stringify(page.next)));
|
|
386
|
+
found.push(...page.items.map(({ id }) => id));
|
|
387
|
+
}
|
|
388
|
+
return found;
|
|
389
|
+
};
|
|
390
|
+
for (const [scan, ids] of scans) {
|
|
391
|
+
const reversed = [...ids].reverse();
|
|
392
|
+
expect(await all(scan, undefined)).toEqual(ids);
|
|
393
|
+
expect(await all(scan, "ascending")).toEqual(ids);
|
|
394
|
+
expect(await all(scan, "descending")).toEqual(reversed);
|
|
395
|
+
const descending = await scan("descending", undefined);
|
|
396
|
+
expect((await scan("descending", descending.next)).items.map(({ id }) => id)).toEqual(reversed.slice(2, 4));
|
|
397
|
+
await expect(scan("ascending", descending.next)).rejects.toThrow("cursor");
|
|
398
|
+
}
|
|
399
|
+
}),
|
|
400
|
+
createCase(options, "replaces complete task records and pages filtered task scans", async (storage) => {
|
|
401
|
+
const rootId = await createRoot(storage);
|
|
402
|
+
const firstId = await storage.mintId();
|
|
403
|
+
const secondId = await storage.mintId();
|
|
404
|
+
const thirdId = await storage.mintId();
|
|
405
|
+
const first = { ...pendingTask(firstId, rootId), memos: { winner: "first" } };
|
|
406
|
+
const second = { ...pendingTask(secondId, rootId), background: true };
|
|
407
|
+
const third = { ...pendingTask(thirdId, rootId), abortRequested: true };
|
|
408
|
+
await storage.commit([
|
|
409
|
+
{ type: "task", value: first },
|
|
410
|
+
{ type: "task", value: second },
|
|
411
|
+
{ type: "task", value: third },
|
|
412
|
+
], context);
|
|
413
|
+
const running = {
|
|
414
|
+
...first,
|
|
415
|
+
state: { status: "running", checkpoint: { phase: "effect", attempt: 1 } },
|
|
416
|
+
abortRequested: true,
|
|
417
|
+
};
|
|
418
|
+
await storage.commit([{ type: "task", value: running }], context);
|
|
419
|
+
expect(await storage.task(firstId, context)).toEqual(running);
|
|
420
|
+
const terminal = {
|
|
421
|
+
id: firstId,
|
|
422
|
+
conversationId: rootId,
|
|
423
|
+
kind: first.kind,
|
|
424
|
+
version: first.version,
|
|
425
|
+
input: first.input,
|
|
426
|
+
state: { status: "terminal", outcome: { status: "completed", result: { entryId: 99 } } },
|
|
427
|
+
background: false,
|
|
428
|
+
abortRequested: true,
|
|
429
|
+
};
|
|
430
|
+
await storage.commit([{ type: "task", value: terminal }], context);
|
|
431
|
+
expect(await storage.task(firstId, context)).toEqual(terminal);
|
|
432
|
+
const pendingPage = await storage.scanTasks({ status: "pending" }, 1, undefined, context);
|
|
433
|
+
expect(pendingPage.items.map(({ id }) => id)).toEqual([secondId]);
|
|
434
|
+
expect(pendingPage.next).toBeDefined();
|
|
435
|
+
expect((await storage.scanTasks({ status: "pending" }, 1, pendingPage.next, context)).items.map(({ id }) => id)).toEqual([thirdId]);
|
|
436
|
+
expect((await storage.scanTasks({ status: "terminal", abortRequested: true }, 10, undefined, context)).items).toEqual([terminal]);
|
|
437
|
+
expect((await storage.scanTasks({ background: true }, 10, undefined, context)).items.map(({ id }) => id)).toEqual([secondId]);
|
|
438
|
+
}),
|
|
439
|
+
createCase(options, "stores owners and scans waiting and completing tasks by status", async (storage) => {
|
|
440
|
+
const rootId = await createRoot(storage);
|
|
441
|
+
const ownerId = await storage.mintId();
|
|
442
|
+
const waitingId = await storage.mintId();
|
|
443
|
+
const completingId = await storage.mintId();
|
|
444
|
+
const owner = pendingTask(ownerId, rootId);
|
|
445
|
+
const waiting = {
|
|
446
|
+
...pendingTask(waitingId, rootId),
|
|
447
|
+
owner: ownerId,
|
|
448
|
+
state: { status: "waiting", checkpoint: { phase: "next" }, on: [ownerId], policy: "allSettled" },
|
|
449
|
+
memos: { kept: true },
|
|
450
|
+
};
|
|
451
|
+
const { state: _state, ...base } = pendingTask(completingId, rootId);
|
|
452
|
+
const completing = {
|
|
453
|
+
...base,
|
|
454
|
+
owner: ownerId,
|
|
455
|
+
state: { status: "completing", outcome: { status: "failed", error: { message: "held" } } },
|
|
456
|
+
};
|
|
457
|
+
const writes = [owner, waiting, completing].map((value) => ({ type: "task", value }));
|
|
458
|
+
await storage.commit(writes, context);
|
|
459
|
+
expect(await storage.task(waitingId, context)).toEqual(waiting);
|
|
460
|
+
expect(await storage.task(completingId, context)).toEqual(completing);
|
|
461
|
+
const scan = async (status) => (await storage.scanTasks({ status }, 10, undefined, context)).items;
|
|
462
|
+
expect(await scan("waiting")).toEqual([waiting]);
|
|
463
|
+
expect(await scan("completing")).toEqual([completing]);
|
|
464
|
+
expect((await scan("pending")).map(({ id }) => id)).toEqual([ownerId]);
|
|
465
|
+
const terminal = {
|
|
466
|
+
...completing,
|
|
467
|
+
state: { status: "terminal", outcome: completing.state.outcome },
|
|
468
|
+
};
|
|
469
|
+
await storage.commit([{ type: "task", value: terminal }], context);
|
|
470
|
+
expect(await scan("completing")).toEqual([]);
|
|
471
|
+
expect(await scan("terminal")).toEqual([terminal]);
|
|
472
|
+
}),
|
|
473
|
+
createCase(options, "indexes request IDs per conversation and replaces complete submission records", async (storage) => {
|
|
474
|
+
const rootId = await createRoot(storage);
|
|
475
|
+
const secondConversationId = await storage.mintId();
|
|
476
|
+
await storage.commit([{ type: "conversation", value: { id: secondConversationId } }], context);
|
|
477
|
+
const firstId = await storage.mintId();
|
|
478
|
+
const secondId = await storage.mintId();
|
|
479
|
+
const otherConversationId = await storage.mintId();
|
|
480
|
+
const first = {
|
|
481
|
+
id: firstId,
|
|
482
|
+
conversationId: rootId,
|
|
483
|
+
requestId: "same",
|
|
484
|
+
type: "input",
|
|
485
|
+
status: "queued",
|
|
486
|
+
};
|
|
487
|
+
const second = {
|
|
488
|
+
id: secondId,
|
|
489
|
+
conversationId: rootId,
|
|
490
|
+
requestId: "other",
|
|
491
|
+
type: "input",
|
|
492
|
+
status: "queued",
|
|
493
|
+
};
|
|
494
|
+
const otherConversation = {
|
|
495
|
+
id: otherConversationId,
|
|
496
|
+
conversationId: secondConversationId,
|
|
497
|
+
requestId: "same",
|
|
498
|
+
type: "input",
|
|
499
|
+
status: "queued",
|
|
500
|
+
};
|
|
501
|
+
await storage.commit([
|
|
502
|
+
{ type: "submission", value: first },
|
|
503
|
+
{ type: "submission", value: second },
|
|
504
|
+
{ type: "submission", value: otherConversation },
|
|
505
|
+
], context);
|
|
506
|
+
expect(await storage.submissionByRequest(rootId, "same", context)).toEqual(first);
|
|
507
|
+
expect(await storage.submissionByRequest(secondConversationId, "same", context)).toEqual(otherConversation);
|
|
508
|
+
const placedSecond = {
|
|
509
|
+
...second,
|
|
510
|
+
status: "placed",
|
|
511
|
+
entry: await storage.mintId(),
|
|
512
|
+
};
|
|
513
|
+
await storage.commit([{ type: "submission", value: placedSecond }], context);
|
|
514
|
+
expect(await storage.submission(secondId, context)).toEqual(placedSecond);
|
|
515
|
+
expect(await storage.submissionByRequest(rootId, "other", context)).toEqual(placedSecond);
|
|
516
|
+
const ids = async (query) => {
|
|
517
|
+
const found = [];
|
|
518
|
+
let cursor;
|
|
519
|
+
do {
|
|
520
|
+
const page = await storage.scanSubmissions(query, 1, cursor, context);
|
|
521
|
+
found.push(...page.items.map(({ id }) => id));
|
|
522
|
+
cursor = page.next;
|
|
523
|
+
} while (cursor !== undefined);
|
|
524
|
+
return found;
|
|
525
|
+
};
|
|
526
|
+
expect(await ids({})).toEqual([firstId, secondId, otherConversationId]);
|
|
527
|
+
expect(await ids({ conversationId: rootId })).toEqual([firstId, secondId]);
|
|
528
|
+
// A status change moves the record between status scans.
|
|
529
|
+
expect(await ids({ status: "queued" })).toEqual([firstId, otherConversationId]);
|
|
530
|
+
expect(await ids({ status: "placed" })).toEqual([secondId]);
|
|
531
|
+
expect(await ids({ conversationId: secondConversationId, status: "queued" })).toEqual([
|
|
532
|
+
otherConversationId,
|
|
533
|
+
]);
|
|
534
|
+
expect(await ids({ conversationId: secondConversationId, status: "placed" })).toEqual([]);
|
|
535
|
+
expect((await storage.scanSubmissions({ status: "placed" }, 10, undefined, context)).items).toEqual([
|
|
536
|
+
placedSecond,
|
|
537
|
+
]);
|
|
538
|
+
}),
|
|
539
|
+
createCase(options, "stores passive write submissions without input-only lifecycle states", async (storage) => {
|
|
540
|
+
const rootId = await createRoot(storage);
|
|
541
|
+
const doneId = await storage.mintId();
|
|
542
|
+
const failedId = await storage.mintId();
|
|
543
|
+
const queuedDone = {
|
|
544
|
+
id: doneId,
|
|
545
|
+
conversationId: rootId,
|
|
546
|
+
requestId: "passive-done",
|
|
547
|
+
type: "write",
|
|
548
|
+
status: "queued",
|
|
549
|
+
};
|
|
550
|
+
const queuedFailed = {
|
|
551
|
+
id: failedId,
|
|
552
|
+
conversationId: rootId,
|
|
553
|
+
requestId: "passive-failed",
|
|
554
|
+
type: "write",
|
|
555
|
+
status: "queued",
|
|
556
|
+
};
|
|
557
|
+
await storage.commit([
|
|
558
|
+
{ type: "submission", value: queuedDone },
|
|
559
|
+
{ type: "submission", value: queuedFailed },
|
|
560
|
+
], context);
|
|
561
|
+
const done = {
|
|
562
|
+
...queuedDone,
|
|
563
|
+
status: "done",
|
|
564
|
+
entry: await storage.mintId(),
|
|
565
|
+
};
|
|
566
|
+
const unanswered = {
|
|
567
|
+
...queuedFailed,
|
|
568
|
+
status: "unanswered",
|
|
569
|
+
reason: "closed",
|
|
570
|
+
detail: { retryable: false },
|
|
571
|
+
};
|
|
572
|
+
await storage.commit([
|
|
573
|
+
{ type: "submission", value: done },
|
|
574
|
+
{ type: "submission", value: unanswered },
|
|
575
|
+
], context);
|
|
576
|
+
expect(await storage.submission(doneId, context)).toEqual(done);
|
|
577
|
+
expect(await storage.submissionByRequest(rootId, "passive-done", context)).toEqual(done);
|
|
578
|
+
expect(await storage.submission(failedId, context)).toEqual(unanswered);
|
|
579
|
+
expect(await storage.submissionByRequest(rootId, "passive-failed", context)).toEqual(unanswered);
|
|
580
|
+
}),
|
|
581
|
+
createCase(options, "reconstructs rewindable documents and preserves half-open incarnations", async (storage) => {
|
|
582
|
+
const rootId = await createRoot(storage);
|
|
583
|
+
const firstId = await storage.mintId();
|
|
584
|
+
const firstRecord = {
|
|
585
|
+
id: firstId,
|
|
586
|
+
kind: "conversation.notes",
|
|
587
|
+
scope: { kind: "conversation", conversationId: rootId },
|
|
588
|
+
history: "rewindable",
|
|
589
|
+
fork: "asOf",
|
|
590
|
+
};
|
|
591
|
+
const initial = { items: ["a"], nested: { count: 1 } };
|
|
592
|
+
const createdAt = await storage.commit([
|
|
593
|
+
{
|
|
594
|
+
type: "document.create",
|
|
595
|
+
record: firstRecord,
|
|
596
|
+
content: { kind: "base", version: 1, value: initial },
|
|
597
|
+
},
|
|
598
|
+
], context);
|
|
599
|
+
const appended = ["b"];
|
|
600
|
+
const ops = [
|
|
601
|
+
["p", ["items"], 1, 0, appended],
|
|
602
|
+
["s", ["nested", "count"], 2],
|
|
603
|
+
];
|
|
604
|
+
const changedAt = await storage.commit([{ type: "document.change", id: firstId, content: { kind: "delta", version: 1, ops } }], context);
|
|
605
|
+
initial.items.push("caller mutation");
|
|
606
|
+
appended.push("caller mutation");
|
|
607
|
+
expect(await storage.document(firstId, createdAt, context)).toMatchObject({
|
|
608
|
+
version: 1,
|
|
609
|
+
value: { items: ["a"], nested: { count: 1 } },
|
|
610
|
+
deltasSinceBase: 0,
|
|
611
|
+
});
|
|
612
|
+
const changed = (await storage.document(firstId, changedAt, context));
|
|
613
|
+
expect(changed.value).toEqual({ items: ["a", "b"], nested: { count: 2 } });
|
|
614
|
+
expect(changed.deltasSinceBase).toBe(1);
|
|
615
|
+
changed.value.items.push("read mutation");
|
|
616
|
+
expect((await storage.document(firstId, "current", context))?.value).toEqual({
|
|
617
|
+
items: ["a", "b"],
|
|
618
|
+
nested: { count: 2 },
|
|
619
|
+
});
|
|
620
|
+
const checkpointAt = await storage.commit([
|
|
621
|
+
{
|
|
622
|
+
type: "document.change",
|
|
623
|
+
id: firstId,
|
|
624
|
+
content: { kind: "base", version: 2, value: { items: ["checkpoint"], nested: { count: 3 } } },
|
|
625
|
+
},
|
|
626
|
+
], context);
|
|
627
|
+
const replacedAt = await storage.commit([
|
|
628
|
+
{
|
|
629
|
+
type: "document.change",
|
|
630
|
+
id: firstId,
|
|
631
|
+
content: {
|
|
632
|
+
kind: "delta",
|
|
633
|
+
version: 2,
|
|
634
|
+
ops: [["r", { items: ["replacement"], nested: { count: 4 } }]],
|
|
635
|
+
},
|
|
636
|
+
},
|
|
637
|
+
], context);
|
|
638
|
+
expect(await storage.document(firstId, changedAt, context)).toMatchObject({
|
|
639
|
+
version: 1,
|
|
640
|
+
value: { items: ["a", "b"], nested: { count: 2 } },
|
|
641
|
+
});
|
|
642
|
+
expect(await storage.document(firstId, checkpointAt, context)).toMatchObject({
|
|
643
|
+
version: 2,
|
|
644
|
+
value: { items: ["checkpoint"], nested: { count: 3 } },
|
|
645
|
+
deltasSinceBase: 0,
|
|
646
|
+
});
|
|
647
|
+
expect(await storage.document(firstId, replacedAt, context)).toMatchObject({
|
|
648
|
+
value: { items: ["replacement"], nested: { count: 4 } },
|
|
649
|
+
deltasSinceBase: 1,
|
|
650
|
+
});
|
|
651
|
+
expect((await storage.document(firstId, "current", context))?.deltasSinceBase).toBe(1);
|
|
652
|
+
const secondId = await storage.mintId();
|
|
653
|
+
const retiredAt = await storage.commit([
|
|
654
|
+
{
|
|
655
|
+
type: "document.create",
|
|
656
|
+
record: { ...firstRecord, id: secondId },
|
|
657
|
+
content: { kind: "base", version: 1, value: { items: ["new"] } },
|
|
658
|
+
},
|
|
659
|
+
{ type: "document.retire", id: firstId },
|
|
660
|
+
{
|
|
661
|
+
type: "document.change",
|
|
662
|
+
id: firstId,
|
|
663
|
+
content: { kind: "delta", version: 2, ops: [["s", ["retiring"], true]] },
|
|
664
|
+
},
|
|
665
|
+
], context);
|
|
666
|
+
const address = { kind: firstRecord.kind, scope: firstRecord.scope };
|
|
667
|
+
expect((await storage.findDocument(address, changedAt, context))?.id).toBe(firstId);
|
|
668
|
+
expect(await storage.findDocument(address, retiredAt, context)).toMatchObject({
|
|
669
|
+
id: secondId,
|
|
670
|
+
createdAt: retiredAt,
|
|
671
|
+
});
|
|
672
|
+
expect((await storage.scanDocuments({ scope: firstRecord.scope, at: changedAt }, 10, undefined, context)).items.map(({ id }) => id)).toEqual([firstId]);
|
|
673
|
+
expect((await storage.scanDocuments({ scope: firstRecord.scope, at: retiredAt }, 10, undefined, context)).items.map(({ id }) => id)).toEqual([secondId]);
|
|
674
|
+
expect(await storage.document(firstId, retiredAt, context)).toBeUndefined();
|
|
675
|
+
expect((await storage.document(secondId, "current", context))?.value).toEqual({ items: ["new"] });
|
|
676
|
+
}),
|
|
677
|
+
createCase(options, "streams long document tails across root replacement deltas", async (storage) => {
|
|
678
|
+
const rootId = await createRoot(storage);
|
|
679
|
+
const id = await storage.mintId();
|
|
680
|
+
const record = {
|
|
681
|
+
id,
|
|
682
|
+
kind: "conversation.long-tail",
|
|
683
|
+
scope: { kind: "conversation", conversationId: rootId },
|
|
684
|
+
history: "rewindable",
|
|
685
|
+
fork: "asOf",
|
|
686
|
+
};
|
|
687
|
+
const initial = {
|
|
688
|
+
revision: 0,
|
|
689
|
+
rows: Array.from({ length: 512 }, (_, value) => ({ value, stable: `row-${value}` })),
|
|
690
|
+
};
|
|
691
|
+
const createdAt = await storage.commit([{ type: "document.create", record, content: { kind: "base", version: 1, value: initial } }], context);
|
|
692
|
+
const beforeReplacement = structuredClone(initial);
|
|
693
|
+
let beforeReplacementAt = createdAt;
|
|
694
|
+
for (let revision = 1; revision <= 24; revision++) {
|
|
695
|
+
const index = (revision * 17) % beforeReplacement.rows.length;
|
|
696
|
+
beforeReplacement.rows[index].value = -revision;
|
|
697
|
+
beforeReplacement.revision = revision;
|
|
698
|
+
beforeReplacementAt = await storage.commit([
|
|
699
|
+
{
|
|
700
|
+
type: "document.change",
|
|
701
|
+
id,
|
|
702
|
+
content: {
|
|
703
|
+
kind: "delta",
|
|
704
|
+
version: 1,
|
|
705
|
+
ops: [
|
|
706
|
+
["s", ["rows", index, "value"], -revision],
|
|
707
|
+
["s", ["revision"], revision],
|
|
708
|
+
],
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
], context);
|
|
712
|
+
}
|
|
713
|
+
const replacement = {
|
|
714
|
+
revision: 100,
|
|
715
|
+
rows: Array.from({ length: 512 }, (_, value) => ({ value: 10_000 + value, stable: `new-${value}` })),
|
|
716
|
+
};
|
|
717
|
+
const replacementSnapshot = structuredClone(replacement);
|
|
718
|
+
const replacementAt = await storage.commit([
|
|
719
|
+
{
|
|
720
|
+
type: "document.change",
|
|
721
|
+
id,
|
|
722
|
+
content: { kind: "delta", version: 1, ops: [["r", replacement]] },
|
|
723
|
+
},
|
|
724
|
+
], context);
|
|
725
|
+
replacement.rows[0].value = -999;
|
|
726
|
+
const current = structuredClone(replacementSnapshot);
|
|
727
|
+
for (let revision = 101; revision <= 124; revision++) {
|
|
728
|
+
const index = (revision * 19) % current.rows.length;
|
|
729
|
+
current.rows[index].value = -revision;
|
|
730
|
+
current.revision = revision;
|
|
731
|
+
await storage.commit([
|
|
732
|
+
{
|
|
733
|
+
type: "document.change",
|
|
734
|
+
id,
|
|
735
|
+
content: {
|
|
736
|
+
kind: "delta",
|
|
737
|
+
version: 1,
|
|
738
|
+
ops: [
|
|
739
|
+
["s", ["rows", index, "value"], -revision],
|
|
740
|
+
["s", ["revision"], revision],
|
|
741
|
+
],
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
], context);
|
|
745
|
+
}
|
|
746
|
+
expect((await storage.document(id, createdAt, context))?.value).toEqual(initial);
|
|
747
|
+
expect((await storage.document(id, beforeReplacementAt, context))?.value).toEqual(beforeReplacement);
|
|
748
|
+
expect((await storage.document(id, replacementAt, context))?.value).toEqual(replacementSnapshot);
|
|
749
|
+
const read = (await storage.document(id, "current", context));
|
|
750
|
+
expect(read.value).toEqual(current);
|
|
751
|
+
read.value.rows[0].value = -1_000;
|
|
752
|
+
expect((await storage.document(id, "current", context))?.value).toEqual(current);
|
|
753
|
+
}),
|
|
754
|
+
createCase(options, "copies stored document bases independently and rejects ambiguous sources", async (storage) => {
|
|
755
|
+
const rootId = await createRoot(storage);
|
|
756
|
+
const childId = await storage.mintId();
|
|
757
|
+
const secondChildId = await storage.mintId();
|
|
758
|
+
await storage.commit([
|
|
759
|
+
{ type: "conversation", value: { id: childId } },
|
|
760
|
+
{ type: "conversation", value: { id: secondChildId } },
|
|
761
|
+
], context);
|
|
762
|
+
const sourceId = await storage.mintId();
|
|
763
|
+
const sourceRecord = {
|
|
764
|
+
id: sourceId,
|
|
765
|
+
kind: "copy.source",
|
|
766
|
+
scope: { kind: "conversation", conversationId: rootId },
|
|
767
|
+
history: "rewindable",
|
|
768
|
+
fork: "asOf",
|
|
769
|
+
};
|
|
770
|
+
const createdAt = await storage.commit([
|
|
771
|
+
{
|
|
772
|
+
type: "document.create",
|
|
773
|
+
record: sourceRecord,
|
|
774
|
+
content: { kind: "base", version: 2, value: { count: 1, rows: [{ value: "base" }] } },
|
|
775
|
+
},
|
|
776
|
+
], context);
|
|
777
|
+
await storage.commit([
|
|
778
|
+
{
|
|
779
|
+
type: "document.change",
|
|
780
|
+
id: sourceId,
|
|
781
|
+
content: {
|
|
782
|
+
kind: "delta",
|
|
783
|
+
version: 2,
|
|
784
|
+
ops: [
|
|
785
|
+
["s", ["count"], 2],
|
|
786
|
+
["p", ["rows"], 1, 0, [{ value: "current" }]],
|
|
787
|
+
],
|
|
788
|
+
},
|
|
789
|
+
},
|
|
790
|
+
], context);
|
|
791
|
+
const historicalCopyId = await storage.mintId();
|
|
792
|
+
const currentCopyId = await storage.mintId();
|
|
793
|
+
const retiredCopyId = await storage.mintId();
|
|
794
|
+
const childRecord = (id, conversationId) => ({
|
|
795
|
+
id,
|
|
796
|
+
kind: sourceRecord.kind,
|
|
797
|
+
scope: { kind: "conversation", conversationId },
|
|
798
|
+
history: "rewindable",
|
|
799
|
+
fork: "asOf",
|
|
800
|
+
});
|
|
801
|
+
await storage.commit([
|
|
802
|
+
{
|
|
803
|
+
type: "document.copy",
|
|
804
|
+
record: childRecord(historicalCopyId, childId),
|
|
805
|
+
source: { id: sourceId, at: createdAt },
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
type: "document.copy",
|
|
809
|
+
record: childRecord(currentCopyId, secondChildId),
|
|
810
|
+
source: { id: sourceId, at: "current" },
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
type: "document.copy",
|
|
814
|
+
record: childRecord(retiredCopyId, rootId),
|
|
815
|
+
source: { id: sourceId, at: "current" },
|
|
816
|
+
},
|
|
817
|
+
{ type: "document.retire", id: retiredCopyId },
|
|
818
|
+
], context);
|
|
819
|
+
expect(await storage.document(historicalCopyId, "current", context)).toMatchObject({
|
|
820
|
+
version: 2,
|
|
821
|
+
value: { count: 1, rows: [{ value: "base" }] },
|
|
822
|
+
});
|
|
823
|
+
expect(await storage.document(currentCopyId, "current", context)).toMatchObject({
|
|
824
|
+
version: 2,
|
|
825
|
+
value: { count: 2, rows: [{ value: "base" }, { value: "current" }] },
|
|
826
|
+
});
|
|
827
|
+
expect(await storage.document(retiredCopyId, "current", context)).toBeUndefined();
|
|
828
|
+
await storage.commit([
|
|
829
|
+
{
|
|
830
|
+
type: "document.change",
|
|
831
|
+
id: sourceId,
|
|
832
|
+
content: { kind: "base", version: 2, value: { count: 99, rows: [] } },
|
|
833
|
+
},
|
|
834
|
+
{ type: "document.retire", id: sourceId },
|
|
835
|
+
], context);
|
|
836
|
+
expect((await storage.document(currentCopyId, "current", context))?.value).toEqual({
|
|
837
|
+
count: 2,
|
|
838
|
+
rows: [{ value: "base" }, { value: "current" }],
|
|
839
|
+
});
|
|
840
|
+
const latestSourceId = await storage.mintId();
|
|
841
|
+
const latestCopyId = await storage.mintId();
|
|
842
|
+
const latestSource = {
|
|
843
|
+
id: latestSourceId,
|
|
844
|
+
kind: "copy.latest",
|
|
845
|
+
scope: { kind: "conversation", conversationId: rootId },
|
|
846
|
+
history: "latest",
|
|
847
|
+
fork: "current",
|
|
848
|
+
};
|
|
849
|
+
await storage.commit([
|
|
850
|
+
{
|
|
851
|
+
type: "document.create",
|
|
852
|
+
record: latestSource,
|
|
853
|
+
content: { kind: "base", version: 4, value: { retained: "copy" } },
|
|
854
|
+
},
|
|
855
|
+
], context);
|
|
856
|
+
await storage.commit([
|
|
857
|
+
{
|
|
858
|
+
type: "document.copy",
|
|
859
|
+
record: {
|
|
860
|
+
...latestSource,
|
|
861
|
+
id: latestCopyId,
|
|
862
|
+
scope: { kind: "conversation", conversationId: childId },
|
|
863
|
+
},
|
|
864
|
+
source: { id: latestSourceId, at: "current" },
|
|
865
|
+
},
|
|
866
|
+
], context);
|
|
867
|
+
await storage.commit([
|
|
868
|
+
{
|
|
869
|
+
type: "document.change",
|
|
870
|
+
id: latestSourceId,
|
|
871
|
+
content: { kind: "base", version: 4, value: { retained: "source-only" } },
|
|
872
|
+
},
|
|
873
|
+
{ type: "document.retire", id: latestSourceId },
|
|
874
|
+
], context);
|
|
875
|
+
expect(await storage.document(latestCopyId, "current", context)).toMatchObject({
|
|
876
|
+
version: 4,
|
|
877
|
+
value: { retained: "copy" },
|
|
878
|
+
});
|
|
879
|
+
const conflictId = await storage.mintId();
|
|
880
|
+
let conflictError;
|
|
881
|
+
try {
|
|
882
|
+
await storage.commit([
|
|
883
|
+
{
|
|
884
|
+
type: "document.copy",
|
|
885
|
+
record: childRecord(conflictId, childId),
|
|
886
|
+
source: { id: currentCopyId, at: "current" },
|
|
887
|
+
},
|
|
888
|
+
{ type: "document.retire", id: currentCopyId },
|
|
889
|
+
], context);
|
|
890
|
+
}
|
|
891
|
+
catch (error) {
|
|
892
|
+
conflictError = error;
|
|
893
|
+
}
|
|
894
|
+
// Rejected without effect: the copy and the retirement in the same batch are both absent.
|
|
895
|
+
expect(conflictError instanceof Error).toBe(true);
|
|
896
|
+
expect(await storage.document(conflictId, "current", context)).toBeUndefined();
|
|
897
|
+
expect((await storage.document(currentCopyId, "current", context))?.value).toEqual({
|
|
898
|
+
count: 2,
|
|
899
|
+
rows: [{ value: "base" }, { value: "current" }],
|
|
900
|
+
});
|
|
901
|
+
const mismatchId = await storage.mintId();
|
|
902
|
+
let mismatchError;
|
|
903
|
+
try {
|
|
904
|
+
await storage.commit([
|
|
905
|
+
{
|
|
906
|
+
type: "document.copy",
|
|
907
|
+
record: { ...childRecord(mismatchId, childId), kind: "copy.mismatch" },
|
|
908
|
+
source: { id: currentCopyId, at: "current" },
|
|
909
|
+
},
|
|
910
|
+
], context);
|
|
911
|
+
}
|
|
912
|
+
catch (error) {
|
|
913
|
+
mismatchError = error;
|
|
914
|
+
}
|
|
915
|
+
expect(mismatchError instanceof Error).toBe(true);
|
|
916
|
+
expect(await storage.document(mismatchId, "current", context)).toBeUndefined();
|
|
917
|
+
}),
|
|
918
|
+
createCase(options, "uses bases for version transitions and rejects historical reads of current-only documents", async (storage) => {
|
|
919
|
+
await createRoot(storage);
|
|
920
|
+
const id = await storage.mintId();
|
|
921
|
+
const record = {
|
|
922
|
+
id,
|
|
923
|
+
kind: "session.settings",
|
|
924
|
+
scope: { kind: "session" },
|
|
925
|
+
};
|
|
926
|
+
await storage.commit([{ type: "document.create", record, content: { kind: "base", version: 1, value: { count: 1 } } }], context);
|
|
927
|
+
await storage.commit([{ type: "document.change", id, content: { kind: "delta", version: 1, ops: [["s", ["count"], 2]] } }], context);
|
|
928
|
+
const migratedAt = await storage.commit([{ type: "document.change", id, content: { kind: "base", version: 2, value: { count: 3 } } }], context);
|
|
929
|
+
expect(await storage.document(id, "current", context)).toMatchObject({ version: 2, value: { count: 3 } });
|
|
930
|
+
await expect(storage.document(id, migratedAt, context)).rejects.toThrow("does not retain historical content");
|
|
931
|
+
await expect(storage.commit([{ type: "document.change", id, content: { kind: "delta", version: 1, ops: [["s", ["count"], 4]] } }], context)).rejects.toThrow("version transition requires a base");
|
|
932
|
+
expect((await storage.document(id, "current", context))?.value).toEqual({ count: 3 });
|
|
933
|
+
await storage.commit([{ type: "document.retire", id }], context);
|
|
934
|
+
expect(await storage.document(id, "current", context)).toBeUndefined();
|
|
935
|
+
}),
|
|
936
|
+
createCase(options, "indexes logical addresses and exact-scope scans independently", async (storage) => {
|
|
937
|
+
const rootId = await createRoot(storage);
|
|
938
|
+
const firstId = await storage.mintId();
|
|
939
|
+
const secondId = await storage.mintId();
|
|
940
|
+
const conversationId = await storage.mintId();
|
|
941
|
+
const taskId = await storage.mintId();
|
|
942
|
+
const taskSingletonId = await storage.mintId();
|
|
943
|
+
const taskFamilyId = await storage.mintId();
|
|
944
|
+
const taskOtherKindId = await storage.mintId();
|
|
945
|
+
const createdAt = await storage.commit([
|
|
946
|
+
{ type: "task", value: pendingTask(taskId, rootId) },
|
|
947
|
+
{
|
|
948
|
+
type: "document.create",
|
|
949
|
+
record: {
|
|
950
|
+
id: firstId,
|
|
951
|
+
kind: "cache",
|
|
952
|
+
scope: { kind: "session" },
|
|
953
|
+
key: "__proto__",
|
|
954
|
+
},
|
|
955
|
+
content: { kind: "base", version: 1, value: { owner: "first" } },
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
type: "document.create",
|
|
959
|
+
record: {
|
|
960
|
+
id: secondId,
|
|
961
|
+
kind: "cache",
|
|
962
|
+
scope: { kind: "session" },
|
|
963
|
+
key: "constructor",
|
|
964
|
+
},
|
|
965
|
+
content: { kind: "base", version: 1, value: { owner: "second" } },
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
type: "document.create",
|
|
969
|
+
record: {
|
|
970
|
+
id: conversationId,
|
|
971
|
+
kind: "cache",
|
|
972
|
+
scope: { kind: "conversation", conversationId: rootId },
|
|
973
|
+
history: "latest",
|
|
974
|
+
fork: "current",
|
|
975
|
+
key: "__proto__",
|
|
976
|
+
},
|
|
977
|
+
content: { kind: "base", version: 1, value: { owner: "conversation" } },
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
type: "document.create",
|
|
981
|
+
record: {
|
|
982
|
+
id: taskSingletonId,
|
|
983
|
+
kind: "task.cache",
|
|
984
|
+
scope: { kind: "task", taskId },
|
|
985
|
+
},
|
|
986
|
+
content: { kind: "base", version: 1, value: { owner: "singleton" } },
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
type: "document.create",
|
|
990
|
+
record: {
|
|
991
|
+
id: taskFamilyId,
|
|
992
|
+
kind: "task.cache",
|
|
993
|
+
scope: { kind: "task", taskId },
|
|
994
|
+
key: "member",
|
|
995
|
+
},
|
|
996
|
+
content: { kind: "base", version: 1, value: { owner: "family" } },
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
type: "document.create",
|
|
1000
|
+
record: {
|
|
1001
|
+
id: taskOtherKindId,
|
|
1002
|
+
kind: "task.other",
|
|
1003
|
+
scope: { kind: "task", taskId },
|
|
1004
|
+
},
|
|
1005
|
+
content: { kind: "base", version: 1, value: { owner: "other" } },
|
|
1006
|
+
},
|
|
1007
|
+
], context);
|
|
1008
|
+
expect((await storage.findDocument({ kind: "cache", scope: { kind: "session" }, key: "__proto__" }, "current", context))?.id).toBe(firstId);
|
|
1009
|
+
expect((await storage.scanDocuments({ scope: { kind: "session" }, at: "current" }, 1, undefined, context)).items).toHaveLength(1);
|
|
1010
|
+
const first = await storage.scanDocuments({ scope: { kind: "session" }, at: "current" }, 1, undefined, context);
|
|
1011
|
+
const second = await storage.scanDocuments({ scope: { kind: "session" }, at: "current" }, 1, first.next, context);
|
|
1012
|
+
expect([...first.items, ...second.items].map(({ id }) => id)).toEqual([firstId, secondId]);
|
|
1013
|
+
expect((await storage.scanDocuments({ scope: { kind: "conversation", conversationId: rootId }, at: "current" }, 10, undefined, context)).items.map(({ id }) => id)).toEqual([conversationId]);
|
|
1014
|
+
expect((await storage.findDocument({ kind: "task.cache", scope: { kind: "task", taskId } }, "current", context))
|
|
1015
|
+
?.id).toBe(taskSingletonId);
|
|
1016
|
+
expect((await storage.findDocument({ kind: "task.cache", scope: { kind: "task", taskId }, key: "member" }, "current", context))?.id).toBe(taskFamilyId);
|
|
1017
|
+
expect((await storage.scanDocuments({ scope: { kind: "task", taskId }, at: "current", kind: "task.cache" }, 10, undefined, context)).items.map(({ id }) => id)).toEqual([taskSingletonId, taskFamilyId]);
|
|
1018
|
+
await expect(storage.document(taskSingletonId, createdAt, context)).rejects.toThrow("does not retain historical content");
|
|
1019
|
+
}),
|
|
1020
|
+
createCase(options, "keeps document lifecycle failures atomic and gives create-plus-retire an empty lifetime", async (storage) => {
|
|
1021
|
+
const rootId = await createRoot(storage);
|
|
1022
|
+
const firstId = await storage.mintId();
|
|
1023
|
+
const secondId = await storage.mintId();
|
|
1024
|
+
const record = {
|
|
1025
|
+
id: firstId,
|
|
1026
|
+
kind: "singleton",
|
|
1027
|
+
scope: { kind: "session" },
|
|
1028
|
+
};
|
|
1029
|
+
await storage.commit([{ type: "document.create", record, content: { kind: "base", version: 1, value: { value: 1 } } }], context);
|
|
1030
|
+
await expect(storage.commit([
|
|
1031
|
+
{
|
|
1032
|
+
type: "document.create",
|
|
1033
|
+
record: { ...record, id: secondId },
|
|
1034
|
+
content: { kind: "base", version: 1, value: { value: 2 } },
|
|
1035
|
+
},
|
|
1036
|
+
{ type: "document.change", id: firstId, content: { kind: "delta", version: 1, ops: [] } },
|
|
1037
|
+
], context)).rejects.toThrow("already has a current incarnation");
|
|
1038
|
+
expect((await storage.document(firstId, "current", context))?.value).toEqual({ value: 1 });
|
|
1039
|
+
expect(await storage.document(secondId, "current", context)).toBeUndefined();
|
|
1040
|
+
const emptyId = await storage.mintId();
|
|
1041
|
+
const emptyAt = await storage.commit([
|
|
1042
|
+
{
|
|
1043
|
+
type: "document.create",
|
|
1044
|
+
record: {
|
|
1045
|
+
id: emptyId,
|
|
1046
|
+
kind: record.kind,
|
|
1047
|
+
key: "empty",
|
|
1048
|
+
scope: { kind: "conversation", conversationId: rootId },
|
|
1049
|
+
history: "rewindable",
|
|
1050
|
+
fork: "initial",
|
|
1051
|
+
},
|
|
1052
|
+
content: { kind: "base", version: 1, value: {} },
|
|
1053
|
+
},
|
|
1054
|
+
{ type: "document.retire", id: emptyId },
|
|
1055
|
+
], context);
|
|
1056
|
+
expect(await storage.document(emptyId, "current", context)).toBeUndefined();
|
|
1057
|
+
expect(await storage.document(emptyId, emptyAt, context)).toBeUndefined();
|
|
1058
|
+
expect(await storage.findDocument({
|
|
1059
|
+
kind: record.kind,
|
|
1060
|
+
scope: { kind: "conversation", conversationId: rootId },
|
|
1061
|
+
key: "empty",
|
|
1062
|
+
}, emptyAt, context)).toBeUndefined();
|
|
1063
|
+
}),
|
|
1064
|
+
createCase(options, "rolls back record tables and secondary indexes when a document command fails", async (storage) => {
|
|
1065
|
+
const rootId = await createRoot(storage);
|
|
1066
|
+
const taskId = await storage.mintId();
|
|
1067
|
+
const submissionId = await storage.mintId();
|
|
1068
|
+
const documentId = await storage.mintId();
|
|
1069
|
+
const task = pendingTask(taskId, rootId);
|
|
1070
|
+
const submission = {
|
|
1071
|
+
id: submissionId,
|
|
1072
|
+
conversationId: rootId,
|
|
1073
|
+
requestId: "atomic",
|
|
1074
|
+
type: "input",
|
|
1075
|
+
status: "queued",
|
|
1076
|
+
};
|
|
1077
|
+
const record = {
|
|
1078
|
+
id: documentId,
|
|
1079
|
+
kind: "atomic",
|
|
1080
|
+
scope: { kind: "session" },
|
|
1081
|
+
};
|
|
1082
|
+
const baselineSeq = await storage.commit([
|
|
1083
|
+
{ type: "task", value: task },
|
|
1084
|
+
{ type: "submission", value: submission },
|
|
1085
|
+
{ type: "document.create", record, content: { kind: "base", version: 1, value: { count: 1 } } },
|
|
1086
|
+
], context);
|
|
1087
|
+
const entryId = await storage.mintId();
|
|
1088
|
+
const conflictingDocumentId = await storage.mintId();
|
|
1089
|
+
await expect(storage.commit([
|
|
1090
|
+
{
|
|
1091
|
+
type: "task",
|
|
1092
|
+
value: { ...task, state: { status: "running", checkpoint: { phase: "effect" } } },
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
type: "submission",
|
|
1096
|
+
value: { ...submission, status: "unanswered", reason: "failed" },
|
|
1097
|
+
},
|
|
1098
|
+
{ type: "entry", value: entry(entryId, rootId, "transient") },
|
|
1099
|
+
{
|
|
1100
|
+
type: "document.create",
|
|
1101
|
+
record: { ...record, id: conflictingDocumentId },
|
|
1102
|
+
content: { kind: "base", version: 1, value: { count: 2 } },
|
|
1103
|
+
},
|
|
1104
|
+
], context)).rejects.toThrow("already has a current incarnation");
|
|
1105
|
+
expect(await storage.task(taskId, context)).toEqual(task);
|
|
1106
|
+
expect((await storage.scanTasks({ status: "pending" }, 10, undefined, context)).items).toEqual([task]);
|
|
1107
|
+
expect(await storage.submissionByRequest(rootId, "atomic", context)).toEqual(submission);
|
|
1108
|
+
expect(await storage.entry(entryId, context)).toBeUndefined();
|
|
1109
|
+
expect(await storage.document(conflictingDocumentId, "current", context)).toBeUndefined();
|
|
1110
|
+
expect((await storage.findDocument({ kind: record.kind, scope: record.scope }, "current", context))?.id).toBe(documentId);
|
|
1111
|
+
const afterRollbackSeq = await storage.commit([
|
|
1112
|
+
{
|
|
1113
|
+
type: "document.change",
|
|
1114
|
+
id: documentId,
|
|
1115
|
+
content: { kind: "delta", version: 1, ops: [["s", ["count"], 3]] },
|
|
1116
|
+
},
|
|
1117
|
+
], context);
|
|
1118
|
+
expect(afterRollbackSeq).toBeGreaterThan(baselineSeq);
|
|
1119
|
+
}),
|
|
1120
|
+
createCase(options, "keeps indexed string identities lossless", async (storage) => {
|
|
1121
|
+
const rootId = await createRoot(storage);
|
|
1122
|
+
const first = "\ud800";
|
|
1123
|
+
const second = "\ud801";
|
|
1124
|
+
const firstTaskId = await storage.mintId();
|
|
1125
|
+
const secondTaskId = await storage.mintId();
|
|
1126
|
+
const firstSubmissionId = await storage.mintId();
|
|
1127
|
+
const secondSubmissionId = await storage.mintId();
|
|
1128
|
+
const firstKindDocumentId = await storage.mintId();
|
|
1129
|
+
const secondKindDocumentId = await storage.mintId();
|
|
1130
|
+
const firstKeyDocumentId = await storage.mintId();
|
|
1131
|
+
const secondKeyDocumentId = await storage.mintId();
|
|
1132
|
+
await storage.commit([
|
|
1133
|
+
{ type: "task", value: { ...pendingTask(firstTaskId, rootId), kind: first } },
|
|
1134
|
+
{ type: "task", value: { ...pendingTask(secondTaskId, rootId), kind: second } },
|
|
1135
|
+
{
|
|
1136
|
+
type: "submission",
|
|
1137
|
+
value: {
|
|
1138
|
+
id: firstSubmissionId,
|
|
1139
|
+
conversationId: rootId,
|
|
1140
|
+
requestId: first,
|
|
1141
|
+
type: "input",
|
|
1142
|
+
status: "queued",
|
|
1143
|
+
},
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
type: "submission",
|
|
1147
|
+
value: {
|
|
1148
|
+
id: secondSubmissionId,
|
|
1149
|
+
conversationId: rootId,
|
|
1150
|
+
requestId: second,
|
|
1151
|
+
type: "input",
|
|
1152
|
+
status: "queued",
|
|
1153
|
+
},
|
|
1154
|
+
},
|
|
1155
|
+
{
|
|
1156
|
+
type: "document.create",
|
|
1157
|
+
record: { id: firstKindDocumentId, kind: first, scope: { kind: "session" } },
|
|
1158
|
+
content: { kind: "base", version: 1, value: { identity: "first kind" } },
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
type: "document.create",
|
|
1162
|
+
record: { id: secondKindDocumentId, kind: second, scope: { kind: "session" } },
|
|
1163
|
+
content: { kind: "base", version: 1, value: { identity: "second kind" } },
|
|
1164
|
+
},
|
|
1165
|
+
{
|
|
1166
|
+
type: "document.create",
|
|
1167
|
+
record: { id: firstKeyDocumentId, kind: "family", key: first, scope: { kind: "session" } },
|
|
1168
|
+
content: { kind: "base", version: 1, value: { identity: "first key" } },
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
type: "document.create",
|
|
1172
|
+
record: { id: secondKeyDocumentId, kind: "family", key: second, scope: { kind: "session" } },
|
|
1173
|
+
content: { kind: "base", version: 1, value: { identity: "second key" } },
|
|
1174
|
+
},
|
|
1175
|
+
], context);
|
|
1176
|
+
expect((await storage.scanTasks({ kind: first }, 10, undefined, context)).items.map(({ id }) => id)).toEqual([
|
|
1177
|
+
firstTaskId,
|
|
1178
|
+
]);
|
|
1179
|
+
expect((await storage.scanTasks({ kind: second }, 10, undefined, context)).items.map(({ id }) => id)).toEqual([
|
|
1180
|
+
secondTaskId,
|
|
1181
|
+
]);
|
|
1182
|
+
expect((await storage.task(firstTaskId, context))?.kind).toBe(first);
|
|
1183
|
+
expect((await storage.task(secondTaskId, context))?.kind).toBe(second);
|
|
1184
|
+
expect((await storage.submissionByRequest(rootId, first, context))?.requestId).toBe(first);
|
|
1185
|
+
expect((await storage.submissionByRequest(rootId, first, context))?.id).toBe(firstSubmissionId);
|
|
1186
|
+
expect((await storage.submissionByRequest(rootId, second, context))?.id).toBe(secondSubmissionId);
|
|
1187
|
+
expect((await storage.findDocument({ kind: first, scope: { kind: "session" } }, "current", context))?.id).toBe(firstKindDocumentId);
|
|
1188
|
+
expect((await storage.findDocument({ kind: second, scope: { kind: "session" } }, "current", context))?.id).toBe(secondKindDocumentId);
|
|
1189
|
+
expect((await storage.findDocument({ kind: "family", key: first, scope: { kind: "session" } }, "current", context))
|
|
1190
|
+
?.id).toBe(firstKeyDocumentId);
|
|
1191
|
+
expect((await storage.findDocument({ kind: "family", key: second, scope: { kind: "session" } }, "current", context))?.id).toBe(secondKeyDocumentId);
|
|
1192
|
+
expect((await storage.scanDocuments({ scope: { kind: "session" }, at: "current", kind: first }, 10, undefined, context)).items.map(({ id }) => id)).toEqual([firstKindDocumentId]);
|
|
1193
|
+
}),
|
|
1194
|
+
createCase(options, "keeps one global record ID namespace and rejects exhausted ID minting", async (storage) => {
|
|
1195
|
+
const rootId = await createRoot(storage);
|
|
1196
|
+
const explicitEntryId = idFromNumber(100);
|
|
1197
|
+
await storage.commit([{ type: "entry", value: entry(explicitEntryId, rootId) }], context);
|
|
1198
|
+
expect(await storage.mintId()).toBe(101);
|
|
1199
|
+
await expect(storage.commit([{ type: "task", value: pendingTask(idFromNumber(explicitEntryId), rootId) }], context)).rejects.toThrow(`ID ${explicitEntryId} already belongs to entry`);
|
|
1200
|
+
await storage.commit([{ type: "entry", value: entry(idFromNumber(Number.MAX_SAFE_INTEGER), rootId, "last-id") }], context);
|
|
1201
|
+
await expect(storage.mintId()).rejects.toThrow("ID space is exhausted");
|
|
1202
|
+
await expect(storage.mintId()).rejects.toThrow("ID space is exhausted");
|
|
1203
|
+
}),
|
|
1204
|
+
createCase(options, "rejects every operation after close", async (storage) => {
|
|
1205
|
+
await createRoot(storage);
|
|
1206
|
+
await storage.close(context);
|
|
1207
|
+
await expect(storage.conversation(ROOT_CONVERSATION_ID, context)).rejects.toThrow("closed");
|
|
1208
|
+
await expect(storage.commit([], context)).rejects.toThrow("closed");
|
|
1209
|
+
await expect(storage.mintId()).rejects.toThrow("closed");
|
|
1210
|
+
}),
|
|
1211
|
+
];
|
|
1212
|
+
}
|
|
1213
|
+
//# sourceMappingURL=storage-conformance.js.map
|