@jskit-ai/assistant-core 0.1.199 → 0.1.201
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/examples/conversation/README.md +8 -1
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +4 -1
- package/package.json +99 -36
- package/src/client/conversation/AssistantConversationElement.vue +27 -4
- package/src/client/conversation/AssistantConversationStatus.vue +24 -0
- package/src/client/conversation/AssistantGoalControl.vue +8 -5
- package/src/client/conversation/AssistantTranscript.vue +10 -3
- package/src/client/conversation/index.js +2 -0
- package/src/client/conversation/messageDelivery.js +115 -32
- package/src/client/conversation/retainedConversation.js +32 -0
- package/src/client/conversation/useAssistantQuestions.js +76 -0
- package/src/client/lib/assistantApi.js +72 -2
- package/src/server/conversation/agentRun.js +222 -0
- package/src/server/conversation/attachments.js +93 -0
- package/src/server/conversation/claudeClient.js +221 -0
- package/src/server/conversation/claudeHistory.js +256 -0
- package/src/server/conversation/claudeProcess.js +327 -0
- package/src/server/conversation/claudeTools.js +42 -0
- package/src/server/conversation/claudeTurn.js +1082 -0
- package/src/server/conversation/codexConfiguration.js +1183 -0
- package/src/server/conversation/codexConversations.js +1082 -0
- package/src/server/conversation/codexDetachedTurn.js +332 -0
- package/src/server/conversation/codexEvents.js +194 -1
- package/src/server/conversation/codexGoalCommands.js +124 -0
- package/src/server/conversation/codexHelperExecution.js +306 -0
- package/src/server/conversation/codexHelperThreadLedger.js +804 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
- package/src/server/conversation/codexHelperThreads.js +638 -0
- package/src/server/conversation/codexHistoryAdapter.js +369 -0
- package/src/server/conversation/codexMessageCommands.js +663 -0
- package/src/server/conversation/codexMessageDelivery.js +211 -0
- package/src/server/conversation/codexNativeHistoryExport.js +95 -0
- package/src/server/conversation/codexNotificationObserver.js +482 -0
- package/src/server/conversation/codexNotificationQueue.js +103 -0
- package/src/server/conversation/codexObservationControl.js +174 -0
- package/src/server/conversation/codexOutput.js +931 -0
- package/src/server/conversation/codexProcess.js +1559 -0
- package/src/server/conversation/codexProcessLeader.js +14 -0
- package/src/server/conversation/codexProvider.js +2710 -0
- package/src/server/conversation/codexProviderOwner.js +1058 -0
- package/src/server/conversation/codexProviderSelection.js +218 -0
- package/src/server/conversation/codexReasoning.js +314 -0
- package/src/server/conversation/codexRenewalCommands.js +290 -0
- package/src/server/conversation/codexResultSettlement.js +409 -0
- package/src/server/conversation/codexRunJournal.js +658 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
- package/src/server/conversation/codexSessionCleanup.js +96 -0
- package/src/server/conversation/codexSessionReconciliation.js +284 -0
- package/src/server/conversation/codexThreadReadiness.js +397 -0
- package/src/server/conversation/codexThreadStatus.js +354 -0
- package/src/server/conversation/codexTurn.js +701 -255
- package/src/server/conversation/codexTurnControl.js +317 -0
- package/src/server/conversation/codexTurnRecovery.js +990 -0
- package/src/server/conversation/codexTurnState.js +549 -0
- package/src/server/conversation/commandHook.js +18 -0
- package/src/server/conversation/commandWrapper.js +37 -0
- package/src/server/conversation/configuration.js +43 -0
- package/src/server/conversation/continuity.js +289 -0
- package/src/server/conversation/fileStorage.js +55 -0
- package/src/server/conversation/hookBridge.js +19 -0
- package/src/server/conversation/index.js +13 -0
- package/src/server/conversation/localExecution.js +665 -0
- package/src/server/conversation/memoryStorage.js +7 -72
- package/src/server/conversation/nativeHistoryExport.js +90 -0
- package/src/server/conversation/nativeStorage.js +23 -0
- package/src/server/conversation/openCodeClient.js +209 -19
- package/src/server/conversation/openCodePlugin.js +84 -0
- package/src/server/conversation/openCodeProcess.js +476 -0
- package/src/server/conversation/openCodeRuntime.js +1578 -0
- package/src/server/conversation/openCodeTools.js +60 -0
- package/src/server/conversation/openCodeTurn.js +557 -0
- package/src/server/conversation/provider.js +30 -0
- package/src/server/conversation/providers/api.js +136 -0
- package/src/server/conversation/providers/claude.js +63 -0
- package/src/server/conversation/providers/claudeDriver.js +425 -0
- package/src/server/conversation/providers/codex.js +248 -0
- package/src/server/conversation/providers/codexDriver.js +1078 -0
- package/src/server/conversation/providers/opencode.js +49 -0
- package/src/server/conversation/providers/opencodeDriver.js +650 -0
- package/src/server/conversation/providers/scoped.js +312 -0
- package/src/server/conversation/runtime.js +2220 -0
- package/src/server/conversation/runtimeStateUpgrade.js +125 -0
- package/src/server/conversation/storage.js +184 -0
- package/src/server/conversation/streams.js +7 -2
- package/src/server/conversation/structuredOutput.js +240 -0
- package/src/server/conversation/systemPrompt.js +54 -0
- package/src/server/conversation/tools.js +84 -0
- package/src/server/conversation/transcript.js +5 -4
- package/src/server/hosts/claudeHistory.js +5 -0
- package/src/server/hosts/claudeProcess.js +11 -0
- package/src/server/hosts/claudeTurn.js +4 -0
- package/src/server/hosts/codexConfiguration.js +20 -0
- package/src/server/hosts/codexEvents.js +13 -0
- package/src/server/hosts/codexProcess.js +12 -0
- package/src/server/hosts/codexProvider.js +23 -0
- package/src/server/hosts/codexTurn.js +20 -0
- package/src/server/hosts/nativeHistory.js +3 -0
- package/src/server/hosts/openCodeClient.js +3 -0
- package/src/server/hosts/openCodeProcess.js +23 -0
- package/src/server/hosts/openCodeTurn.js +7 -0
- package/src/server/index.js +2 -0
- package/src/server/lib/aiConnectionClient.js +13 -6
- package/src/server/lib/assistantCompletion.js +190 -0
- package/src/server/lib/assistantToolLoop.js +314 -0
- package/src/server/lib/serviceToolCatalog.js +25 -10
- package/src/shared/assistantResponseText.js +29 -0
- package/src/shared/conversation/attachments.js +19 -0
- package/src/shared/conversation/index.js +3 -0
- package/src/shared/conversation/pagination.js +78 -0
- package/src/shared/conversation/patches.js +150 -0
- package/src/shared/conversation/questions.js +48 -0
- package/src/shared/conversation/streaming.js +7 -4
- package/src/shared/index.js +2 -0
- package/src/shared/nativeProviders.js +45 -0
- package/src/testing/conversationStorageContract.js +31 -1
- package/src/testing/nativeCodex.js +11 -0
- package/src/testing/nativeOpenCode.js +3 -0
- package/test/aiConnectionClient.test.js +2 -0
- package/test/apiConversationDriver.test.js +73 -0
- package/test/assistantApiSurfaceHeader.test.js +85 -0
- package/test/assistantBoundedToolLoop.test.js +285 -0
- package/test/assistantScroll.browser.test.js +16 -0
- package/test/claudeClient.test.js +189 -0
- package/test/claudeConversationSession.test.js +1173 -0
- package/test/claudeInstructionResume.test.js +82 -0
- package/test/claudeProcess.test.js +204 -0
- package/test/claudeTurn.test.js +253 -0
- package/test/codexConversationSession.test.js +2122 -0
- package/test/codexGoal.test.js +37 -0
- package/test/codexHelperDeletion.test.js +163 -0
- package/test/codexHelperThreadLedger.test.js +494 -0
- package/test/codexHistoryAdapter.test.js +541 -0
- package/test/codexInteractiveArguments.test.js +30 -0
- package/test/codexIsolation.test.js +756 -0
- package/test/codexModelCatalog.test.js +362 -0
- package/test/codexNativeHistoryExport.test.js +81 -0
- package/test/codexThreadSelection.test.js +136 -0
- package/test/codexTurn.test.js +2158 -0
- package/test/conversation.test.js +110 -0
- package/test/conversationAgentRun.test.js +326 -0
- package/test/conversationApiFacilities.test.js +90 -0
- package/test/conversationAttachments.test.js +238 -0
- package/test/conversationCommandWrapper.test.js +48 -0
- package/test/conversationConfiguration.test.js +174 -0
- package/test/conversationNativeStorage.test.js +25 -0
- package/test/conversationReplacement.test.js +172 -0
- package/test/conversationRuntime.test.js +241 -0
- package/test/conversationRuntimeStateUpgrade.test.js +144 -0
- package/test/conversationSession.test.js +1080 -0
- package/test/conversationStreaming.test.js +68 -0
- package/test/conversationSystemPrompt.test.js +84 -0
- package/test/conversationTools.test.js +416 -0
- package/test/fileConversationStorage.test.js +73 -0
- package/test/fixtures/codexHelperThreadLedger.js +39 -0
- package/test/localConversationExecution.test.js +90 -0
- package/test/messageDelivery.test.js +234 -1
- package/test/nativeConversationRetirement.test.js +215 -0
- package/test/openCodeClient.test.js +515 -0
- package/test/openCodeProcess.test.js +177 -0
- package/test/openCodeRuntime.test.js +322 -0
- package/test/openCodeTools.test.js +287 -0
- package/test/opencodeConversationSession.test.js +789 -0
- package/test/serviceToolCatalog.test.js +24 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createCodexAppServerProviderOwner } from "../src/server/conversation/codexProviderOwner.js";
|
|
4
|
+
|
|
5
|
+
test("cached goal reads retain the original session and interactive-provider boundary", async t => {
|
|
6
|
+
const owner = createCodexAppServerProviderOwner();
|
|
7
|
+
const keys = [];
|
|
8
|
+
const reads = [];
|
|
9
|
+
t.after(() => keys.forEach(key => owner.closeProvider(key)));
|
|
10
|
+
function retain(key, sessionKey, { helper = false, available = true } = {}) {
|
|
11
|
+
keys.push(key);
|
|
12
|
+
return owner.createProvider({ providerKey: key,
|
|
13
|
+
providerOptions: { runtimeDir: `/tmp/cached-goal-${key}` }, owner: { sessionKey },
|
|
14
|
+
create: () => ({
|
|
15
|
+
isHelperProvider: () => helper,
|
|
16
|
+
isAvailable: () => available,
|
|
17
|
+
async readGoal(threadId) { reads.push([key, threadId]); return { goal: { status: "paused" } }; },
|
|
18
|
+
ensureAvailable() { assert.fail("Reading a goal must not acquire a provider"); },
|
|
19
|
+
close() {}
|
|
20
|
+
}) });
|
|
21
|
+
}
|
|
22
|
+
assert.deepEqual(await owner.readSessionGoal("one", ""), { status: "available", threadId: "", goal: null });
|
|
23
|
+
assert.deepEqual(await owner.readSessionGoal("one", "thread-one"), { status: "unavailable", goal: null });
|
|
24
|
+
retain("other", "two");
|
|
25
|
+
retain("helper", "one", { helper: true });
|
|
26
|
+
retain("disconnected", "one", { available: false });
|
|
27
|
+
assert.deepEqual(await owner.readSessionGoal("one", "thread-one"), { status: "unavailable", goal: null });
|
|
28
|
+
assert.deepEqual(reads, []);
|
|
29
|
+
const interactive = retain("interactive", "one");
|
|
30
|
+
assert.deepEqual(await owner.readSessionGoal("one", "thread-one"), {
|
|
31
|
+
status: "available", threadId: "thread-one", goal: { status: "paused" }
|
|
32
|
+
});
|
|
33
|
+
assert.deepEqual(reads, [["interactive", "thread-one"]]);
|
|
34
|
+
const failure = new Error("Native goal read failed");
|
|
35
|
+
interactive.readGoal = async () => { throw failure; };
|
|
36
|
+
await assert.rejects(owner.readSessionGoal("one", "thread-one"), error => error === failure);
|
|
37
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
codexAppServerThreadHasReadableHistory,
|
|
5
|
+
deleteCodexAppServerThread,
|
|
6
|
+
deleteCodexAppServerHelperThread
|
|
7
|
+
} from "../src/server/conversation/codexProvider.js";
|
|
8
|
+
|
|
9
|
+
function invalidRequest(method) {
|
|
10
|
+
return Object.assign(new Error(`${method} rejected`), { code: -32600, method });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// The original Public lifecycle tests retain durable receipts, authority and
|
|
14
|
+
// retry assertions. These cases cover the native protocol branch moved here.
|
|
15
|
+
test("Helper deletion awaits the exact active-turn interruption before confirming deletion", async () => {
|
|
16
|
+
const calls = [];
|
|
17
|
+
const started = Promise.withResolvers();
|
|
18
|
+
const interrupted = Promise.withResolvers();
|
|
19
|
+
const deleted = { id: "helper-thread" };
|
|
20
|
+
const pending = deleteCodexAppServerHelperThread({
|
|
21
|
+
threadId: "helper-thread",
|
|
22
|
+
turnId: "helper-turn",
|
|
23
|
+
provider: {
|
|
24
|
+
async interruptTurn(threadId, turnId) {
|
|
25
|
+
calls.push(["interrupt", threadId, turnId]);
|
|
26
|
+
started.resolve();
|
|
27
|
+
await interrupted.promise;
|
|
28
|
+
},
|
|
29
|
+
async deleteThread(threadId) { calls.push(["delete", threadId]); return deleted; },
|
|
30
|
+
async readThread() { assert.fail("A confirmed native deletion does not need an absence probe"); }
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
await started.promise;
|
|
34
|
+
assert.deepEqual(calls, [["interrupt", "helper-thread", "helper-turn"]]);
|
|
35
|
+
interrupted.resolve();
|
|
36
|
+
assert.deepEqual(await pending, {
|
|
37
|
+
deleted: true, interrupted: true, ok: true, result: deleted,
|
|
38
|
+
status: "deleted", threadId: "helper-thread", turnId: "helper-turn", interruptError: null
|
|
39
|
+
});
|
|
40
|
+
assert.deepEqual(calls, [["interrupt", "helper-thread", "helper-turn"], ["delete", "helper-thread"]]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("Helper deletion retains interruption failure while allowing confirmed thread removal", async () => {
|
|
44
|
+
const failure = new Error("Interruption unavailable");
|
|
45
|
+
const result = await deleteCodexAppServerHelperThread({
|
|
46
|
+
threadId: "helper-thread",
|
|
47
|
+
turnId: "helper-turn",
|
|
48
|
+
provider: {
|
|
49
|
+
async interruptTurn() { throw failure; },
|
|
50
|
+
async deleteThread(threadId) { assert.equal(threadId, "helper-thread"); return {}; }
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
assert.equal(result.ok, true);
|
|
54
|
+
assert.equal(result.deleted, true);
|
|
55
|
+
assert.equal(result.interrupted, false);
|
|
56
|
+
assert.equal(result.interruptError, failure);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("Helper deletion accepts invalid-request absence only after the original read-back check", async () => {
|
|
60
|
+
const calls = [];
|
|
61
|
+
const result = await deleteCodexAppServerHelperThread({
|
|
62
|
+
threadId: "helper-thread",
|
|
63
|
+
provider: {
|
|
64
|
+
async deleteThread(id) { calls.push(["delete", id]); throw invalidRequest("thread/delete"); },
|
|
65
|
+
async readThread(id) { calls.push(["read", id]); throw invalidRequest("thread/read"); }
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
assert.deepEqual(calls, [["delete", "helper-thread"], ["read", "helper-thread"]]);
|
|
69
|
+
assert.deepEqual(result, {
|
|
70
|
+
deleted: false, interrupted: false, ok: true, status: "notFound",
|
|
71
|
+
threadId: "helper-thread", turnId: "", interruptError: null
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("Helper deletion keeps unproven native failures instead of acknowledging cleanup", async () => {
|
|
76
|
+
for (const outcome of ["readable", "read-failure", "wrong-delete-method", "delete-failure"]) {
|
|
77
|
+
let reads = 0;
|
|
78
|
+
const failure = outcome === "delete-failure" ? new Error("Delete transport disconnected")
|
|
79
|
+
: invalidRequest(outcome === "wrong-delete-method" ? "thread/resume" : "thread/delete");
|
|
80
|
+
const result = await deleteCodexAppServerHelperThread({
|
|
81
|
+
threadId: "helper-thread",
|
|
82
|
+
provider: {
|
|
83
|
+
async deleteThread() { throw failure; },
|
|
84
|
+
async readThread() {
|
|
85
|
+
reads += 1;
|
|
86
|
+
if (outcome === "read-failure") throw new Error("Read transport disconnected");
|
|
87
|
+
return { id: "helper-thread", turns: [] };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
assert.equal(result.ok, false, outcome);
|
|
92
|
+
assert.equal(result.error, failure, outcome);
|
|
93
|
+
assert.equal(result.interruptError, null, outcome);
|
|
94
|
+
assert.equal(reads, ["readable", "read-failure"].includes(outcome) ? 1 : 0, outcome);
|
|
95
|
+
assert.equal("deleted" in result, false, outcome);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("Helper deletion rejects unavailable or unconfirmed native deletion without a history fallback", async () => {
|
|
100
|
+
for (const value of [undefined, null, [], false]) {
|
|
101
|
+
const result = await deleteCodexAppServerHelperThread({
|
|
102
|
+
threadId: "helper-thread",
|
|
103
|
+
provider: {
|
|
104
|
+
async deleteThread() { return value; },
|
|
105
|
+
async readThread() { assert.fail("An invalid result is not native proof of absence"); }
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
assert.equal(result.ok, false);
|
|
109
|
+
assert.equal(result.error.code, "codex_helper_thread_delete_unconfirmed");
|
|
110
|
+
assert.equal(result.error.message, "Codex app-server returned an invalid thread deletion result.");
|
|
111
|
+
}
|
|
112
|
+
const unavailable = await deleteCodexAppServerHelperThread({
|
|
113
|
+
provider: {}, threadId: "helper-thread", turnId: "helper-turn"
|
|
114
|
+
});
|
|
115
|
+
assert.equal(unavailable.ok, false);
|
|
116
|
+
assert.equal(unavailable.error.message, "Codex provider cannot delete a helper thread.");
|
|
117
|
+
assert.equal(unavailable.interruptError.message, "Codex provider cannot interrupt an active helper turn.");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("the original readable-history classifier preserves normalization and unrelated errors", async () => {
|
|
121
|
+
const calls = [];
|
|
122
|
+
assert.equal(await codexAppServerThreadHasReadableHistory({
|
|
123
|
+
async readThread(id) { calls.push(id); return { turns: [] }; }
|
|
124
|
+
}, " helper-thread "), true);
|
|
125
|
+
assert.deepEqual(calls, ["helper-thread"]);
|
|
126
|
+
assert.equal(await codexAppServerThreadHasReadableHistory({
|
|
127
|
+
async readThread() { throw invalidRequest("thread/read"); }
|
|
128
|
+
}, "helper-thread"), false);
|
|
129
|
+
const failure = invalidRequest("thread/resume");
|
|
130
|
+
await assert.rejects(codexAppServerThreadHasReadableHistory({
|
|
131
|
+
async readThread() { throw failure; }
|
|
132
|
+
}, "helper-thread"), error => error === failure);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("direct native deletion validates its result without interrupting or reading history", async () => {
|
|
136
|
+
const calls = [];
|
|
137
|
+
const deleted = { id: "helper-thread" };
|
|
138
|
+
const provider = {
|
|
139
|
+
async deleteThread(threadId) { calls.push(threadId); return deleted; },
|
|
140
|
+
async interruptTurn() { assert.fail("Compensation has no active turn to interrupt"); },
|
|
141
|
+
async readThread() { assert.fail("Direct deletion does not substitute an absence proof"); }
|
|
142
|
+
};
|
|
143
|
+
assert.equal(await deleteCodexAppServerThread({ provider, threadId: "helper-thread" }), deleted);
|
|
144
|
+
assert.deepEqual(calls, ["helper-thread"]);
|
|
145
|
+
for (const value of [undefined, null, [], false]) {
|
|
146
|
+
await assert.rejects(deleteCodexAppServerThread({
|
|
147
|
+
provider: { ...provider, async deleteThread() { return value; } }, threadId: "helper-thread"
|
|
148
|
+
}), { message: "Codex app-server returned an invalid thread deletion result." });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("direct native deletion preserves invalid-request and transport errors without read-back", async () => {
|
|
153
|
+
for (const failure of [invalidRequest("thread/delete"), new Error("Delete transport disconnected")]) {
|
|
154
|
+
await assert.rejects(deleteCodexAppServerThread({
|
|
155
|
+
threadId: "helper-thread",
|
|
156
|
+
provider: {
|
|
157
|
+
async deleteThread(threadId) { assert.equal(threadId, "helper-thread"); throw failure; },
|
|
158
|
+
async interruptTurn() { assert.fail("Compensation must not interrupt"); },
|
|
159
|
+
async readThread() { assert.fail("Compensation must retain the original native failure"); }
|
|
160
|
+
}
|
|
161
|
+
}), error => error === failure);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { createCodexHelperThreadLedgerOwner } from "../src/server/conversation/codexHelperThreadLedger.js";
|
|
8
|
+
import { createCodexHelperThreadLifecycle } from "../src/server/conversation/codexHelperThreads.js";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION,
|
|
12
|
+
CODEX_HELPER_THREAD_LIFECYCLES,
|
|
13
|
+
codexHelperThreadRecordId,
|
|
14
|
+
createCodexHelperThreadLedger,
|
|
15
|
+
defineCodexHelperThreadRecord,
|
|
16
|
+
helperProfile
|
|
17
|
+
} from "./fixtures/codexHelperThreadLedger.js";
|
|
18
|
+
|
|
19
|
+
function ownershipRecord(root, overrides = {}) {
|
|
20
|
+
const now = new Date().toISOString();
|
|
21
|
+
return defineCodexHelperThreadRecord({
|
|
22
|
+
createdAt: now,
|
|
23
|
+
executionProfile: helperProfile(),
|
|
24
|
+
identity: {
|
|
25
|
+
providerId: "codex",
|
|
26
|
+
providerKeyFingerprint: `sha256:${"c".repeat(64)}`,
|
|
27
|
+
runtime: {
|
|
28
|
+
accountIdentitySignature: `sha256:${"a".repeat(64)}`,
|
|
29
|
+
authStateSignature: `v1:${"b".repeat(24)}`,
|
|
30
|
+
endpoint: `unix://${path.join(root, "codex.sock")}`,
|
|
31
|
+
executionMode: "interactive",
|
|
32
|
+
executionContextHash: "c".repeat(12),
|
|
33
|
+
provider: "codex_app_server",
|
|
34
|
+
runtimeDir: path.join(root, "codex-runtime"),
|
|
35
|
+
runtimesHash: "d".repeat(12),
|
|
36
|
+
terminalEnvHash: "e".repeat(12),
|
|
37
|
+
toolHomeSource: "",
|
|
38
|
+
transport: "unix"
|
|
39
|
+
},
|
|
40
|
+
server: {
|
|
41
|
+
userAgent: "vibe64/0.149.0 (ledger unit test)"
|
|
42
|
+
},
|
|
43
|
+
transportId: "codex_app_server"
|
|
44
|
+
},
|
|
45
|
+
lifecycle: CODEX_HELPER_THREAD_LIFECYCLES.READY,
|
|
46
|
+
ownershipId: "ownership-1",
|
|
47
|
+
projectContextRoot: path.join(root, "authority"),
|
|
48
|
+
projectRuntimeRoot: root,
|
|
49
|
+
revision: 1,
|
|
50
|
+
schemaVersion: CODEX_HELPER_THREAD_LEDGER_SCHEMA_VERSION,
|
|
51
|
+
sessionId: "session-1",
|
|
52
|
+
threadId: "thread-1",
|
|
53
|
+
turnId: "",
|
|
54
|
+
updatedAt: now,
|
|
55
|
+
workdir: path.join(root, "sessions", "active", "session-1", "source"),
|
|
56
|
+
...overrides
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("a standalone ledger codec supplies retained Helper control matching", () => {
|
|
61
|
+
const ledgerOwner = createCodexHelperThreadLedgerOwner({
|
|
62
|
+
executionProfile: input => ({ purpose: input.purpose, model: input.model })
|
|
63
|
+
});
|
|
64
|
+
const owner = createCodexHelperThreadLifecycle({
|
|
65
|
+
providerOwner: { providers: new Map() }, ledgerOwner
|
|
66
|
+
});
|
|
67
|
+
const provider = {};
|
|
68
|
+
const scope = { projectRuntimeRoot: "/runtime", sessionId: "session-1", threadId: "thread-1", provider, workdir: "/source" };
|
|
69
|
+
const record = { ...scope, lifecycle: CODEX_HELPER_THREAD_LIFECYCLES.READY,
|
|
70
|
+
executionProfile: { purpose: "summary", model: "selected-model" } };
|
|
71
|
+
owner.threads.set(owner.key(scope), record);
|
|
72
|
+
assert.equal(owner.threadForOperation({ ...scope,
|
|
73
|
+
executionProfile: { model: "selected-model", purpose: "summary", presentation: "ignored" }
|
|
74
|
+
}), record);
|
|
75
|
+
assert.throws(() => owner.threadForOperation({ ...scope,
|
|
76
|
+
executionProfile: { purpose: "different-task", model: "selected-model" }
|
|
77
|
+
}), { code: "codex_helper_thread_unavailable" });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
async function withLedger(operation) {
|
|
81
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "vibe64-helper-ledger-"));
|
|
82
|
+
try {
|
|
83
|
+
await operation({
|
|
84
|
+
ledger: createCodexHelperThreadLedger({ projectRuntimeRoot: root }),
|
|
85
|
+
root
|
|
86
|
+
});
|
|
87
|
+
} finally {
|
|
88
|
+
await rm(root, { force: true, recursive: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function deferred() {
|
|
93
|
+
let resolve;
|
|
94
|
+
const promise = new Promise((resolvePromise) => {
|
|
95
|
+
resolve = resolvePromise;
|
|
96
|
+
});
|
|
97
|
+
return { promise, resolve };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function isolatedLedgerModule(label = "ledger") {
|
|
101
|
+
const moduleUrl = new URL(
|
|
102
|
+
"./fixtures/codexHelperThreadLedger.js",
|
|
103
|
+
import.meta.url
|
|
104
|
+
);
|
|
105
|
+
moduleUrl.searchParams.set("test-instance", `${label}-${crypto.randomUUID()}`);
|
|
106
|
+
return import(moduleUrl.href);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
test("helper ownership ledger round-trips and enforces revision CAS", async () => {
|
|
110
|
+
await withLedger(async ({ ledger, root }) => {
|
|
111
|
+
const first = ownershipRecord(root);
|
|
112
|
+
await assert.rejects(
|
|
113
|
+
ledger.write(ownershipRecord(root, { revision: 2 })),
|
|
114
|
+
/must start at revision 1/u
|
|
115
|
+
);
|
|
116
|
+
assert.deepEqual(await ledger.write(first), first);
|
|
117
|
+
assert.deepEqual(await ledger.readAll(), {
|
|
118
|
+
failures: [],
|
|
119
|
+
records: [first]
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const second = ownershipRecord(root, {
|
|
123
|
+
createdAt: first.createdAt,
|
|
124
|
+
ownershipId: first.ownershipId,
|
|
125
|
+
revision: 2,
|
|
126
|
+
updatedAt: new Date(Date.parse(first.updatedAt) + 1).toISOString()
|
|
127
|
+
});
|
|
128
|
+
await assert.rejects(
|
|
129
|
+
ledger.write(ownershipRecord(root, {
|
|
130
|
+
createdAt: first.createdAt,
|
|
131
|
+
ownershipId: first.ownershipId,
|
|
132
|
+
revision: 3,
|
|
133
|
+
updatedAt: second.updatedAt
|
|
134
|
+
}), { expected: first }),
|
|
135
|
+
(error) => error.code === "vibe64_codex_helper_ledger_conflict"
|
|
136
|
+
);
|
|
137
|
+
await assert.rejects(
|
|
138
|
+
ledger.write(ownershipRecord(root, {
|
|
139
|
+
createdAt: first.createdAt,
|
|
140
|
+
identity: {
|
|
141
|
+
...first.identity,
|
|
142
|
+
providerKeyFingerprint: `sha256:${"d".repeat(64)}`
|
|
143
|
+
},
|
|
144
|
+
ownershipId: first.ownershipId,
|
|
145
|
+
revision: 2,
|
|
146
|
+
updatedAt: second.updatedAt
|
|
147
|
+
}), { expected: first }),
|
|
148
|
+
(error) => error.code === "vibe64_codex_helper_ledger_conflict"
|
|
149
|
+
);
|
|
150
|
+
assert.deepEqual(await ledger.write(second, { expected: first }), second);
|
|
151
|
+
await assert.rejects(
|
|
152
|
+
ledger.write(ownershipRecord(root, {
|
|
153
|
+
createdAt: first.createdAt,
|
|
154
|
+
ownershipId: first.ownershipId,
|
|
155
|
+
revision: 3,
|
|
156
|
+
updatedAt: new Date(Date.parse(first.updatedAt) + 2).toISOString()
|
|
157
|
+
}), { expected: first }),
|
|
158
|
+
(error) => error.code === "vibe64_codex_helper_ledger_conflict"
|
|
159
|
+
);
|
|
160
|
+
await assert.rejects(
|
|
161
|
+
ledger.remove(first),
|
|
162
|
+
(error) => error.code === "vibe64_codex_helper_ledger_conflict"
|
|
163
|
+
);
|
|
164
|
+
assert.equal(await ledger.remove(second), true);
|
|
165
|
+
await assert.rejects(
|
|
166
|
+
ledger.remove(second),
|
|
167
|
+
(error) => error.code === "vibe64_codex_helper_ledger_conflict"
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("helper ownership ledger serializes competing creates and updates", async () => {
|
|
173
|
+
await withLedger(async ({ ledger, root }) => {
|
|
174
|
+
const otherLedger = createCodexHelperThreadLedger({ projectRuntimeRoot: root });
|
|
175
|
+
const first = ownershipRecord(root);
|
|
176
|
+
const competing = ownershipRecord(root, { ownershipId: "ownership-2" });
|
|
177
|
+
const creates = await Promise.allSettled([
|
|
178
|
+
ledger.write(first),
|
|
179
|
+
otherLedger.write(competing)
|
|
180
|
+
]);
|
|
181
|
+
assert.equal(creates.filter(({ status }) => status === "fulfilled").length, 1);
|
|
182
|
+
assert.equal(creates.filter(({ status }) => status === "rejected").length, 1);
|
|
183
|
+
assert.equal(
|
|
184
|
+
creates.find(({ status }) => status === "rejected").reason.code,
|
|
185
|
+
"vibe64_codex_helper_ledger_conflict"
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const current = (await ledger.readAll()).records[0];
|
|
189
|
+
const update = (suffix) => ownershipRecord(root, {
|
|
190
|
+
createdAt: current.createdAt,
|
|
191
|
+
lifecycle: CODEX_HELPER_THREAD_LIFECYCLES.ACTIVE,
|
|
192
|
+
ownershipId: current.ownershipId,
|
|
193
|
+
revision: 2,
|
|
194
|
+
turnId: `turn-${suffix}`,
|
|
195
|
+
updatedAt: new Date(Date.parse(current.updatedAt) + Number(suffix)).toISOString()
|
|
196
|
+
});
|
|
197
|
+
const updates = await Promise.allSettled([
|
|
198
|
+
ledger.write(update(1), { expected: current }),
|
|
199
|
+
otherLedger.write(update(2), { expected: current })
|
|
200
|
+
]);
|
|
201
|
+
assert.equal(updates.filter(({ status }) => status === "fulfilled").length, 1);
|
|
202
|
+
assert.equal(updates.filter(({ status }) => status === "rejected").length, 1);
|
|
203
|
+
assert.equal(
|
|
204
|
+
updates.find(({ status }) => status === "rejected").reason.code,
|
|
205
|
+
"vibe64_codex_helper_ledger_conflict"
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("helper ownership ledger never overwrites or removes mismatched durable state", async () => {
|
|
211
|
+
await withLedger(async ({ ledger, root }) => {
|
|
212
|
+
const first = ownershipRecord(root);
|
|
213
|
+
await ledger.write(first);
|
|
214
|
+
const filePath = path.join(ledger.root, `${codexHelperThreadRecordId(first)}.json`);
|
|
215
|
+
const tampered = {
|
|
216
|
+
...first,
|
|
217
|
+
lifecycle: CODEX_HELPER_THREAD_LIFECYCLES.CLEANUP_REQUIRED
|
|
218
|
+
};
|
|
219
|
+
await writeFile(filePath, `${JSON.stringify(tampered, null, 2)}\n`, "utf8");
|
|
220
|
+
const second = ownershipRecord(root, {
|
|
221
|
+
createdAt: first.createdAt,
|
|
222
|
+
ownershipId: first.ownershipId,
|
|
223
|
+
revision: 2,
|
|
224
|
+
updatedAt: new Date(Date.parse(first.updatedAt) + 1).toISOString()
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await assert.rejects(
|
|
228
|
+
ledger.write(second, { expected: first }),
|
|
229
|
+
(error) => error.code === "vibe64_codex_helper_ledger_conflict"
|
|
230
|
+
);
|
|
231
|
+
await assert.rejects(
|
|
232
|
+
ledger.remove(first),
|
|
233
|
+
(error) => error.code === "vibe64_codex_helper_ledger_conflict"
|
|
234
|
+
);
|
|
235
|
+
assert.deepEqual(JSON.parse(await readFile(filePath, "utf8")), tampered);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("helper ownership ledger recovers a lock left by a dead process", async () => {
|
|
240
|
+
await withLedger(async ({ ledger, root }) => {
|
|
241
|
+
const record = ownershipRecord(root);
|
|
242
|
+
const locksRoot = path.join(ledger.root, ".locks");
|
|
243
|
+
const lockPath = path.join(locksRoot, "ledger.lock");
|
|
244
|
+
await mkdir(locksRoot, { recursive: true });
|
|
245
|
+
await writeFile(lockPath, `${JSON.stringify({
|
|
246
|
+
createdAt: new Date().toISOString(),
|
|
247
|
+
pid: 2_147_483_647,
|
|
248
|
+
processId: "dead-process",
|
|
249
|
+
token: "dead-lock"
|
|
250
|
+
})}\n`);
|
|
251
|
+
|
|
252
|
+
assert.deepEqual(await ledger.write(record), record);
|
|
253
|
+
assert.deepEqual(await readdir(locksRoot), []);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("helper ownership ledger serializes stale-lock recovery across module instances", async () => {
|
|
258
|
+
const root = await mkdtemp(path.join(os.tmpdir(), "vibe64-helper-ledger-race-"));
|
|
259
|
+
const staleVerified = deferred();
|
|
260
|
+
const releaseStaleRecovery = deferred();
|
|
261
|
+
const contenderBlocked = deferred();
|
|
262
|
+
let contenderAcquired = false;
|
|
263
|
+
let firstWrite = null;
|
|
264
|
+
let secondWrite = null;
|
|
265
|
+
try {
|
|
266
|
+
const [firstModule, secondModule] = await Promise.all([
|
|
267
|
+
isolatedLedgerModule("stale-recovery-a"),
|
|
268
|
+
isolatedLedgerModule("stale-recovery-b")
|
|
269
|
+
]);
|
|
270
|
+
const firstLedger = firstModule.createCodexHelperThreadLedger({
|
|
271
|
+
observeLock: async ({ stage }) => {
|
|
272
|
+
if (stage === "stale-verified") {
|
|
273
|
+
staleVerified.resolve();
|
|
274
|
+
await releaseStaleRecovery.promise;
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
projectRuntimeRoot: root
|
|
278
|
+
});
|
|
279
|
+
const secondLedger = secondModule.createCodexHelperThreadLedger({
|
|
280
|
+
observeLock: ({ stage }) => {
|
|
281
|
+
if (stage === "host-acquired") {
|
|
282
|
+
contenderAcquired = true;
|
|
283
|
+
}
|
|
284
|
+
if (stage === "host-contended") {
|
|
285
|
+
contenderBlocked.resolve();
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
projectRuntimeRoot: root
|
|
289
|
+
});
|
|
290
|
+
const locksRoot = path.join(firstLedger.root, ".locks");
|
|
291
|
+
const lockPath = path.join(locksRoot, "ledger.lock");
|
|
292
|
+
await mkdir(locksRoot, { recursive: true });
|
|
293
|
+
await writeFile(lockPath, `${JSON.stringify({
|
|
294
|
+
createdAt: new Date().toISOString(),
|
|
295
|
+
pid: 2_147_483_647,
|
|
296
|
+
processId: "dead-process",
|
|
297
|
+
token: "dead-lock"
|
|
298
|
+
})}\n`);
|
|
299
|
+
|
|
300
|
+
const record = ownershipRecord(root);
|
|
301
|
+
firstWrite = firstLedger.write(record);
|
|
302
|
+
let staleTimeout;
|
|
303
|
+
try {
|
|
304
|
+
await Promise.race([
|
|
305
|
+
staleVerified.promise,
|
|
306
|
+
new Promise((_, reject) => {
|
|
307
|
+
staleTimeout = setTimeout(
|
|
308
|
+
() => reject(new Error("First ledger did not reach stale-lock verification.")),
|
|
309
|
+
1000
|
|
310
|
+
);
|
|
311
|
+
}),
|
|
312
|
+
firstWrite.then(
|
|
313
|
+
() => Promise.reject(new Error("First ledger completed before stale-lock verification.")),
|
|
314
|
+
(error) => Promise.reject(error)
|
|
315
|
+
)
|
|
316
|
+
]);
|
|
317
|
+
} finally {
|
|
318
|
+
clearTimeout(staleTimeout);
|
|
319
|
+
}
|
|
320
|
+
secondWrite = secondLedger.write(record);
|
|
321
|
+
let contentionTimeout;
|
|
322
|
+
try {
|
|
323
|
+
await Promise.race([
|
|
324
|
+
contenderBlocked.promise,
|
|
325
|
+
new Promise((_, reject) => {
|
|
326
|
+
contentionTimeout = setTimeout(
|
|
327
|
+
() => reject(new Error("Competing ledger did not reach the host lock.")),
|
|
328
|
+
1000
|
|
329
|
+
);
|
|
330
|
+
})
|
|
331
|
+
]);
|
|
332
|
+
} finally {
|
|
333
|
+
clearTimeout(contentionTimeout);
|
|
334
|
+
}
|
|
335
|
+
assert.equal(contenderAcquired, false);
|
|
336
|
+
|
|
337
|
+
releaseStaleRecovery.resolve();
|
|
338
|
+
assert.deepEqual(await firstWrite, record);
|
|
339
|
+
await assert.rejects(
|
|
340
|
+
secondWrite,
|
|
341
|
+
(error) => error.code === "vibe64_codex_helper_ledger_conflict"
|
|
342
|
+
);
|
|
343
|
+
assert.equal(contenderAcquired, true);
|
|
344
|
+
assert.deepEqual(await readdir(locksRoot), []);
|
|
345
|
+
assert.equal((await readdir(root)).includes("3"), false);
|
|
346
|
+
} finally {
|
|
347
|
+
releaseStaleRecovery.resolve();
|
|
348
|
+
await Promise.allSettled([firstWrite, secondWrite].filter(Boolean));
|
|
349
|
+
await rm(root, { force: true, recursive: true });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("helper ownership ledger preserves malformed and oversized state as blockers", async () => {
|
|
354
|
+
await withLedger(async ({ ledger, root }) => {
|
|
355
|
+
const record = ownershipRecord(root);
|
|
356
|
+
await ledger.write(record);
|
|
357
|
+
const recordPath = path.join(ledger.root, `${codexHelperThreadRecordId(record)}.json`);
|
|
358
|
+
await writeFile(recordPath, "x".repeat((64 * 1024) + 1));
|
|
359
|
+
|
|
360
|
+
const listed = await ledger.readAll();
|
|
361
|
+
assert.equal(listed.records.length, 0);
|
|
362
|
+
assert.equal(listed.failures.length, 1);
|
|
363
|
+
assert.match(listed.failures[0].error, /exceeds 65536 bytes/u);
|
|
364
|
+
assert.equal((await readFile(recordPath, "utf8")).length, (64 * 1024) + 1);
|
|
365
|
+
|
|
366
|
+
const lockPath = path.join(
|
|
367
|
+
ledger.root,
|
|
368
|
+
".locks",
|
|
369
|
+
"ledger.lock"
|
|
370
|
+
);
|
|
371
|
+
await writeFile(lockPath, "not-json");
|
|
372
|
+
await assert.rejects(
|
|
373
|
+
ledger.write(ownershipRecord(root, { ownershipId: "ownership-2" })),
|
|
374
|
+
/lock cannot be verified/u
|
|
375
|
+
);
|
|
376
|
+
assert.equal(await readFile(lockPath, "utf8"), "not-json");
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("helper ownership records reject relative, non-normalized, and unbounded identity", async () => {
|
|
381
|
+
await withLedger(async ({ root }) => {
|
|
382
|
+
const valid = ownershipRecord(root);
|
|
383
|
+
assert.throws(
|
|
384
|
+
() => defineCodexHelperThreadRecord({ ...valid, workdir: "relative/source" }),
|
|
385
|
+
/normalized absolute path/u
|
|
386
|
+
);
|
|
387
|
+
assert.throws(
|
|
388
|
+
() => defineCodexHelperThreadRecord({
|
|
389
|
+
...valid,
|
|
390
|
+
projectContextRoot: `${root}/authority/../authority`
|
|
391
|
+
}),
|
|
392
|
+
/normalized absolute path/u
|
|
393
|
+
);
|
|
394
|
+
assert.throws(
|
|
395
|
+
() => defineCodexHelperThreadRecord({
|
|
396
|
+
...valid,
|
|
397
|
+
identity: {
|
|
398
|
+
...valid.identity,
|
|
399
|
+
runtime: {
|
|
400
|
+
...valid.identity.runtime,
|
|
401
|
+
endpoint: "x".repeat(8193)
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}),
|
|
405
|
+
/exceeds 8192 characters/u
|
|
406
|
+
);
|
|
407
|
+
assert.throws(
|
|
408
|
+
() => defineCodexHelperThreadRecord({
|
|
409
|
+
...valid,
|
|
410
|
+
identity: {
|
|
411
|
+
...valid.identity,
|
|
412
|
+
providerKeyFingerprint: "not-a-fingerprint"
|
|
413
|
+
}
|
|
414
|
+
}),
|
|
415
|
+
/must be a SHA-256 signature/u
|
|
416
|
+
);
|
|
417
|
+
assert.throws(
|
|
418
|
+
() => defineCodexHelperThreadRecord({
|
|
419
|
+
...valid,
|
|
420
|
+
identity: {
|
|
421
|
+
...valid.identity,
|
|
422
|
+
runtime: {
|
|
423
|
+
...valid.identity.runtime,
|
|
424
|
+
executionMode: "unsupported"
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}),
|
|
428
|
+
/managed Codex app-server/u
|
|
429
|
+
);
|
|
430
|
+
assert.throws(
|
|
431
|
+
() => defineCodexHelperThreadRecord({
|
|
432
|
+
...valid,
|
|
433
|
+
identity: {
|
|
434
|
+
...valid.identity,
|
|
435
|
+
runtime: {
|
|
436
|
+
...valid.identity.runtime,
|
|
437
|
+
authStateSignature: "credential-shaped-state"
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}),
|
|
441
|
+
/versioned state signature/u
|
|
442
|
+
);
|
|
443
|
+
assert.throws(
|
|
444
|
+
() => defineCodexHelperThreadRecord({
|
|
445
|
+
...valid,
|
|
446
|
+
identity: {
|
|
447
|
+
...valid.identity,
|
|
448
|
+
runtime: {
|
|
449
|
+
...valid.identity.runtime,
|
|
450
|
+
endpoint: "https://example.invalid/codex"
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}),
|
|
454
|
+
/managed Codex app-server/u
|
|
455
|
+
);
|
|
456
|
+
assert.doesNotThrow(
|
|
457
|
+
() => defineCodexHelperThreadRecord({
|
|
458
|
+
...valid,
|
|
459
|
+
identity: {
|
|
460
|
+
...valid.identity,
|
|
461
|
+
runtime: {
|
|
462
|
+
...valid.identity.runtime,
|
|
463
|
+
toolHomeSource: path.join(root, "credential-home")
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
);
|
|
468
|
+
assert.throws(
|
|
469
|
+
() => defineCodexHelperThreadRecord({
|
|
470
|
+
...valid,
|
|
471
|
+
updatedAt: new Date(Date.parse(valid.createdAt) - 1000).toISOString()
|
|
472
|
+
}),
|
|
473
|
+
/cannot precede createdAt/u
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test("helper ownership ledger bounds record inventory before parsing it", async () => {
|
|
479
|
+
await withLedger(async ({ ledger, root }) => {
|
|
480
|
+
await mkdir(ledger.root, { recursive: true });
|
|
481
|
+
await Promise.all(Array.from({ length: 1024 }, (_, index) => {
|
|
482
|
+
return writeFile(path.join(ledger.root, `${String(index).padStart(4, "0")}.json`), "{}");
|
|
483
|
+
}));
|
|
484
|
+
await assert.rejects(
|
|
485
|
+
ledger.write(ownershipRecord(root)),
|
|
486
|
+
/cannot exceed 1024 records/u
|
|
487
|
+
);
|
|
488
|
+
await writeFile(path.join(ledger.root, "overflow.json"), "{}");
|
|
489
|
+
const listed = await ledger.readAll();
|
|
490
|
+
assert.equal(listed.records.length, 0);
|
|
491
|
+
assert.equal(listed.failures.length, 1);
|
|
492
|
+
assert.match(listed.failures[0].error, /exceeds 1024 records/u);
|
|
493
|
+
});
|
|
494
|
+
});
|