@jskit-ai/assistant-core 0.1.213 → 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 +16 -2
- package/src/server/conversation/claudeTurn.js +28 -8
- 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 +132 -24
- 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 +418 -64
- 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 +1329 -3
- package/test/claudeProcess.test.js +101 -0
- package/test/claudeTurn.test.js +31 -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 +112 -1
- package/test/openCodeClient.test.js +186 -0
- package/test/openCodeRuntime.test.js +78 -0
- package/test/opencodeConversationSession.test.js +637 -5
|
@@ -513,3 +513,189 @@ test("OpenCode retained storage validates complete inventories and bounded cance
|
|
|
513
513
|
await assert.rejects(cancelledPage, { name: "AbortError" });
|
|
514
514
|
|
|
515
515
|
});
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
test("stopped OpenCode native database retains the original MessageV2 projection without native writes", async t => {
|
|
519
|
+
const { mkdtemp, readFile, lstat, readdir, rm } = await import("node:fs/promises");
|
|
520
|
+
const { tmpdir } = await import("node:os");
|
|
521
|
+
const path = await import("node:path");
|
|
522
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
523
|
+
const { readOpenCodeConversationDatabase } = await import("../src/server/hosts/openCodeClient.js");
|
|
524
|
+
const { readStoppedNativeDatabase } = await import("../src/server/hosts/nativeHistory.js");
|
|
525
|
+
const root = await mkdtemp(path.join(tmpdir(), "assistant-native-opencode-"));
|
|
526
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
527
|
+
const file = path.join(root, "native space.db");
|
|
528
|
+
const db = new DatabaseSync(file);
|
|
529
|
+
db.exec(`CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT, project_id TEXT);
|
|
530
|
+
CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT, time_created INTEGER, data TEXT);
|
|
531
|
+
CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT, session_id TEXT, data TEXT);
|
|
532
|
+
CREATE TABLE session_input (session_id TEXT, promoted_seq INTEGER);`);
|
|
533
|
+
db.prepare("INSERT INTO session VALUES (?, ?, ?)").run("ses_original", "/private/actor/colleague_scope", "project_original");
|
|
534
|
+
const insert = db.prepare("INSERT INTO message VALUES (?, ?, ?, ?)");
|
|
535
|
+
insert.run("msg_original_user", "ses_original", 1, JSON.stringify({ id: "untrusted-payload-id", role: "user", time: { created: 1 } }));
|
|
536
|
+
insert.run("msg_original_answer", "ses_original", 2, JSON.stringify({ role: "assistant", parentID: "msg_original_user", finish: "stop", time: { created: 2, completed: 3 } }));
|
|
537
|
+
db.prepare("INSERT INTO part VALUES (?, ?, ?, ?)").run("prt_original_text", "msg_original_answer", "ses_original",
|
|
538
|
+
JSON.stringify({ type: "text", text: "Exact answer 🦊", id: "untrusted-part-id" }));
|
|
539
|
+
db.close();
|
|
540
|
+
const before = await readFile(file);
|
|
541
|
+
const stat = await lstat(file);
|
|
542
|
+
const messages = [];
|
|
543
|
+
const result = await readOpenCodeConversationDatabase({ databasePath: file, conversationId: "ses_original", onMessage: row => messages.push(row) });
|
|
544
|
+
assert.equal(result.session.directory, "/private/actor/colleague_scope");
|
|
545
|
+
assert.equal(result.session.project_id, "project_original");
|
|
546
|
+
assert.equal(result.recordCount, 2);
|
|
547
|
+
assert.match(result.revision, /^[a-f0-9]{64}$/u);
|
|
548
|
+
assert.deepEqual(messages.map(row => [row.id, row.sessionID, row.type]), [
|
|
549
|
+
["msg_original_user", "ses_original", "user"], ["msg_original_answer", "ses_original", "assistant"]
|
|
550
|
+
]);
|
|
551
|
+
assert.equal(messages[1].parentID, "msg_original_user");
|
|
552
|
+
assert.deepEqual(messages[1].content, [{ type: "text", text: "Exact answer 🦊", id: "prt_original_text", sessionID: "ses_original", messageID: "msg_original_answer" }]);
|
|
553
|
+
await assert.rejects(readStoppedNativeDatabase(file, database => database.exec("DELETE FROM message")), /readonly|read-only/iu);
|
|
554
|
+
const cancelled = new Error("Caller stopped original inspection");
|
|
555
|
+
await assert.rejects(readOpenCodeConversationDatabase({ databasePath: file, conversationId: "ses_original", onMessage() {}, signal: AbortSignal.abort(cancelled) }), error => error === cancelled);
|
|
556
|
+
assert.deepEqual(await readFile(file), before);
|
|
557
|
+
const after = await lstat(file);
|
|
558
|
+
for (const key of ["dev", "ino", "size", "mtimeMs", "ctimeMs", "mode"]) assert.equal(after[key], stat[key]);
|
|
559
|
+
assert.deepEqual(await readdir(root), ["native space.db"]);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("stopped OpenCode database refuses pending native work and foreign parts with no storage changes", async t => {
|
|
563
|
+
const { mkdtemp, readFile, rm } = await import("node:fs/promises");
|
|
564
|
+
const { tmpdir } = await import("node:os");
|
|
565
|
+
const path = await import("node:path");
|
|
566
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
567
|
+
const { readOpenCodeConversationDatabase } = await import("../src/server/hosts/openCodeClient.js");
|
|
568
|
+
const root = await mkdtemp(path.join(tmpdir(), "assistant-native-opencode-refusal-"));
|
|
569
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
570
|
+
const file = path.join(root, "native.db");
|
|
571
|
+
const db = new DatabaseSync(file);
|
|
572
|
+
db.exec(`CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT);
|
|
573
|
+
CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT, time_created INTEGER, data TEXT);
|
|
574
|
+
CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT, session_id TEXT, data TEXT);
|
|
575
|
+
CREATE TABLE session_input (session_id TEXT, promoted_seq INTEGER);
|
|
576
|
+
INSERT INTO session VALUES ('ses_original', '/private/actor/scope');
|
|
577
|
+
INSERT INTO session_input VALUES ('ses_original', NULL);`);
|
|
578
|
+
db.close();
|
|
579
|
+
let seen = 0;
|
|
580
|
+
const before = await readFile(file);
|
|
581
|
+
await assert.rejects(readOpenCodeConversationDatabase({ databasePath: file, conversationId: "ses_original", onMessage() { seen += 1; } }), /unpromoted native request/u);
|
|
582
|
+
assert.equal(seen, 0);
|
|
583
|
+
assert.deepEqual(await readFile(file), before);
|
|
584
|
+
const edited = new DatabaseSync(file);
|
|
585
|
+
edited.exec("DELETE FROM session_input");
|
|
586
|
+
edited.prepare("INSERT INTO message VALUES (?, ?, ?, ?)").run("msg_original", "ses_original", 1, JSON.stringify({ role: "assistant", finish: "stop" }));
|
|
587
|
+
edited.prepare("INSERT INTO part VALUES (?, ?, ?, ?)").run("prt_foreign", "msg_original", "ses_other", JSON.stringify({ type: "text", text: "Other private owner" }));
|
|
588
|
+
edited.close();
|
|
589
|
+
const foreignBefore = await readFile(file);
|
|
590
|
+
await assert.rejects(readOpenCodeConversationDatabase({ databasePath: file, conversationId: "ses_original", onMessage() { seen += 1; } }), /could not be inspected completely/u);
|
|
591
|
+
assert.equal(seen, 0);
|
|
592
|
+
assert.deepEqual(await readFile(file), foreignBefore);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test("stopped native inspection rejects live journals and noncanonical files before opening storage", async t => {
|
|
596
|
+
const { mkdtemp, readFile, writeFile, readdir, symlink, rm } = await import("node:fs/promises");
|
|
597
|
+
const { tmpdir } = await import("node:os");
|
|
598
|
+
const path = await import("node:path");
|
|
599
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
600
|
+
const { readStoppedNativeDatabase } = await import("../src/server/hosts/nativeHistory.js");
|
|
601
|
+
const root = await mkdtemp(path.join(tmpdir(), "assistant-native-journal-"));
|
|
602
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
603
|
+
const file = path.join(root, "native.db");
|
|
604
|
+
const db = new DatabaseSync(file);
|
|
605
|
+
db.exec("CREATE TABLE retained (id TEXT)");
|
|
606
|
+
db.close();
|
|
607
|
+
const before = await readFile(file);
|
|
608
|
+
const sidecar = Buffer.from("Uncheckpointed native storage, retained exactly");
|
|
609
|
+
await writeFile(`${file}-wal`, sidecar);
|
|
610
|
+
let read = false;
|
|
611
|
+
await assert.rejects(readStoppedNativeDatabase(file, () => { read = true; }), /journal sidecar/u);
|
|
612
|
+
assert.equal(read, false);
|
|
613
|
+
assert.deepEqual(await readFile(file), before);
|
|
614
|
+
assert.deepEqual(await readFile(`${file}-wal`), sidecar);
|
|
615
|
+
await symlink(file, path.join(root, "link.db"));
|
|
616
|
+
await assert.rejects(readStoppedNativeDatabase(path.join(root, "link.db"), () => { read = true; }), /regular file.*canonical path/u);
|
|
617
|
+
assert.equal(read, false);
|
|
618
|
+
assert.deepEqual((await readdir(root)).sort(), ["link.db", "native.db", "native.db-wal"]);
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
test("stopped native WAL inspection reads newest committed OpenCode rows without changing native files and removes private snapshots", async t => {
|
|
623
|
+
const { mkdtemp, readFile, writeFile, lstat, readdir, rm } = await import("node:fs/promises");
|
|
624
|
+
const { tmpdir } = await import("node:os");
|
|
625
|
+
const path = await import("node:path");
|
|
626
|
+
const { spawnSync } = await import("node:child_process");
|
|
627
|
+
const { DatabaseSync } = await import("node:sqlite");
|
|
628
|
+
const { readOpenCodeConversationDatabase } = await import("../src/server/hosts/openCodeClient.js");
|
|
629
|
+
const { readStoppedNativeDatabase } = await import("../src/server/hosts/nativeHistory.js");
|
|
630
|
+
const root = await mkdtemp(path.join(tmpdir(), "assistant-native-stopped-wal-"));
|
|
631
|
+
t.after(() => rm(root, { recursive: true, force: true }));
|
|
632
|
+
const file = path.join(root, "native.db");
|
|
633
|
+
const db = new DatabaseSync(file);
|
|
634
|
+
db.exec(`CREATE TABLE session (id TEXT PRIMARY KEY, directory TEXT);
|
|
635
|
+
CREATE TABLE message (id TEXT PRIMARY KEY, session_id TEXT, time_created INTEGER, data TEXT);
|
|
636
|
+
CREATE TABLE part (id TEXT PRIMARY KEY, message_id TEXT, session_id TEXT, data TEXT);
|
|
637
|
+
INSERT INTO session VALUES ('ses_original', '/private/actor/colleague_scope');`);
|
|
638
|
+
db.close();
|
|
639
|
+
// Leave real committed SQLite WAL frames after the only fixture writer exits.
|
|
640
|
+
// The base contains the schema, but neither of these newly committed messages.
|
|
641
|
+
const writer = spawnSync(process.execPath, ["--input-type=module", "-e", `
|
|
642
|
+
import { DatabaseSync } from "node:sqlite";
|
|
643
|
+
const database = new DatabaseSync(process.argv[1]);
|
|
644
|
+
database.exec("PRAGMA journal_mode=WAL; PRAGMA wal_autocheckpoint=0; BEGIN");
|
|
645
|
+
const insert = database.prepare("INSERT INTO message VALUES (?, ?, ?, ?)");
|
|
646
|
+
insert.run("msg_user", "ses_original", 1, JSON.stringify({ role: "user", time: { created: 1 } }));
|
|
647
|
+
insert.run("msg_answer", "ses_original", 2, JSON.stringify({ role: "assistant", parentID: "msg_user", finish: "stop", time: { created: 2, completed: 3 } }));
|
|
648
|
+
database.prepare("INSERT INTO part VALUES (?, ?, ?, ?)").run("prt_text", "msg_answer", "ses_original", JSON.stringify({ type: "text", text: "Newest committed answer 🦊" }));
|
|
649
|
+
database.exec("COMMIT");
|
|
650
|
+
process.kill(process.pid, "SIGKILL");
|
|
651
|
+
`, file], { encoding: "utf8", timeout: 10_000 });
|
|
652
|
+
assert.equal(writer.signal, "SIGKILL", writer.stderr);
|
|
653
|
+
const before = {};
|
|
654
|
+
for (const suffix of ["", "-wal", "-shm"]) before[suffix] = { bytes: await readFile(file + suffix), stat: await lstat(file + suffix) };
|
|
655
|
+
assert.ok(before["-wal"].bytes.length > 32);
|
|
656
|
+
const nativeNames = await readdir(root);
|
|
657
|
+
let snapshot;
|
|
658
|
+
const count = await readStoppedNativeDatabase(file, database => {
|
|
659
|
+
snapshot = database.prepare("PRAGMA database_list").get().file;
|
|
660
|
+
assert.notEqual(snapshot, file, "WAL inspection opens only the private snapshot");
|
|
661
|
+
assert.equal(database.prepare("SELECT COUNT(*) AS count FROM message").get().count, 2);
|
|
662
|
+
assert.throws(() => database.exec("DELETE FROM message"), /readonly|read-only/iu);
|
|
663
|
+
return 2;
|
|
664
|
+
});
|
|
665
|
+
assert.equal(count, 2);
|
|
666
|
+
await assert.rejects(lstat(path.dirname(snapshot)), { code: "ENOENT" });
|
|
667
|
+
const messages = [];
|
|
668
|
+
const result = await readOpenCodeConversationDatabase({ databasePath: file, conversationId: "ses_original", onMessage: row => messages.push(row) });
|
|
669
|
+
assert.equal(result.recordCount, 2);
|
|
670
|
+
assert.deepEqual(messages.map(row => row.id), ["msg_user", "msg_answer"]);
|
|
671
|
+
assert.equal(messages[1].content[0].text, "Newest committed answer 🦊");
|
|
672
|
+
const cancelled = new Error("Cancel the private stopped snapshot");
|
|
673
|
+
const controller = new AbortController();
|
|
674
|
+
await assert.rejects(readStoppedNativeDatabase(file, database => {
|
|
675
|
+
snapshot = database.prepare("PRAGMA database_list").get().file;
|
|
676
|
+
controller.abort(cancelled);
|
|
677
|
+
}, { signal: controller.signal }), error => error === cancelled);
|
|
678
|
+
await assert.rejects(lstat(path.dirname(snapshot)), { code: "ENOENT" });
|
|
679
|
+
const interruptedProjection = new AbortController();
|
|
680
|
+
await assert.rejects(readOpenCodeConversationDatabase({ databasePath: file, conversationId: "ses_original", signal: interruptedProjection.signal,
|
|
681
|
+
onMessage() { interruptedProjection.abort(cancelled); } }), error => error === cancelled);
|
|
682
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
683
|
+
assert.deepEqual(await readFile(file + suffix), before[suffix].bytes);
|
|
684
|
+
const after = await lstat(file + suffix);
|
|
685
|
+
for (const key of ["dev", "ino", "size", "mtimeMs", "ctimeMs", "mode"]) assert.equal(after[key], before[suffix].stat[key]);
|
|
686
|
+
}
|
|
687
|
+
assert.deepEqual(await readdir(root), nativeNames);
|
|
688
|
+
const invalidWal = Buffer.from(before["-wal"].bytes);
|
|
689
|
+
invalidWal.writeUInt32BE(0, 0);
|
|
690
|
+
await writeFile(file + "-wal", invalidWal);
|
|
691
|
+
let invalidRead = false;
|
|
692
|
+
await assert.rejects(readStoppedNativeDatabase(file, () => { invalidRead = true; }), /journal sidecar/u);
|
|
693
|
+
assert.equal(invalidRead, false, "Malformed paired WAL refuses before the reader callback");
|
|
694
|
+
assert.deepEqual(await readFile(file + "-wal"), invalidWal);
|
|
695
|
+
await writeFile(file + "-wal", before["-wal"].bytes);
|
|
696
|
+
await assert.rejects(readStoppedNativeDatabase(file, async database => {
|
|
697
|
+
snapshot = database.prepare("PRAGMA database_list").get().file;
|
|
698
|
+
await writeFile(file + "-wal", Buffer.concat([before["-wal"].bytes, Buffer.from("Fixture writer changed native storage")]));
|
|
699
|
+
}), /Native storage changed during inspection/u);
|
|
700
|
+
await assert.rejects(lstat(path.dirname(snapshot)), { code: "ENOENT" });
|
|
701
|
+
});
|
|
@@ -643,3 +643,81 @@ test("original message monitor clone retains only its registered target's final
|
|
|
643
643
|
assert.equal(modelPrompts, 0);
|
|
644
644
|
assert.deepEqual(errors, []);
|
|
645
645
|
});
|
|
646
|
+
|
|
647
|
+
test("credential publication refuses another provider and unowned consumers before stopping or writing", async () => {
|
|
648
|
+
for (const unowned of [false, true]) {
|
|
649
|
+
const { runtime, acquire, stops } = fixture();
|
|
650
|
+
const selected = await acquire("selected"); selected.modelProviderId = "deepseek";
|
|
651
|
+
const peer = await acquire("peer"); peer.modelProviderId = unowned ? "deepseek" : "zai";
|
|
652
|
+
let published = false;
|
|
653
|
+
const effects = [];
|
|
654
|
+
await assert.rejects(runtime.invalidateRuntimes({ modelProviderId: "deepseek", publish() { published = true; } }, {
|
|
655
|
+
release: { ownsTarget: target => !unowned || target !== peer,
|
|
656
|
+
beforeRelease: async target => effects.push(target.key), onRemoved: async () => {}, failure: error => { throw error; } }
|
|
657
|
+
}), { code: "assistant_opencode_connection_in_use" });
|
|
658
|
+
assert.equal(published, false);
|
|
659
|
+
assert.deepEqual(effects, []);
|
|
660
|
+
assert.deepEqual(stops, []);
|
|
661
|
+
assert.equal(runtime.processes.get("peer"), peer);
|
|
662
|
+
assert.equal(peer.abortController.signal.aborted, false);
|
|
663
|
+
assert.equal(selected.abortController.signal.aborted, false);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
test("credential publication fences new acquisition and ensure across native cleanup and private publication", async () => {
|
|
668
|
+
const { runtime, acquire, connection, starts, stops } = fixture();
|
|
669
|
+
const selected = await acquire("selected"); selected.modelProviderId = "deepseek";
|
|
670
|
+
const publishing = Promise.withResolvers(), finish = Promise.withResolvers();
|
|
671
|
+
let saved = "old";
|
|
672
|
+
const changed = runtime.invalidateRuntimes({ modelProviderId: "deepseek", async publish() {
|
|
673
|
+
assert.equal(stops.length, 1); publishing.resolve(); await finish.promise; saved = "new";
|
|
674
|
+
} }, { release: { ownsTarget: () => true, beforeRelease: async () => {}, onRemoved: async () => {}, failure: error => { throw error; } } });
|
|
675
|
+
await publishing.promise;
|
|
676
|
+
assert.equal(saved, "old");
|
|
677
|
+
await assert.rejects(acquire("late"), { code: "assistant_opencode_connection_changing" });
|
|
678
|
+
await assert.rejects(runtime.ensure(connection, async () => { throw new Error("must not start"); }), { code: "assistant_opencode_connection_changing" });
|
|
679
|
+
assert.equal(starts.length, 1);
|
|
680
|
+
finish.resolve();
|
|
681
|
+
assert.equal((await changed).ok, true);
|
|
682
|
+
assert.equal(saved, "new");
|
|
683
|
+
await acquire("retry");
|
|
684
|
+
assert.equal(starts.length, 2);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test("credential barrier drains admitted acquisitions and refuses a late peer without native effects", async () => {
|
|
688
|
+
const { runtime, stops } = fixture();
|
|
689
|
+
const started = Promise.withResolvers(), finish = Promise.withResolvers();
|
|
690
|
+
const pending = runtime.acquire("late-peer", async () => {
|
|
691
|
+
started.resolve(); await finish.promise;
|
|
692
|
+
return { key: "late-peer", modelProviderId: "zai", abortController: new AbortController() };
|
|
693
|
+
});
|
|
694
|
+
await started.promise;
|
|
695
|
+
let published = false;
|
|
696
|
+
const changed = runtime.invalidateRuntimes({ modelProviderId: "deepseek", publish() { published = true; } }, {
|
|
697
|
+
release: { ownsTarget: () => true, beforeRelease: async () => { throw new Error("must not release"); } }
|
|
698
|
+
});
|
|
699
|
+
const rejected = assert.rejects(changed, { code: "assistant_opencode_connection_in_use" });
|
|
700
|
+
finish.resolve(); await pending; await rejected;
|
|
701
|
+
assert.equal(published, false);
|
|
702
|
+
assert.deepEqual(stops, []);
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
test("unconfirmed terminal and failed verified stop keep the prior credential and permit safe retry", async () => {
|
|
706
|
+
let terminalOpen = true, stopFails = true, published = false;
|
|
707
|
+
const { runtime, acquire, stops } = fixture({ stop: async () => ({ exited: !stopFails }) });
|
|
708
|
+
const target = await acquire("selected"); target.modelProviderId = "deepseek";
|
|
709
|
+
const input = { modelProviderId: "deepseek", publish() { published = true; } };
|
|
710
|
+
const preparation = { release: { ownsTarget: () => true,
|
|
711
|
+
assertConnectionChange() { if (terminalOpen) throw new Error("close terminal"); },
|
|
712
|
+
beforeRelease: async () => {}, onRemoved: async () => {}, failure: error => { throw error; } } };
|
|
713
|
+
await assert.rejects(runtime.invalidateRuntimes(input, preparation), /close terminal/);
|
|
714
|
+
assert.equal(target.abortController.signal.aborted, false);
|
|
715
|
+
assert.equal(stops.length, 0);
|
|
716
|
+
terminalOpen = false;
|
|
717
|
+
await assert.rejects(runtime.invalidateRuntimes(input, preparation), { code: "assistant_opencode_stop_unverified" });
|
|
718
|
+
assert.equal(published, false);
|
|
719
|
+
stopFails = false;
|
|
720
|
+
assert.equal((await runtime.invalidateRuntimes(input, preparation)).ok, true);
|
|
721
|
+
assert.equal(published, true);
|
|
722
|
+
assert.equal(stops.length, 2);
|
|
723
|
+
});
|