@jskit-ai/assistant-core 0.1.212 → 0.1.214
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/package.json +5 -5
- package/fixtures/responsive-assistant/ConversationFixture.vue +7 -1
- package/package.json +5 -5
- package/src/client/conversation/AssistantConversationElement.vue +5 -1
- package/src/client/conversation/AssistantTranscript.vue +10 -1
- package/src/server/conversation/agentRun.js +38 -11
- package/src/server/conversation/claudeHistory.js +40 -5
- package/src/server/conversation/claudeProcess.js +28 -4
- package/src/server/conversation/claudeTools.js +11 -2
- package/src/server/conversation/claudeTurn.js +213 -23
- package/src/server/conversation/codexConfiguration.js +1 -1
- package/src/server/conversation/codexHistoryAdapter.js +11 -4
- package/src/server/conversation/codexNotificationQueue.js +11 -2
- package/src/server/conversation/codexObservationControl.js +8 -8
- package/src/server/conversation/codexOutput.js +20 -15
- package/src/server/conversation/codexProvider.js +40 -3
- package/src/server/conversation/codexProviderOwner.js +110 -1
- package/src/server/conversation/codexTurn.js +3 -0
- package/src/server/conversation/continuity.js +24 -6
- package/src/server/conversation/index.js +1 -1
- package/src/server/conversation/nativeHistoryExport.js +97 -0
- package/src/server/conversation/openCodeClient.js +47 -1
- package/src/server/conversation/openCodeRuntime.js +183 -28
- package/src/server/conversation/openCodeTurn.js +65 -24
- package/src/server/conversation/providers/api.js +7 -0
- package/src/server/conversation/providers/claudeDriver.js +52 -7
- package/src/server/conversation/providers/codex.js +19 -2
- package/src/server/conversation/providers/codexDriver.js +92 -14
- package/src/server/conversation/providers/opencodeDriver.js +31 -9
- package/src/server/conversation/runtime.js +424 -65
- package/src/server/conversation/runtimeStateUpgrade.js +77 -2
- package/src/server/conversation/streams.js +25 -5
- package/src/server/conversation/tools.js +2 -2
- package/src/server/conversation/transcript.js +23 -8
- package/src/server/hosts/claudeHistory.js +1 -0
- package/src/server/hosts/claudeProcess.js +1 -0
- package/src/server/hosts/codexProvider.js +3 -0
- package/src/server/hosts/nativeHistory.js +1 -0
- package/src/server/hosts/openCodeClient.js +1 -0
- package/src/server/hosts/openCodeTurn.js +1 -0
- package/src/server/index.js +1 -1
- package/src/server/lib/assistantToolLoop.js +99 -2
- package/test/aiConnectionClient.test.js +57 -0
- package/test/assistantBoundedToolLoop.test.js +332 -0
- package/test/assistantScroll.browser.test.js +151 -0
- package/test/claudeConversationSession.test.js +1417 -3
- package/test/claudeProcess.test.js +175 -0
- package/test/claudeTurn.test.js +466 -0
- package/test/codexConversationSession.test.js +1257 -12
- package/test/codexHistoryAdapter.test.js +74 -1
- package/test/codexTurn.test.js +282 -1
- package/test/conversation.test.js +83 -0
- package/test/conversationAgentRun.test.js +181 -0
- package/test/conversationRuntime.test.js +95 -0
- package/test/conversationRuntimeStateUpgrade.test.js +89 -0
- package/test/conversationStreaming.test.js +85 -0
- package/test/conversationTools.test.js +165 -1
- package/test/openCodeClient.test.js +186 -0
- package/test/openCodeRuntime.test.js +372 -0
- package/test/opencodeConversationSession.test.js +637 -5
|
@@ -230,7 +230,7 @@ async function fixture(t, options = {}) {
|
|
|
230
230
|
connections: { resolve: options.resolve || (async () => ({ providerId: "test", model: "test-model", sdkPackage: "@ai-sdk/openai-compatible",
|
|
231
231
|
modelLimits: { context: 32000, output: 4000 }, apiKey: key })) },
|
|
232
232
|
host: { workdir: directory, env: { ...process.env, UNRELATED_SECRET: "must-not-inherit" },
|
|
233
|
-
commands: { opencode: command }, execution: options.execution, nativeTools: options.nativeTools, commandWrapper: options.commandWrapper },
|
|
233
|
+
commands: { opencode: command }, execution: options.execution, nativeTools: options.nativeTools, commandWrapper: options.commandWrapper, opencode: options.opencode },
|
|
234
234
|
limits: { admissionTimeoutMs: 250, ...options.limits } });
|
|
235
235
|
runtimes.push(value);
|
|
236
236
|
return value;
|
|
@@ -242,7 +242,7 @@ async function fixture(t, options = {}) {
|
|
|
242
242
|
connections: { resolve: options.resolve || (async () => ({ providerId: "test", model: "test-model", sdkPackage: "@ai-sdk/openai-compatible",
|
|
243
243
|
modelLimits: { context: 32000, output: 4000 }, apiKey: key })) },
|
|
244
244
|
host: { workdir: directory, env: { ...process.env, UNRELATED_SECRET: "must-not-inherit" },
|
|
245
|
-
commands: { opencode: command }, execution: options.execution, nativeTools: options.nativeTools, commandWrapper: options.commandWrapper },
|
|
245
|
+
commands: { opencode: command }, execution: options.execution, nativeTools: options.nativeTools, commandWrapper: options.commandWrapper, opencode: options.opencode },
|
|
246
246
|
limits: { admissionTimeoutMs: 250, ...options.limits }
|
|
247
247
|
},
|
|
248
248
|
binding: () => storage.read("conversation", async tx => (await tx.readMetadata()).runtime.binding),
|
|
@@ -1056,7 +1056,7 @@ for (const savedStatus of ["cancelled", "failed", "unknown-tool", "interrupted"]
|
|
|
1056
1056
|
|
|
1057
1057
|
// Opt-in bound host uses the same native server, registry and Send/monitor owner.
|
|
1058
1058
|
// The original standalone fixture above remains byte-for-byte unchanged.
|
|
1059
|
-
async function boundToolFixture(t) {
|
|
1059
|
+
async function boundToolFixture(t, { runInContext, monitorRunInContext, monitorContext } = {}) {
|
|
1060
1060
|
const { createOpenCodeConversationServer, openCodeApplicationToolSchemas } = await import("../src/server/conversation/openCodeProcess.js");
|
|
1061
1061
|
const { createOpenCodeSharedRuntime, ensureOpenCodeSession } = await import("../src/server/conversation/openCodeRuntime.js");
|
|
1062
1062
|
const { observeOpenCodeEvents } = await import("../src/server/conversation/openCodeTurn.js");
|
|
@@ -1086,7 +1086,7 @@ async function boundToolFixture(t) {
|
|
|
1086
1086
|
const driver = createOpenCodeConversationDriver(f.driverOptions);
|
|
1087
1087
|
const handle = await driver.open({ onFailure() {}, conversation: {
|
|
1088
1088
|
publish() {}, native: { owner,
|
|
1089
|
-
acquire: async context => ({ key, options: context || {} }),
|
|
1089
|
+
acquire: async context => ({ key, options: context || {}, runInContext }),
|
|
1090
1090
|
preparation: {
|
|
1091
1091
|
cleanup: () => ({ sessionId: id, options: {}, application: {
|
|
1092
1092
|
closeTerminals: async () => ({ ok: true }), beforeRelease: async () => {},
|
|
@@ -1120,12 +1120,16 @@ async function boundToolFixture(t) {
|
|
|
1120
1120
|
prompt: { text: input.message }, beforeDispatch: threadId => input.onPromptSending({ threadId }) }),
|
|
1121
1121
|
commit: async admitted => { const turn = { turnId: input.messageId, messages: [{ role: "user", messageId: input.messageId, text: input.message }] }; receipts.push({ admitted, turn }); return turn; },
|
|
1122
1122
|
monitor: {
|
|
1123
|
+
runInContext: monitorRunInContext,
|
|
1123
1124
|
prepare: () => ({ fields: {} }),
|
|
1124
1125
|
create(target, turn, _metadata, options) { return {
|
|
1125
1126
|
observe: (_turn, signal) => observeOpenCodeEvents(target.server.client, target.upstreamSessionId,
|
|
1126
1127
|
{ signal, abortController: turn.abortController, eventStartedAt: turn.eventStartedAt }),
|
|
1127
1128
|
eventReady: options.eventReady, finalResponse: { agent: "jskit-assistant-actions", model: { id: "test-model", providerID: "test" }, recoveryMessageId: `msg_recovery_${nativeId}` },
|
|
1128
|
-
async writeRun() {
|
|
1129
|
+
async writeRun() { await monitorContext?.("writeRun"); },
|
|
1130
|
+
async projectMessages() { await monitorContext?.("projectMessages"); return { failure: "", providerApiFailure: false }; },
|
|
1131
|
+
completeResult: (_turn, result) => result.failure,
|
|
1132
|
+
onRetired() { monitorContext?.("onRetired"); }
|
|
1129
1133
|
}; }
|
|
1130
1134
|
}
|
|
1131
1135
|
}, project: value => value };
|
|
@@ -1340,3 +1344,631 @@ test("opted bound OpenCode does not abort newer same-turn input when an already
|
|
|
1340
1344
|
await peer.handle.cancel(); await waiting.completion;
|
|
1341
1345
|
}
|
|
1342
1346
|
});
|
|
1347
|
+
|
|
1348
|
+
// Observe the original standalone HTTP/plugin fixture and its canonical writer.
|
|
1349
|
+
// Completed rows always come from the native prompt handler, never a seeded turn.
|
|
1350
|
+
const completedOpenCodeValue = { kind: "tool", text: "Checking.", toolName: "numbers_read", arguments: "{}" };
|
|
1351
|
+
const completedOpenCodeSchema = { type: "object", additionalProperties: false,
|
|
1352
|
+
required: ["kind", "text", "toolName", "arguments"],
|
|
1353
|
+
properties: { kind: { type: "string", enum: ["reply", "tool"] }, text: { type: "string", maxLength: 64 },
|
|
1354
|
+
toolName: { type: "string", maxLength: 64 }, arguments: { type: "string", maxLength: 64 } } };
|
|
1355
|
+
async function completedOpenCodeFixture(t, options = {}) {
|
|
1356
|
+
const context = { actor: { id: "owner" }, surface: "app", permissions: ["numbers.read"] };
|
|
1357
|
+
let effects = 0;
|
|
1358
|
+
let accountResolutions = 0;
|
|
1359
|
+
const appendedRows = [];
|
|
1360
|
+
const actions = applicationActions(async () => { effects++; return { value: 42 }; });
|
|
1361
|
+
const f = await fixture(t, { ...options, context, actions,
|
|
1362
|
+
assistantResponses: options.assistantResponses || [{ text: JSON.stringify(completedOpenCodeValue) }],
|
|
1363
|
+
configuration: { ...configuration, outputSchema: completedOpenCodeSchema, ...options.configuration } });
|
|
1364
|
+
await f.first.close();
|
|
1365
|
+
const storage = { ...f.storage,
|
|
1366
|
+
write: (id, callback) => f.storage.write(id, transaction => callback({ ...transaction,
|
|
1367
|
+
async appendMessage(turnId, message) {
|
|
1368
|
+
await transaction.appendMessage(turnId, message);
|
|
1369
|
+
if (message.role === "system" && message.origin === "application") {
|
|
1370
|
+
appendedRows.push(structuredClone(await transaction.readTurn(turnId)));
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
})) };
|
|
1374
|
+
let runtime;
|
|
1375
|
+
let conversation;
|
|
1376
|
+
async function close() {
|
|
1377
|
+
if (runtime) { await runtime.close(); runtime = null; }
|
|
1378
|
+
}
|
|
1379
|
+
async function reopen() {
|
|
1380
|
+
await close();
|
|
1381
|
+
runtime = createConversationRuntime({ engine: "opencode", storage, actions, authorize: () => true,
|
|
1382
|
+
completedEnvelope: true, ...f.driverOptions,
|
|
1383
|
+
connections: { resolve: async input => { accountResolutions++; return f.driverOptions.connections.resolve(input); } } });
|
|
1384
|
+
try { conversation = await runtime.open({ id: "conversation", context }); }
|
|
1385
|
+
catch (error) { await close(); throw error; }
|
|
1386
|
+
return conversation;
|
|
1387
|
+
}
|
|
1388
|
+
await reopen();
|
|
1389
|
+
const controller = new AbortController();
|
|
1390
|
+
return { ...f, context, controller, appendedRows, actions, close, reopen,
|
|
1391
|
+
effects: () => effects, accountResolutions: () => accountResolutions,
|
|
1392
|
+
get conversation() { return conversation; },
|
|
1393
|
+
async complete(messageId = "completed-opencode-response") {
|
|
1394
|
+
const receipt = await conversation.wake({ messageId, text: "Read a number" });
|
|
1395
|
+
const state = await conversation.wait();
|
|
1396
|
+
const turn = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
1397
|
+
assert.equal(turn.metadata.runtime.status, "complete", state.error);
|
|
1398
|
+
assert.equal(turn.metadata.runtime.completedEnvelope, true);
|
|
1399
|
+
assert.equal(turn.assistant.text, JSON.stringify(completedOpenCodeValue));
|
|
1400
|
+
return { messageId, turnId: receipt.turnId };
|
|
1401
|
+
},
|
|
1402
|
+
prepare: receipt => conversation.prepareCompletedResponse(receipt, { signal: controller.signal }) };
|
|
1403
|
+
}
|
|
1404
|
+
const executeCompletedOpenCode = (prepared, argumentsText = "{}") => prepared.tools.execute({
|
|
1405
|
+
id: prepared.toolCallId, name: "numbers_read", arguments: argumentsText
|
|
1406
|
+
});
|
|
1407
|
+
|
|
1408
|
+
test("completed OpenCode canonical admission preserves its trusted marker and actual native association", async t => {
|
|
1409
|
+
const f = await completedOpenCodeFixture(t);
|
|
1410
|
+
try {
|
|
1411
|
+
const receipt = await f.complete();
|
|
1412
|
+
assert.equal(f.appendedRows.length, 1);
|
|
1413
|
+
const admitted = f.appendedRows[0];
|
|
1414
|
+
assert.equal(admitted.turnId, receipt.turnId);
|
|
1415
|
+
assert.equal(admitted.system.messageId, receipt.messageId);
|
|
1416
|
+
assert.equal(admitted.system.origin, "application");
|
|
1417
|
+
assert.equal(admitted.user, null);
|
|
1418
|
+
assert.equal(admitted.metadata.runtime.completedEnvelope, true);
|
|
1419
|
+
assert.equal(admitted.metadata.runtime.status, "running");
|
|
1420
|
+
const canonical = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
1421
|
+
assert.equal(canonical.system.messageId, admitted.system.messageId);
|
|
1422
|
+
assert.equal(canonical.metadata.runtime.segmentId, admitted.metadata.runtime.segmentId);
|
|
1423
|
+
assert.equal(canonical.metadata.runtime.engine, "opencode");
|
|
1424
|
+
assert.equal(canonical.metadata.runtime.status, "complete");
|
|
1425
|
+
assert.equal(canonical.metadata.runtime.completedEnvelope, true);
|
|
1426
|
+
const binding = await f.binding();
|
|
1427
|
+
assert.match(binding.sessionId, /^ses_/);
|
|
1428
|
+
assert.equal(typeof binding.accountIdentity, "string");
|
|
1429
|
+
assert.ok(binding.accountIdentity);
|
|
1430
|
+
const trace = await f.trace();
|
|
1431
|
+
const prompts = trace.filter(row => row.url?.endsWith("/prompt_async"));
|
|
1432
|
+
assert.equal(prompts.length, 1);
|
|
1433
|
+
assert.equal(prompts[0].url, `/session/${binding.sessionId}/prompt_async`);
|
|
1434
|
+
assert.equal(prompts[0].body.agent, "jskit-assistant");
|
|
1435
|
+
assert.equal(prompts[0].body.model.modelID, "test-model");
|
|
1436
|
+
assert.match(prompts[0].body.parts[0].text, /Return only one JSON value matching this JSON Schema/);
|
|
1437
|
+
const sessions = JSON.parse(await readFile(binding.databasePath, "utf8"));
|
|
1438
|
+
const native = sessions.find(row => row.id === binding.sessionId);
|
|
1439
|
+
assert.ok(native);
|
|
1440
|
+
const authored = native.messages.find(row => row.info.role === "user" && row.info.id === prompts[0].body.messageID);
|
|
1441
|
+
assert.ok(authored, "The native owner recorded the actual admitted prompt ID");
|
|
1442
|
+
const answer = native.messages.find(row => row.info.role === "assistant" && row.info.parentID === authored.info.id);
|
|
1443
|
+
assert.ok(answer);
|
|
1444
|
+
assert.equal(answer.info.finish, "stop");
|
|
1445
|
+
assert.equal(answer.parts[0].text, JSON.stringify(completedOpenCodeValue));
|
|
1446
|
+
assert.equal(canonical.assistant.messageId, `${receipt.turnId}:${answer.info.id}:assistant`);
|
|
1447
|
+
const prepared = await f.prepare(receipt);
|
|
1448
|
+
assert.equal(prepared.text, canonical.assistant.text);
|
|
1449
|
+
assert.equal(prepared.toolCallId, `${receipt.messageId}:operation`);
|
|
1450
|
+
assert.equal(f.effects(), 0);
|
|
1451
|
+
assert.equal((await f.trace()).filter(row => row.url === "/api/session").length, 1);
|
|
1452
|
+
} finally { await f.close(); }
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
test("completed OpenCode prepared handles and reopen retain one durable effect without native replay", async t => {
|
|
1456
|
+
const f = await completedOpenCodeFixture(t);
|
|
1457
|
+
try {
|
|
1458
|
+
const receipt = await f.complete();
|
|
1459
|
+
const originalBinding = await f.binding();
|
|
1460
|
+
const readsBefore = (await f.trace()).filter(row => row.url?.includes("/message")).length;
|
|
1461
|
+
const one = await f.prepare(receipt), two = await f.prepare(receipt);
|
|
1462
|
+
assert.equal(one.text, JSON.stringify(completedOpenCodeValue));
|
|
1463
|
+
assert.equal(one.toolCallId, `${receipt.messageId}:operation`);
|
|
1464
|
+
assert.equal(two.toolCallId, one.toolCallId);
|
|
1465
|
+
const [first, second] = await Promise.all([executeCompletedOpenCode(one), executeCompletedOpenCode(two)]);
|
|
1466
|
+
assert.equal(first.ok, true);
|
|
1467
|
+
assert.deepEqual(second, first);
|
|
1468
|
+
assert.equal(f.effects(), 1);
|
|
1469
|
+
await assert.rejects(executeCompletedOpenCode(two, '{"changed":true}'), /different arguments/);
|
|
1470
|
+
const saved = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
1471
|
+
assert.equal(saved.metadata.applicationTools.length, 1);
|
|
1472
|
+
assert.equal(saved.metadata.applicationTools[0].id, one.toolCallId);
|
|
1473
|
+
assert.equal(saved.metadata.applicationTools[0].status, "complete");
|
|
1474
|
+
await f.reopen();
|
|
1475
|
+
assert.deepEqual(await executeCompletedOpenCode(await f.prepare(receipt)), first);
|
|
1476
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId)), saved);
|
|
1477
|
+
const restored = await f.binding();
|
|
1478
|
+
for (const key of ["sessionId", "accountIdentity", "workdir", "directory", "databasePath", "runtimeDirectory"]) {
|
|
1479
|
+
assert.equal(restored[key], originalBinding[key]);
|
|
1480
|
+
}
|
|
1481
|
+
assert.equal(f.effects(), 1);
|
|
1482
|
+
const trace = await f.trace();
|
|
1483
|
+
assert.ok(trace.filter(row => row.url?.includes("/message")).length >= readsBefore + 4,
|
|
1484
|
+
"Both prepared handles and the cold owner inspect the real native input history");
|
|
1485
|
+
assert.equal(trace.filter(row => row.url?.endsWith("/prompt_async")).length, 1);
|
|
1486
|
+
assert.equal(trace.filter(row => row.url === "/api/session").length, 1);
|
|
1487
|
+
} finally { await f.close(); }
|
|
1488
|
+
});
|
|
1489
|
+
|
|
1490
|
+
test("completed OpenCode cached bindings cannot mask missing or changed persisted native fingerprints", async t => {
|
|
1491
|
+
const f = await completedOpenCodeFixture(t);
|
|
1492
|
+
try {
|
|
1493
|
+
const receipt = await f.complete();
|
|
1494
|
+
const prepared = await f.prepare(receipt);
|
|
1495
|
+
const before = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
1496
|
+
for (const missing of [true, false]) {
|
|
1497
|
+
const changed = structuredClone(before);
|
|
1498
|
+
if (missing) delete changed.runtime.binding.accountIdentity;
|
|
1499
|
+
else changed.runtime.binding.accountIdentity = "different-persisted-fixture-identity";
|
|
1500
|
+
await f.storage.write("conversation", tx => tx.writeMetadata(changed));
|
|
1501
|
+
try {
|
|
1502
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1503
|
+
await assert.rejects(f.prepare(receipt), missing ? /no saved account fingerprint/ : /different account binding/);
|
|
1504
|
+
await assert.rejects(executeCompletedOpenCode(prepared), /identity changed|no saved account fingerprint|different account binding/);
|
|
1505
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readMetadata()), changed);
|
|
1506
|
+
}
|
|
1507
|
+
} finally { await f.storage.write("conversation", tx => tx.writeMetadata(before)); }
|
|
1508
|
+
}
|
|
1509
|
+
assert.equal(f.effects(), 0);
|
|
1510
|
+
assert.equal((await f.trace()).filter(row => row.url?.endsWith("/prompt_async")).length, 1);
|
|
1511
|
+
assert.equal((await f.trace()).filter(row => row.url === "/api/session").length, 1);
|
|
1512
|
+
} finally { await f.close(); }
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
test("completed OpenCode freshly resolved accounts cannot inherit cached completion authority", async t => {
|
|
1516
|
+
const f = await completedOpenCodeFixture(t);
|
|
1517
|
+
try {
|
|
1518
|
+
const receipt = await f.complete();
|
|
1519
|
+
const prepared = await f.prepare(receipt);
|
|
1520
|
+
const before = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
1521
|
+
const resolutions = f.accountResolutions();
|
|
1522
|
+
f.setKey("changed-opencode-fixture-account");
|
|
1523
|
+
try {
|
|
1524
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1525
|
+
await assert.rejects(f.prepare(receipt), /different account binding/);
|
|
1526
|
+
await assert.rejects(executeCompletedOpenCode(prepared), /different account binding/);
|
|
1527
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readMetadata()), before);
|
|
1528
|
+
}
|
|
1529
|
+
assert.ok(f.accountResolutions() >= resolutions + 4, "Every preparation and effect checks the original fresh connection resolver");
|
|
1530
|
+
assert.equal(f.effects(), 0);
|
|
1531
|
+
assert.equal((await f.trace()).filter(row => row.url?.endsWith("/prompt_async")).length, 1);
|
|
1532
|
+
assert.equal((await f.trace()).filter(row => row.url === "/api/session").length, 1);
|
|
1533
|
+
} finally { f.setKey("test-key"); }
|
|
1534
|
+
} finally { await f.close(); }
|
|
1535
|
+
});
|
|
1536
|
+
|
|
1537
|
+
test("completed OpenCode requires its actual admitted native input on cold inspection", async t => {
|
|
1538
|
+
const f = await completedOpenCodeFixture(t);
|
|
1539
|
+
try {
|
|
1540
|
+
const receipt = await f.complete();
|
|
1541
|
+
await f.prepare(receipt);
|
|
1542
|
+
const canonical = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
1543
|
+
const binding = await f.binding();
|
|
1544
|
+
const prompt = (await f.trace()).find(row => row.url?.endsWith("/prompt_async"));
|
|
1545
|
+
await f.close();
|
|
1546
|
+
const nativeSource = await readFile(binding.databasePath, "utf8");
|
|
1547
|
+
const sessions = JSON.parse(nativeSource);
|
|
1548
|
+
const session = sessions.find(row => row.id === binding.sessionId);
|
|
1549
|
+
const accepted = session.messages.filter(row => row.info.role === "user" && row.info.id === prompt.body.messageID);
|
|
1550
|
+
assert.equal(accepted.length, 1, "Remove only evidence that the original native prompt handler actually wrote");
|
|
1551
|
+
session.messages = session.messages.filter(row => row !== accepted[0]);
|
|
1552
|
+
await writeFile(binding.databasePath, JSON.stringify(sessions));
|
|
1553
|
+
try {
|
|
1554
|
+
const readsBefore = (await f.trace()).filter(row => row.url?.includes("/message")).length;
|
|
1555
|
+
// If the original cold owner refuses before inspection, keep that real
|
|
1556
|
+
// failure visible; do not seed or replace a native/canonical completion.
|
|
1557
|
+
await f.reopen();
|
|
1558
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1559
|
+
await assert.rejects(f.prepare(receipt), /could not verify this response association/);
|
|
1560
|
+
}
|
|
1561
|
+
assert.ok((await f.trace()).filter(row => row.url?.includes("/message")).length >= readsBefore + 2);
|
|
1562
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId)), canonical);
|
|
1563
|
+
assert.equal(f.effects(), 0);
|
|
1564
|
+
assert.equal((await f.trace()).filter(row => row.url?.endsWith("/prompt_async")).length, 1);
|
|
1565
|
+
assert.equal((await f.trace()).filter(row => row.url === "/api/session").length, 1);
|
|
1566
|
+
} finally { await f.close(); await writeFile(binding.databasePath, nativeSource); }
|
|
1567
|
+
} finally { await f.close(); }
|
|
1568
|
+
});
|
|
1569
|
+
|
|
1570
|
+
test("completed OpenCode response custody refuses foreign native conversation and host scope before effects", async t => {
|
|
1571
|
+
const f = await completedOpenCodeFixture(t);
|
|
1572
|
+
try {
|
|
1573
|
+
const receipt = await f.complete();
|
|
1574
|
+
const prepared = await f.prepare(receipt);
|
|
1575
|
+
const before = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
1576
|
+
const canonical = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
1577
|
+
for (const [key, value] of [
|
|
1578
|
+
["sessionId", "ses_foreign_fixture_session"], ["workdir", "/different-fixture-workdir"],
|
|
1579
|
+
["directory", "/different-fixture-directory"], ["databasePath", "/different-fixture-history"],
|
|
1580
|
+
["runtimeDirectory", "/different-fixture-runtime"]
|
|
1581
|
+
]) {
|
|
1582
|
+
const changed = structuredClone(before);
|
|
1583
|
+
changed.runtime.binding[key] = value;
|
|
1584
|
+
await f.storage.write("conversation", tx => tx.writeMetadata(changed));
|
|
1585
|
+
try {
|
|
1586
|
+
await assert.rejects(f.prepare(receipt), /different native conversation or host scope/);
|
|
1587
|
+
await assert.rejects(executeCompletedOpenCode(prepared), /identity changed|different native conversation or host scope/);
|
|
1588
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readMetadata()), changed);
|
|
1589
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId)), canonical);
|
|
1590
|
+
} finally { await f.storage.write("conversation", tx => tx.writeMetadata(before)); }
|
|
1591
|
+
}
|
|
1592
|
+
assert.equal(f.effects(), 0);
|
|
1593
|
+
assert.equal((await f.trace()).filter(row => row.url?.endsWith("/prompt_async")).length, 1);
|
|
1594
|
+
assert.equal((await f.trace()).filter(row => row.url === "/api/session").length, 1);
|
|
1595
|
+
} finally { await f.close(); }
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
test("completed OpenCode caller markers preserve ordinary native discovery without completion authority", async t => {
|
|
1599
|
+
const f = await completedOpenCodeFixture(t, { assistantResponses: [], configuration: { outputSchema: undefined } });
|
|
1600
|
+
try {
|
|
1601
|
+
const receipt = await f.conversation.send({ messageId: "ordinary-opencode", text: "tools", data: { completedEnvelope: true } });
|
|
1602
|
+
const state = await f.conversation.wait();
|
|
1603
|
+
const turn = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
1604
|
+
assert.equal(turn.metadata.runtime.status, "complete", state.error);
|
|
1605
|
+
assert.equal(turn.metadata.runtime.completedEnvelope, undefined);
|
|
1606
|
+
assert.equal(f.effects(), 0, "Caller data is not a completed-envelope effect authorization");
|
|
1607
|
+
const initialTrace = await f.trace();
|
|
1608
|
+
const ordinaryPrompt = initialTrace.find(row => row.url?.endsWith("/prompt_async"));
|
|
1609
|
+
assert.equal(ordinaryPrompt.body.agent, "jskit-assistant-actions");
|
|
1610
|
+
assert.match(ordinaryPrompt.body.parts[0].text, /\[Application data\]/);
|
|
1611
|
+
assert.deepEqual(initialTrace[0].configuration.agent[ordinaryPrompt.body.agent].permission, {
|
|
1612
|
+
"*": "ask", assistant_action_search: "allow", assistant_action_contract: "allow", assistant_action_execute: "allow"
|
|
1613
|
+
});
|
|
1614
|
+
await assert.rejects(f.prepare({ messageId: "ordinary-opencode", turnId: receipt.turnId }), /no current verified receipt/);
|
|
1615
|
+
// The original native fixture invokes discovery only for exact plain tools.
|
|
1616
|
+
const toolReceipt = await f.conversation.send({ messageId: "ordinary-opencode-tools", text: "tools" });
|
|
1617
|
+
const toolsState = await f.conversation.wait();
|
|
1618
|
+
const toolsTurn = await f.storage.read("conversation", tx => tx.readTurn(toolReceipt.turnId));
|
|
1619
|
+
assert.equal(toolsTurn.metadata.runtime.status, "complete", toolsState.error);
|
|
1620
|
+
assert.equal(toolsTurn.metadata.runtime.completedEnvelope, undefined);
|
|
1621
|
+
assert.equal(toolsTurn.metadata.applicationTools.length, 3);
|
|
1622
|
+
assert.equal(f.effects(), 1);
|
|
1623
|
+
assert.equal(JSON.parse(toolsTurn.assistant.text).result.result.value, 42);
|
|
1624
|
+
await assert.rejects(f.prepare({ messageId: "ordinary-opencode-tools", turnId: toolReceipt.turnId }), /no current verified receipt/);
|
|
1625
|
+
const trace = await f.trace();
|
|
1626
|
+
const prompts = trace.filter(row => row.url?.endsWith("/prompt_async"));
|
|
1627
|
+
assert.equal(prompts.length, 2);
|
|
1628
|
+
assert.equal(prompts[1].body.parts[0].text, "tools");
|
|
1629
|
+
assert.equal(trace.filter(row => row.url === "/api/session").length, 1);
|
|
1630
|
+
} finally { await f.close(); }
|
|
1631
|
+
});
|
|
1632
|
+
|
|
1633
|
+
|
|
1634
|
+
test("completed OpenCode prepared reply preserves native receipt and publishes one plain final across reopen", async t => {
|
|
1635
|
+
const value = { kind: "reply", text: " Verified OpenCode reply. ", toolName: "", arguments: "" };
|
|
1636
|
+
const f = await completedOpenCodeFixture(t, { assistantResponses: [{ text: JSON.stringify(value) }] });
|
|
1637
|
+
try {
|
|
1638
|
+
const { createConversationTranscript } = await import("../src/server/conversation/transcript.js");
|
|
1639
|
+
const transcript = createConversationTranscript({ storage: f.storage });
|
|
1640
|
+
const plain = await transcript.writeConversationUserMessage("conversation", {
|
|
1641
|
+
messageId: "opencode-product-user", text: "Actual product request", data: { original: true }
|
|
1642
|
+
});
|
|
1643
|
+
const receipt = await f.conversation.wake({ messageId: "opencode-internal-reply", text: "Give a reply" },
|
|
1644
|
+
{ excludedMessageIds: [plain.user.messageId] });
|
|
1645
|
+
const state = await f.conversation.wait();
|
|
1646
|
+
assert.equal(state.status, "ready", state.error);
|
|
1647
|
+
const raw = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
1648
|
+
assert.equal(raw.metadata.runtime.status, "complete");
|
|
1649
|
+
assert.equal(raw.metadata.runtime.completedEnvelope, true);
|
|
1650
|
+
assert.equal(raw.assistant.text, JSON.stringify(value));
|
|
1651
|
+
const binding = await f.binding();
|
|
1652
|
+
const nativeBefore = JSON.parse(await readFile(binding.databasePath, "utf8")).find(row => row.id === binding.sessionId);
|
|
1653
|
+
const prompt = (await f.trace()).find(row => row.url?.endsWith("/prompt_async"));
|
|
1654
|
+
const authored = nativeBefore.messages.find(row => row.info.role === "user" && row.info.id === prompt.body.messageID);
|
|
1655
|
+
assert.ok(authored);
|
|
1656
|
+
const answer = nativeBefore.messages.find(row => row.info.role === "assistant" && row.info.parentID === authored.info.id);
|
|
1657
|
+
assert.ok(answer);
|
|
1658
|
+
assert.equal(answer.info.finish, "stop");
|
|
1659
|
+
assert.equal(answer.parts[0].text, JSON.stringify(value));
|
|
1660
|
+
assert.equal(raw.assistant.messageId, `${receipt.turnId}:${answer.info.id}:assistant`);
|
|
1661
|
+
const before = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
1662
|
+
const first = await f.prepare(receipt), second = await f.prepare(receipt);
|
|
1663
|
+
const [one, two] = await Promise.all([first.publishReply({ turnId: plain.turnId }), second.publishReply({ turnId: plain.turnId })]);
|
|
1664
|
+
assert.deepEqual(two, one);
|
|
1665
|
+
assert.equal(one.assistant.text, "Verified OpenCode reply.");
|
|
1666
|
+
assert.deepEqual(one.user, plain.user);
|
|
1667
|
+
assert.equal(one.metadata?.runtime?.engine, undefined);
|
|
1668
|
+
const { conversationMessageIdentity, conversationMessageVersion } = await import("../src/server/conversation/continuity.js");
|
|
1669
|
+
const assistantIdentity = conversationMessageIdentity(plain.turnId, one.assistant);
|
|
1670
|
+
const after = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
1671
|
+
assert.deepEqual(after.runtime.seen, { ...before.runtime.seen,
|
|
1672
|
+
[assistantIdentity]: conversationMessageVersion(one.assistant, { includeData: true }) });
|
|
1673
|
+
const savedRaw = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
1674
|
+
assert.deepEqual(savedRaw, { ...raw, metadata: { ...raw.metadata,
|
|
1675
|
+
runtime: { ...raw.metadata.runtime, publishedReplyTurnId: plain.turnId } } });
|
|
1676
|
+
await f.reopen();
|
|
1677
|
+
assert.deepEqual(await (await f.prepare(receipt)).publishReply({ turnId: plain.turnId }), one);
|
|
1678
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId)), savedRaw);
|
|
1679
|
+
assert.deepEqual((await f.storage.read("conversation", tx => tx.readMetadata())).runtime.seen, after.runtime.seen);
|
|
1680
|
+
const restored = await f.binding();
|
|
1681
|
+
for (const key of ["sessionId", "accountIdentity", "workdir", "directory", "databasePath", "runtimeDirectory"]) {
|
|
1682
|
+
assert.equal(restored[key], binding[key]);
|
|
1683
|
+
}
|
|
1684
|
+
assert.deepEqual(JSON.parse(await readFile(restored.databasePath, "utf8")).find(row => row.id === restored.sessionId).messages,
|
|
1685
|
+
nativeBefore.messages, "Publication and cold replay preserve the actual native input/answer identities and history");
|
|
1686
|
+
assert.deepEqual((await f.conversation.read()).conversationLog.map(turn => turn.assistant?.text), ["Verified OpenCode reply."]);
|
|
1687
|
+
const trace = await f.trace();
|
|
1688
|
+
assert.equal(trace.filter(row => row.url?.endsWith("/prompt_async")).length, 1);
|
|
1689
|
+
assert.equal(trace.filter(row => row.url === "/api/session").length, 1);
|
|
1690
|
+
assert.equal(f.effects(), 0);
|
|
1691
|
+
} finally { await f.close(); }
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
test("completed OpenCode prepared reply rechecks the original fresh account before publication", async t => {
|
|
1695
|
+
const value = { kind: "reply", text: "Verified OpenCode reply.", toolName: "", arguments: "" };
|
|
1696
|
+
const f = await completedOpenCodeFixture(t, { assistantResponses: [{ text: JSON.stringify(value) }] });
|
|
1697
|
+
try {
|
|
1698
|
+
const { createConversationTranscript } = await import("../src/server/conversation/transcript.js");
|
|
1699
|
+
const transcript = createConversationTranscript({ storage: f.storage });
|
|
1700
|
+
const plain = await transcript.writeConversationUserMessage("conversation", {
|
|
1701
|
+
messageId: "opencode-refused-product-user", text: "Actual product request"
|
|
1702
|
+
});
|
|
1703
|
+
const receipt = await f.conversation.wake({ messageId: "opencode-account-reply", text: "Give a reply" },
|
|
1704
|
+
{ excludedMessageIds: [plain.user.messageId] });
|
|
1705
|
+
const state = await f.conversation.wait();
|
|
1706
|
+
assert.equal(state.status, "ready", state.error);
|
|
1707
|
+
const raw = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
1708
|
+
assert.equal(raw.metadata.runtime.status, "complete");
|
|
1709
|
+
assert.equal(raw.assistant.text, JSON.stringify(value));
|
|
1710
|
+
const prepared = await f.prepare(receipt);
|
|
1711
|
+
const before = await f.storage.read("conversation", tx => tx.readMetadata());
|
|
1712
|
+
const resolutions = f.accountResolutions();
|
|
1713
|
+
f.setKey("changed-opencode-fixture-account");
|
|
1714
|
+
try {
|
|
1715
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1716
|
+
await assert.rejects(prepared.publishReply({ turnId: plain.turnId }), /different account binding/);
|
|
1717
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(plain.turnId)), plain);
|
|
1718
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId)), raw);
|
|
1719
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readMetadata()), before);
|
|
1720
|
+
}
|
|
1721
|
+
assert.ok(f.accountResolutions() >= resolutions + 2, "Publication uses the actual fresh connection resolver, not the prepared cached account");
|
|
1722
|
+
const trace = await f.trace();
|
|
1723
|
+
assert.equal(trace.filter(row => row.url?.endsWith("/prompt_async")).length, 1);
|
|
1724
|
+
assert.equal(trace.filter(row => row.url === "/api/session").length, 1);
|
|
1725
|
+
assert.equal(f.effects(), 0);
|
|
1726
|
+
} finally { f.setKey("test-key"); }
|
|
1727
|
+
} finally { await f.close(); }
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
|
|
1731
|
+
// Preserve the original fixed-envelope decoded limits at the native wire boundary.
|
|
1732
|
+
const completedOpenCodeWireSchema = { type: "object", additionalProperties: false,
|
|
1733
|
+
required: ["kind", "text", "toolName", "arguments"], properties: {
|
|
1734
|
+
kind: { type: "string", enum: ["reply", "tool"] }, text: { type: "string", maxLength: 16000 },
|
|
1735
|
+
toolName: { type: "string", maxLength: 256 }, arguments: { type: "string", maxLength: 262144 }
|
|
1736
|
+
} };
|
|
1737
|
+
|
|
1738
|
+
for (const kind of ["tool", "reply"]) test(`completed OpenCode ${kind} wire retains large decoded fields through native storage and preparation`, async t => {
|
|
1739
|
+
const handover = "😀".repeat(20000);
|
|
1740
|
+
const argumentsText = JSON.stringify({ value: handover }).replace(/[\u0080-\uffff]/g,
|
|
1741
|
+
character => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`);
|
|
1742
|
+
const value = kind === "tool" ? { kind, text: "", toolName: "numbers_read", arguments: argumentsText }
|
|
1743
|
+
: { kind, text: "\u0000".repeat(16000), toolName: "", arguments: "" };
|
|
1744
|
+
const raw = JSON.stringify(value);
|
|
1745
|
+
assert.ok(raw.length > (kind === "tool" ? 280000 : 16000));
|
|
1746
|
+
if (kind === "tool") assert.equal(value.arguments.length, 240012);
|
|
1747
|
+
else assert.equal(value.text.length, 16000);
|
|
1748
|
+
const f = await completedOpenCodeFixture(t, { assistantResponses: [{ text: raw }],
|
|
1749
|
+
configuration: { outputSchema: completedOpenCodeWireSchema },
|
|
1750
|
+
limits: { maxFinalReplyCharacters: 16000, maxOutputCharacters: 1670455 } });
|
|
1751
|
+
try {
|
|
1752
|
+
const receipt = await f.conversation.wake({ messageId: `opencode-large-${kind}-wire`, text: "Read the completed envelope" });
|
|
1753
|
+
const state = await f.conversation.wait();
|
|
1754
|
+
assert.equal(state.status, "ready", state.error);
|
|
1755
|
+
const saved = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
1756
|
+
assert.equal(saved.metadata.runtime.status, "complete");
|
|
1757
|
+
assert.equal(saved.metadata.runtime.completedEnvelope, true);
|
|
1758
|
+
assert.equal(saved.system.origin, "application");
|
|
1759
|
+
assert.equal(saved.system.messageId, receipt.messageId);
|
|
1760
|
+
assert.equal(f.appendedRows.length, 1);
|
|
1761
|
+
assert.equal(f.appendedRows[0].metadata.runtime.completedEnvelope, true);
|
|
1762
|
+
assert.equal(saved.assistant.text, raw);
|
|
1763
|
+
const binding = await f.binding();
|
|
1764
|
+
const nativeBefore = JSON.parse(await readFile(binding.databasePath, "utf8")).find(row => row.id === binding.sessionId);
|
|
1765
|
+
const prompt = (await f.trace()).find(row => row.url?.endsWith("/prompt_async"));
|
|
1766
|
+
const authored = nativeBefore.messages.find(row => row.info.role === "user" && row.info.id === prompt.body.messageID);
|
|
1767
|
+
assert.ok(authored);
|
|
1768
|
+
const answer = nativeBefore.messages.find(row => row.info.role === "assistant" && row.info.parentID === authored.info.id);
|
|
1769
|
+
assert.ok(answer);
|
|
1770
|
+
assert.equal(answer.info.finish, "stop");
|
|
1771
|
+
assert.equal(answer.parts[0].text, raw);
|
|
1772
|
+
assert.equal(saved.assistant.messageId, `${receipt.turnId}:${answer.info.id}:assistant`);
|
|
1773
|
+
const prepared = await f.prepare(receipt);
|
|
1774
|
+
assert.equal(prepared.text, raw);
|
|
1775
|
+
const { readAssistantResponseEnvelope } = await import("../src/server/lib/assistantToolLoop.js");
|
|
1776
|
+
const decoded = readAssistantResponseEnvelope(prepared.text);
|
|
1777
|
+
assert.deepEqual(decoded, value);
|
|
1778
|
+
if (kind === "tool") assert.equal(JSON.parse(decoded.arguments).value, handover);
|
|
1779
|
+
assert.equal(f.effects(), 0, "Preparing and decoding do not execute the requested action");
|
|
1780
|
+
await f.reopen();
|
|
1781
|
+
const restored = await f.prepare(receipt);
|
|
1782
|
+
assert.equal(restored.text, raw);
|
|
1783
|
+
assert.equal(restored.toolCallId, prepared.toolCallId);
|
|
1784
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId)), saved);
|
|
1785
|
+
const reopenedBinding = await f.binding();
|
|
1786
|
+
assert.equal(reopenedBinding.sessionId, binding.sessionId);
|
|
1787
|
+
assert.deepEqual(JSON.parse(await readFile(reopenedBinding.databasePath, "utf8")).find(row => row.id === binding.sessionId).messages,
|
|
1788
|
+
nativeBefore.messages);
|
|
1789
|
+
const trace = await f.trace();
|
|
1790
|
+
assert.equal(trace.filter(row => row.url?.endsWith("/prompt_async")).length, 1);
|
|
1791
|
+
assert.equal(trace.filter(row => row.url === "/api/session").length, 1);
|
|
1792
|
+
assert.deepEqual((await f.conversation.read()).conversationLog, []);
|
|
1793
|
+
assert.equal(f.effects(), 0);
|
|
1794
|
+
} finally { await f.close(); }
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1797
|
+
test("ordinary OpenCode forged-data wire retains the human cap in completed mode", async t => {
|
|
1798
|
+
const value = { kind: "reply", text: "\u0000".repeat(16000), toolName: "", arguments: "" };
|
|
1799
|
+
const raw = JSON.stringify(value);
|
|
1800
|
+
const f = await completedOpenCodeFixture(t, { assistantResponses: [{ text: raw }],
|
|
1801
|
+
configuration: { outputSchema: completedOpenCodeWireSchema },
|
|
1802
|
+
limits: { maxFinalReplyCharacters: 16000, maxOutputCharacters: 1670455 } });
|
|
1803
|
+
try {
|
|
1804
|
+
const receipt = await f.conversation.send({ messageId: "opencode-ordinary-forged-wire", text: "An ordinary request",
|
|
1805
|
+
data: { completedEnvelope: true } });
|
|
1806
|
+
await f.conversation.wait();
|
|
1807
|
+
const saved = await f.storage.read("conversation", tx => tx.readTurn(receipt.turnId));
|
|
1808
|
+
assert.equal(saved.metadata.runtime.status, "failed");
|
|
1809
|
+
assert.match(saved.metadata.runtime.error, /final reply limit/);
|
|
1810
|
+
assert.equal(saved.metadata.runtime.completedEnvelope, undefined);
|
|
1811
|
+
assert.equal(saved.user.origin, "user");
|
|
1812
|
+
assert.deepEqual(saved.user.data, { completedEnvelope: true });
|
|
1813
|
+
assert.notEqual(saved.assistant?.text, raw);
|
|
1814
|
+
assert.equal((await f.conversation.inspectDelivery({ messageId: receipt.messageId })).status, "accepted");
|
|
1815
|
+
assert.equal((await f.trace()).filter(row => row.url?.endsWith("/prompt_async")).length, 1);
|
|
1816
|
+
assert.equal(f.effects(), 0);
|
|
1817
|
+
} finally { await f.close(); }
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
|
|
1821
|
+
test("managed OpenCode refreshes its own key in the retained native scope without transferring completed receipt authority", async t => {
|
|
1822
|
+
const { createOpenCodeConversationServer } = await import("../src/server/conversation/openCodeProcess.js");
|
|
1823
|
+
const { createOpenCodeSharedRuntime } = await import("../src/server/conversation/openCodeRuntime.js");
|
|
1824
|
+
let f, owner, foreignScope = false;
|
|
1825
|
+
const starts = [], stops = [];
|
|
1826
|
+
f = await completedOpenCodeFixture(t, { opencode: async ({ connection }) => {
|
|
1827
|
+
const runtimeDirectory = path.join(f.directory, "managed-native");
|
|
1828
|
+
owner ||= createOpenCodeSharedRuntime({ scope: runtimeDirectory, onStop: event => stops.push(event) });
|
|
1829
|
+
const selected = { modelProviderId: connection.providerId, canonicalUrl: connection.baseURL || "",
|
|
1830
|
+
endpointCode: connection.sdkPackage || "", fingerprint: connection.apiKey };
|
|
1831
|
+
return { runtime: owner, runtimeDirectory, allowCredentialRefresh: true,
|
|
1832
|
+
registryPath: path.join(runtimeDirectory, "environments.json"),
|
|
1833
|
+
databasePath: path.join(runtimeDirectory, foreignScope ? "foreign-native.json" : "native.json"), selected,
|
|
1834
|
+
prepareServer: async () => ({ connections: [selected],
|
|
1835
|
+
options: { command: f.driverOptions.host.commands.opencode, workdir: f.directory,
|
|
1836
|
+
stateDirectory: runtimeDirectory, databasePath: path.join(runtimeDirectory, "native.json"),
|
|
1837
|
+
sessionEnvironmentRegistry: path.join(runtimeDirectory, "environments.json"),
|
|
1838
|
+
connection, env: f.driverOptions.host.env },
|
|
1839
|
+
create: async options => {
|
|
1840
|
+
const server = await createOpenCodeConversationServer(options);
|
|
1841
|
+
starts.push({ options, server });
|
|
1842
|
+
return server;
|
|
1843
|
+
}
|
|
1844
|
+
}) };
|
|
1845
|
+
} });
|
|
1846
|
+
try {
|
|
1847
|
+
const first = await f.complete("managed-first");
|
|
1848
|
+
const firstCanonical = await f.storage.read("conversation", tx => tx.readTurn(first.turnId));
|
|
1849
|
+
const before = await f.binding();
|
|
1850
|
+
const prior = JSON.parse(await readFile(before.databasePath, "utf8"));
|
|
1851
|
+
const prepared = await f.prepare(first);
|
|
1852
|
+
f.setKey("managed-replacement-key");
|
|
1853
|
+
await assert.rejects(f.prepare(first), /different account binding/);
|
|
1854
|
+
await assert.rejects(executeCompletedOpenCode(prepared), /different account binding/);
|
|
1855
|
+
assert.deepEqual(await f.binding(), before);
|
|
1856
|
+
assert.equal(f.effects(), 0);
|
|
1857
|
+
const second = await f.complete("managed-second");
|
|
1858
|
+
const secondCanonical = await f.storage.read("conversation", tx => tx.readTurn(second.turnId));
|
|
1859
|
+
const after = await f.binding();
|
|
1860
|
+
assert.equal(after.sessionId, before.sessionId);
|
|
1861
|
+
assert.equal(starts.length, 2);
|
|
1862
|
+
assert.equal(starts[0].options.databasePath, starts[1].options.databasePath);
|
|
1863
|
+
assert.equal(starts[0].options.workdir, starts[1].options.workdir);
|
|
1864
|
+
assert.equal(starts[0].options.connection.apiKey, "test-key");
|
|
1865
|
+
assert.equal(starts[1].options.connection.apiKey, "managed-replacement-key");
|
|
1866
|
+
assert.ok(stops.some(event => event.reason === "opencode-connection-changed"));
|
|
1867
|
+
await assert.rejects(starts[0].server.client.health({ signal: AbortSignal.timeout(1000) }));
|
|
1868
|
+
const trace = await f.trace();
|
|
1869
|
+
assert.equal(trace.filter(row => row.url === "/api/session").length, 1);
|
|
1870
|
+
assert.equal(trace.filter(row => row.url?.endsWith("/prompt_async")).length, 2);
|
|
1871
|
+
const current = JSON.parse(await readFile(after.databasePath, "utf8")).find(row => row.id === after.sessionId);
|
|
1872
|
+
const original = prior.find(row => row.id === before.sessionId);
|
|
1873
|
+
assert.deepEqual(current.messages.slice(0, original.messages.length), original.messages);
|
|
1874
|
+
assert.equal(f.appendedRows.length, 2);
|
|
1875
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(first.turnId)), firstCanonical);
|
|
1876
|
+
foreignScope = true;
|
|
1877
|
+
await assert.rejects(f.conversation.send({ messageId: "foreign-store", text: "Again" }), /another native history store or runtime/);
|
|
1878
|
+
const refused = await f.binding();
|
|
1879
|
+
assert.deepEqual(refused, { ...after, executionId: "", processDirectory: "" });
|
|
1880
|
+
await assert.rejects(starts[1].server.client.health({ signal: AbortSignal.timeout(1000) }));
|
|
1881
|
+
assert.deepEqual(JSON.parse(await readFile(after.databasePath, "utf8")).find(row => row.id === after.sessionId), current);
|
|
1882
|
+
assert.equal(starts.length, 2);
|
|
1883
|
+
assert.equal(f.appendedRows.length, 2);
|
|
1884
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(first.turnId)), firstCanonical);
|
|
1885
|
+
assert.deepEqual(await f.storage.read("conversation", tx => tx.readTurn(second.turnId)), secondCanonical);
|
|
1886
|
+
assert.equal((await f.trace()).filter(row => row.url?.endsWith("/prompt_async")).length, 2);
|
|
1887
|
+
assert.equal(f.effects(), 0);
|
|
1888
|
+
} finally {
|
|
1889
|
+
await f.close();
|
|
1890
|
+
await owner?.stop("managed-fixture-close");
|
|
1891
|
+
}
|
|
1892
|
+
});
|
|
1893
|
+
|
|
1894
|
+
|
|
1895
|
+
test("opted bound OpenCode re-enters supplied callback context after admission ends", async t => {
|
|
1896
|
+
const { AsyncLocalStorage } = await import("node:async_hooks");
|
|
1897
|
+
const context = new AsyncLocalStorage();
|
|
1898
|
+
const admission = { scope: "admission" }, control = { scope: "control" };
|
|
1899
|
+
const released = Promise.withResolvers();
|
|
1900
|
+
t.after(() => released.resolve());
|
|
1901
|
+
let reentries = 0;
|
|
1902
|
+
const f = await boundToolFixture(t, { runInContext(operation) {
|
|
1903
|
+
reentries++;
|
|
1904
|
+
return context.run(control, operation);
|
|
1905
|
+
} });
|
|
1906
|
+
const calls = [];
|
|
1907
|
+
const main = await f.open("delayed-context-main", async call => {
|
|
1908
|
+
await released.promise;
|
|
1909
|
+
assert.equal(context.getStore(), control, "A retained callback must re-enter the supplied context");
|
|
1910
|
+
calls.push(call.name);
|
|
1911
|
+
return {};
|
|
1912
|
+
});
|
|
1913
|
+
const running = context.run(admission, () => main.send("delayed-context-words", "tools"));
|
|
1914
|
+
await running.accepted;
|
|
1915
|
+
assert.equal(context.getStore(), undefined, "The authored admission callback has ended");
|
|
1916
|
+
released.resolve();
|
|
1917
|
+
await running.completion;
|
|
1918
|
+
assert.deepEqual(calls, ["assistant_action_search", "assistant_action_contract", "assistant_action_execute"]);
|
|
1919
|
+
assert.equal(reentries, 3);
|
|
1920
|
+
assert.equal(main.receipts.length, 1);
|
|
1921
|
+
assert.equal(main.receipts[0].turn.messages[0].text, "tools");
|
|
1922
|
+
assert.equal(f.owner.turns.get(main.key).active, false);
|
|
1923
|
+
});
|
|
1924
|
+
|
|
1925
|
+
|
|
1926
|
+
test("opted bound OpenCode retains supplied control through delayed monitor publication and retirement", async t => {
|
|
1927
|
+
const { AsyncLocalStorage } = await import("node:async_hooks");
|
|
1928
|
+
const context = new AsyncLocalStorage();
|
|
1929
|
+
const admission = { scope: "admission" }, control = { scope: "control" };
|
|
1930
|
+
const released = Promise.withResolvers();
|
|
1931
|
+
t.after(() => released.resolve());
|
|
1932
|
+
const phases = [];
|
|
1933
|
+
const f = await boundToolFixture(t, {
|
|
1934
|
+
runInContext: operation => context.run(control, operation),
|
|
1935
|
+
monitorRunInContext: async operation => context.run(control, operation),
|
|
1936
|
+
async monitorContext(phase) {
|
|
1937
|
+
if (phase !== "onRetired") {
|
|
1938
|
+
await released.promise;
|
|
1939
|
+
assert.equal(context.getStore(), control, "The retained monitor must inherit supplied CONTROL");
|
|
1940
|
+
}
|
|
1941
|
+
phases.push({ phase, context: context.getStore() });
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
const calls = [];
|
|
1945
|
+
const main = await f.open("delayed-monitor-main", async call => { calls.push(call.name); return {}; });
|
|
1946
|
+
const running = context.run(admission, () => main.send("delayed-monitor-words", "tools"));
|
|
1947
|
+
await running.accepted;
|
|
1948
|
+
assert.equal(context.getStore(), undefined, "Send admission ended before delayed publication");
|
|
1949
|
+
const retainedMonitor = f.owner.monitors.get(main.key);
|
|
1950
|
+
assert.equal(f.owner.beginMessageMonitor(main.key, f.owner.processes.get(main.key), {}, {}, {
|
|
1951
|
+
runInContext: async operation => context.run(control, operation),
|
|
1952
|
+
prepare: () => assert.fail("An existing monitor must not be recreated")
|
|
1953
|
+
}), retainedMonitor, "An async scope wrapper must preserve the original monitor promise");
|
|
1954
|
+
const preparationFailure = new Error("Exact synchronous monitor preparation failure");
|
|
1955
|
+
const scopeErrors = [];
|
|
1956
|
+
assert.throws(() => f.owner.beginMessageMonitor("failed-monitor-preparation", {}, {}, {}, {
|
|
1957
|
+
runInContext: async operation => context.run(control, operation),
|
|
1958
|
+
prepare() { assert.equal(context.getStore(), control); throw preparationFailure; },
|
|
1959
|
+
onError: error => scopeErrors.push(error)
|
|
1960
|
+
}), error => error === preparationFailure);
|
|
1961
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
1962
|
+
assert.deepEqual(scopeErrors, [], "A synchronous creation failure is not reported again by the scope wrapper");
|
|
1963
|
+
released.resolve();
|
|
1964
|
+
await running.completion;
|
|
1965
|
+
assert.deepEqual(calls, ["assistant_action_search", "assistant_action_contract", "assistant_action_execute"]);
|
|
1966
|
+
assert.ok(phases.some(value => value.phase === "writeRun"));
|
|
1967
|
+
assert.ok(phases.some(value => value.phase === "projectMessages"));
|
|
1968
|
+
assert.ok(phases.some(value => value.phase === "onRetired"));
|
|
1969
|
+
assert.ok(phases.every(value => value.context === control));
|
|
1970
|
+
assert.equal(main.receipts.length, 1);
|
|
1971
|
+
assert.equal(main.receipts[0].turn.messages[0].text, "tools");
|
|
1972
|
+
assert.equal(f.owner.turns.get(main.key).active, false);
|
|
1973
|
+
assert.equal(f.owner.turns.get(main.key).state, "completed");
|
|
1974
|
+
});
|