[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
607
electron/bridges/aiBridge/sdk/codebuddySessionManager.test.cjs
Normal file
607
electron/bridges/aiBridge/sdk/codebuddySessionManager.test.cjs
Normal file
@@ -0,0 +1,607 @@
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { CodebuddySessionManager, computeOptionsFingerprint } = require("./codebuddySessionManager.cjs");
|
||||
|
||||
function collector() {
|
||||
const events = [];
|
||||
const emitter = {
|
||||
text: (t) => events.push({ k: "text", t }),
|
||||
reasoning: (d) => events.push({ k: "reasoning", d }),
|
||||
toolCall: (name, args, id) => events.push({ k: "toolCall", name, args, id }),
|
||||
toolResult: (id, out, name) => events.push({ k: "toolResult", id, out, name }),
|
||||
usage: (usage) => events.push({ k: "usage", usage }),
|
||||
status: (m) => events.push({ k: "status", m }),
|
||||
sessionId: (s) => events.push({ k: "sessionId", s }),
|
||||
emitDone: () => events.push({ k: "done" }),
|
||||
emitError: (m) => events.push({ k: "error", m }),
|
||||
emitEvent: (ev) => events.push({ k: "event", ev }),
|
||||
};
|
||||
return { events, emitter };
|
||||
}
|
||||
|
||||
/** Create a fake V2 session that yields predefined messages. */
|
||||
function fakeSession(messages, opts = {}) {
|
||||
let sentMessages = [];
|
||||
let closed = false;
|
||||
let interruptCalls = 0;
|
||||
return {
|
||||
sessionId: opts.sessionId || "fake-sess-1",
|
||||
sentMessages,
|
||||
get closed() { return closed; },
|
||||
get interruptCalls() { return interruptCalls; },
|
||||
async connect() {},
|
||||
async send(msg) { sentMessages.push(msg); },
|
||||
async *stream() { for (const m of messages) yield m; },
|
||||
async interrupt() { interruptCalls += 1; },
|
||||
async setModel(model) { this._model = model; },
|
||||
setHooks(hooks) { this._hooks = hooks; },
|
||||
setCanUseTool(handler) { this._canUseTool = handler; },
|
||||
close() { closed = true; },
|
||||
};
|
||||
}
|
||||
|
||||
test("getOrCreateSession reuses existing session when options match", async () => {
|
||||
const mgr = new CodebuddySessionManager();
|
||||
const session = fakeSession([], { sessionId: "existing-sess" });
|
||||
const opts = { cwd: "/tmp", model: "glm-5" };
|
||||
mgr.sessions.set("reuse-key", { session, fingerprint: computeOptionsFingerprint(opts) });
|
||||
|
||||
const result = await mgr.getOrCreateSession({
|
||||
sessionKey: "reuse-key",
|
||||
sessionOptions: opts,
|
||||
});
|
||||
assert.equal(result, session);
|
||||
});
|
||||
|
||||
test("getOrCreateSession refreshes turn-scoped callbacks on a reused session", async () => {
|
||||
let createdOptions;
|
||||
const session = fakeSession([], { sessionId: "callback-session" });
|
||||
const mgr = new CodebuddySessionManager({
|
||||
loadSdk: async () => ({
|
||||
unstable_v2_createSession: (options) => {
|
||||
createdOptions = options;
|
||||
return session;
|
||||
},
|
||||
unstable_v2_resumeSession: () => session,
|
||||
}),
|
||||
});
|
||||
const firstEvents = [];
|
||||
const secondEvents = [];
|
||||
const firstOptions = {
|
||||
cwd: "/tmp",
|
||||
hooks: { Notification: [{ hooks: [() => firstEvents.push("hook")] }] },
|
||||
canUseTool: async () => ({ behavior: "allow", updatedInput: {} }),
|
||||
elicitation: {
|
||||
create: async () => {
|
||||
firstEvents.push("elicitation");
|
||||
return { action: "accept" };
|
||||
},
|
||||
},
|
||||
};
|
||||
const secondOptions = {
|
||||
cwd: "/tmp",
|
||||
hooks: { Notification: [{ hooks: [() => secondEvents.push("hook")] }] },
|
||||
canUseTool: async () => ({ behavior: "deny", message: "second turn" }),
|
||||
elicitation: {
|
||||
create: async () => {
|
||||
secondEvents.push("elicitation");
|
||||
return { action: "decline" };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const first = await mgr.getOrCreateSession({
|
||||
sessionKey: "callback-key",
|
||||
sessionOptions: firstOptions,
|
||||
});
|
||||
const second = await mgr.getOrCreateSession({
|
||||
sessionKey: "callback-key",
|
||||
sessionOptions: secondOptions,
|
||||
});
|
||||
|
||||
assert.equal(first, session);
|
||||
assert.equal(second, session);
|
||||
assert.equal(session._hooks, secondOptions.hooks);
|
||||
assert.equal(session._canUseTool, secondOptions.canUseTool);
|
||||
assert.notEqual(createdOptions.elicitation, firstOptions.elicitation);
|
||||
await session._hooks.Notification[0].hooks[0]();
|
||||
assert.deepEqual(await session._canUseTool(), {
|
||||
behavior: "deny",
|
||||
message: "second turn",
|
||||
});
|
||||
assert.deepEqual(
|
||||
await createdOptions.elicitation.create({}, { signal: new AbortController().signal }),
|
||||
{ action: "decline" },
|
||||
);
|
||||
assert.deepEqual(firstEvents, []);
|
||||
assert.deepEqual(secondEvents, ["hook", "elicitation"]);
|
||||
});
|
||||
|
||||
test("getOrCreateSession closes stale session when options change", async () => {
|
||||
const oldSession = fakeSession([], { sessionId: "old-sess" });
|
||||
const replacementSession = fakeSession([], { sessionId: "new-sess" });
|
||||
const mgr = new CodebuddySessionManager({
|
||||
loadSdk: async () => ({
|
||||
unstable_v2_createSession: () => replacementSession,
|
||||
unstable_v2_resumeSession: () => replacementSession,
|
||||
}),
|
||||
});
|
||||
const oldOpts = { cwd: "/tmp", model: "glm-4" };
|
||||
const newOpts = { cwd: "/tmp", model: "glm-5" };
|
||||
mgr.sessions.set("stale-key", {
|
||||
session: oldSession,
|
||||
fingerprint: computeOptionsFingerprint(oldOpts),
|
||||
});
|
||||
|
||||
const result = await mgr.getOrCreateSession({
|
||||
sessionKey: "stale-key",
|
||||
sessionOptions: newOpts,
|
||||
});
|
||||
|
||||
assert.ok(oldSession.closed);
|
||||
assert.equal(result, replacementSession);
|
||||
assert.equal(mgr.sessions.get("stale-key").session, replacementSession);
|
||||
assert.equal(
|
||||
mgr.sessions.get("stale-key").fingerprint,
|
||||
computeOptionsFingerprint(newOpts),
|
||||
);
|
||||
});
|
||||
|
||||
test("runTurn closes a session when initial send fails before fallback", async () => {
|
||||
const session = fakeSession([], { sessionId: "failed-connect-session" });
|
||||
session.send = async () => {
|
||||
throw new Error("connect failed");
|
||||
};
|
||||
const mgr = new CodebuddySessionManager({
|
||||
loadSdk: async () => ({
|
||||
unstable_v2_createSession: () => session,
|
||||
unstable_v2_resumeSession: () => session,
|
||||
}),
|
||||
});
|
||||
|
||||
const { events, emitter } = collector();
|
||||
const result = await mgr.runTurn({
|
||||
sessionKey: "failed-connect-key",
|
||||
prompt: "hello",
|
||||
attachments: [],
|
||||
options: { abortController: new AbortController() },
|
||||
emitter,
|
||||
sessionOptions: {},
|
||||
});
|
||||
|
||||
assert.equal(result, null);
|
||||
assert.equal(session.closed, true);
|
||||
assert.equal(mgr.sessions.has("failed-connect-key"), false);
|
||||
assert.deepEqual(events, []);
|
||||
});
|
||||
|
||||
test("runTurn closes and evicts a session when response streaming fails", async () => {
|
||||
const session = fakeSession([], { sessionId: "failed-stream-session" });
|
||||
session.stream = async function* stream() {
|
||||
throw new Error("transport died");
|
||||
};
|
||||
const mgr = new CodebuddySessionManager({
|
||||
loadSdk: async () => ({
|
||||
unstable_v2_createSession: () => session,
|
||||
unstable_v2_resumeSession: () => session,
|
||||
}),
|
||||
});
|
||||
|
||||
const { events, emitter } = collector();
|
||||
const result = await mgr.runTurn({
|
||||
sessionKey: "failed-stream-key",
|
||||
prompt: "hello",
|
||||
attachments: [],
|
||||
options: { abortController: new AbortController() },
|
||||
emitter,
|
||||
sessionOptions: {},
|
||||
});
|
||||
|
||||
assert.deepEqual(result, {
|
||||
sessionId: "failed-stream-session",
|
||||
usedV2: true,
|
||||
});
|
||||
assert.equal(session.closed, true);
|
||||
assert.equal(mgr.sessions.has("failed-stream-key"), false);
|
||||
assert.deepEqual(events, [
|
||||
{ k: "sessionId", s: "failed-stream-session" },
|
||||
{ k: "error", m: "transport died" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("computeOptionsFingerprint detects option changes", () => {
|
||||
const base = {
|
||||
cwd: "/tmp",
|
||||
model: "glm-5",
|
||||
maxTurns: 10,
|
||||
effort: "high",
|
||||
extraArgs: { "dangerously-skip-permissions": null },
|
||||
};
|
||||
const same = {
|
||||
cwd: "/tmp",
|
||||
model: "glm-5",
|
||||
maxTurns: 10,
|
||||
effort: "high",
|
||||
extraArgs: { "dangerously-skip-permissions": null },
|
||||
};
|
||||
const diffModel = { cwd: "/tmp", model: "glm-4", maxTurns: 10, effort: "high" };
|
||||
const diffMaxTurns = { cwd: "/tmp", model: "glm-5", maxTurns: 20, effort: "high" };
|
||||
const diffEffort = { cwd: "/tmp", model: "glm-5", maxTurns: 10, effort: "low" };
|
||||
const diffExtraArgs = {
|
||||
...base,
|
||||
extraArgs: { "dangerously-skip-permissions": "false" },
|
||||
};
|
||||
assert.equal(computeOptionsFingerprint(base), computeOptionsFingerprint(same));
|
||||
assert.notEqual(computeOptionsFingerprint(base), computeOptionsFingerprint(diffModel));
|
||||
assert.notEqual(computeOptionsFingerprint(base), computeOptionsFingerprint(diffMaxTurns));
|
||||
assert.notEqual(computeOptionsFingerprint(base), computeOptionsFingerprint(diffEffort));
|
||||
assert.notEqual(computeOptionsFingerprint(base), computeOptionsFingerprint(diffExtraArgs));
|
||||
});
|
||||
|
||||
test("getOrCreateSession never reuses sessions with unserializable option fingerprints", async () => {
|
||||
const circular = {};
|
||||
circular.self = circular;
|
||||
const oldSession = fakeSession([], { sessionId: "circular-old" });
|
||||
const replacementSession = fakeSession([], { sessionId: "circular-new" });
|
||||
const mgr = new CodebuddySessionManager({
|
||||
loadSdk: async () => ({
|
||||
unstable_v2_createSession: () => replacementSession,
|
||||
unstable_v2_resumeSession: () => replacementSession,
|
||||
}),
|
||||
});
|
||||
mgr.sessions.set("circular-key", {
|
||||
session: oldSession,
|
||||
fingerprint: computeOptionsFingerprint({ mcpServers: circular }),
|
||||
});
|
||||
|
||||
const result = await mgr.getOrCreateSession({
|
||||
sessionKey: "circular-key",
|
||||
sessionOptions: { mcpServers: circular },
|
||||
});
|
||||
|
||||
assert.equal(result, replacementSession);
|
||||
assert.equal(oldSession.closed, true);
|
||||
assert.equal(mgr.sessions.get("circular-key").session, replacementSession);
|
||||
});
|
||||
|
||||
test("runTurn streams messages via V2 session when available", async () => {
|
||||
const mgr = new CodebuddySessionManager();
|
||||
const messages = [
|
||||
{ type: "system", session_id: "sess-v2" },
|
||||
{ type: "stream_event", event: { type: "content_block_delta", delta: { type: "text_delta", text: "hi from v2" } } },
|
||||
];
|
||||
const session = fakeSession(messages, { sessionId: "sess-v2" });
|
||||
// Pre-populate the session map to bypass SDK import.
|
||||
mgr.sessions.set("preloaded-key", { session, fingerprint: computeOptionsFingerprint({}) });
|
||||
|
||||
const { events, emitter } = collector();
|
||||
const result = await mgr.runTurn({
|
||||
sessionKey: "preloaded-key",
|
||||
prompt: "say hi",
|
||||
attachments: [],
|
||||
options: { abortController: new AbortController() },
|
||||
emitter,
|
||||
sessionOptions: {},
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { sessionId: "sess-v2", usedV2: true });
|
||||
assert.ok(events.some((e) => e.k === "text" && e.t === "hi from v2"));
|
||||
assert.ok(events.some((e) => e.k === "done"));
|
||||
assert.deepEqual(
|
||||
events.filter((event) => event.k === "sessionId"),
|
||||
[{ k: "sessionId", s: "sess-v2" }],
|
||||
);
|
||||
assert.ok(session.sentMessages.includes("say hi"));
|
||||
});
|
||||
|
||||
test("runTurn sends before connecting a resumed session and skips replayed history", async () => {
|
||||
let explicitlyConnected = false;
|
||||
const session = fakeSession([], { sessionId: "resumed-session" });
|
||||
session.connect = async () => {
|
||||
explicitlyConnected = true;
|
||||
};
|
||||
session.send = async (message) => {
|
||||
session.sentMessages.push(message);
|
||||
};
|
||||
session.stream = async function* stream() {
|
||||
if (explicitlyConnected) {
|
||||
yield {
|
||||
type: "assistant",
|
||||
message: { content: [{ type: "text", text: "old response" }] },
|
||||
};
|
||||
}
|
||||
yield {
|
||||
type: "assistant",
|
||||
message: { content: [{ type: "text", text: "new response" }] },
|
||||
};
|
||||
};
|
||||
const mgr = new CodebuddySessionManager({
|
||||
loadSdk: async () => ({
|
||||
unstable_v2_createSession: () => session,
|
||||
unstable_v2_resumeSession: () => session,
|
||||
}),
|
||||
});
|
||||
const { events, emitter } = collector();
|
||||
|
||||
const result = await mgr.runTurn({
|
||||
sessionKey: "resumed-key",
|
||||
prompt: "new question",
|
||||
attachments: [],
|
||||
options: { abortController: new AbortController() },
|
||||
emitter,
|
||||
sessionOptions: {},
|
||||
resumeSessionId: "resumed-session",
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { sessionId: "resumed-session", usedV2: true });
|
||||
assert.equal(explicitlyConnected, false);
|
||||
assert.deepEqual(session.sentMessages, ["new question"]);
|
||||
assert.deepEqual(
|
||||
events.filter((event) => event.k === "text").map((event) => event.t),
|
||||
["new response"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
events.filter((event) => event.k === "sessionId"),
|
||||
[{ k: "sessionId", s: "resumed-session" }],
|
||||
);
|
||||
});
|
||||
|
||||
test("runTurn does not connect or send when already aborted", async () => {
|
||||
let loadSdkCalls = 0;
|
||||
const mgr = new CodebuddySessionManager({
|
||||
loadSdk: async () => {
|
||||
loadSdkCalls += 1;
|
||||
return {};
|
||||
},
|
||||
});
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
const { events, emitter } = collector();
|
||||
|
||||
const result = await mgr.runTurn({
|
||||
sessionKey: "pre-aborted-key",
|
||||
prompt: "must not run",
|
||||
attachments: [],
|
||||
options: { abortController: controller },
|
||||
emitter,
|
||||
sessionOptions: {},
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { sessionId: null, usedV2: true });
|
||||
assert.equal(loadSdkCalls, 0);
|
||||
assert.deepEqual(events, [{ k: "done" }]);
|
||||
});
|
||||
|
||||
test("runTurn does not stream when aborted while the initial send connects", async () => {
|
||||
let releaseSend;
|
||||
const session = fakeSession([], { sessionId: "slow-connect-session" });
|
||||
session.send = (message) => new Promise((resolve) => {
|
||||
session.sentMessages.push(message);
|
||||
releaseSend = resolve;
|
||||
});
|
||||
const mgr = new CodebuddySessionManager({
|
||||
loadSdk: async () => ({
|
||||
unstable_v2_createSession: () => session,
|
||||
unstable_v2_resumeSession: () => session,
|
||||
}),
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const { events, emitter } = collector();
|
||||
|
||||
const runPromise = mgr.runTurn({
|
||||
sessionKey: "slow-connect-key",
|
||||
prompt: "must not run",
|
||||
attachments: [],
|
||||
options: { abortController: controller },
|
||||
emitter,
|
||||
sessionOptions: {},
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(typeof releaseSend, "function");
|
||||
|
||||
controller.abort();
|
||||
releaseSend();
|
||||
const result = await runPromise;
|
||||
|
||||
assert.deepEqual(result, {
|
||||
sessionId: "slow-connect-session",
|
||||
usedV2: true,
|
||||
});
|
||||
assert.deepEqual(session.sentMessages, ["must not run"]);
|
||||
assert.equal(session.interruptCalls, 1);
|
||||
assert.deepEqual(events, [{ k: "done" }]);
|
||||
});
|
||||
|
||||
test("runTurn treats an abort rejection while streaming as normal completion", async () => {
|
||||
let rejectStream;
|
||||
const session = fakeSession([], { sessionId: "stream-abort-session" });
|
||||
session.stream = async function* stream() {
|
||||
await new Promise((_resolve, reject) => {
|
||||
rejectStream = reject;
|
||||
});
|
||||
};
|
||||
session.interrupt = async () => {
|
||||
rejectStream?.(new Error("interrupted"));
|
||||
};
|
||||
const mgr = new CodebuddySessionManager({
|
||||
loadSdk: async () => ({
|
||||
unstable_v2_createSession: () => session,
|
||||
unstable_v2_resumeSession: () => session,
|
||||
}),
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const { events, emitter } = collector();
|
||||
|
||||
const runPromise = mgr.runTurn({
|
||||
sessionKey: "stream-abort-key",
|
||||
prompt: "wait",
|
||||
attachments: [],
|
||||
options: { abortController: controller },
|
||||
emitter,
|
||||
sessionOptions: {},
|
||||
});
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
controller.abort();
|
||||
|
||||
assert.deepEqual(await runPromise, {
|
||||
sessionId: "stream-abort-session",
|
||||
usedV2: true,
|
||||
});
|
||||
assert.ok(mgr.sessions.has("stream-abort-key"));
|
||||
assert.equal(session.closed, false);
|
||||
assert.deepEqual(events, [
|
||||
{ k: "sessionId", s: "stream-abort-session" },
|
||||
{ k: "done" },
|
||||
]);
|
||||
});
|
||||
|
||||
test("steer returns unsupported when no session exists", async () => {
|
||||
const mgr = new CodebuddySessionManager();
|
||||
const { emitter } = collector();
|
||||
const result = await mgr.steer({
|
||||
sessionKey: "nonexistent",
|
||||
prompt: "follow up",
|
||||
attachments: [],
|
||||
emitter,
|
||||
});
|
||||
assert.deepEqual(result, { status: "unsupported" });
|
||||
});
|
||||
|
||||
test("steer stays unsupported because Session.send resets the active SDK stream", async () => {
|
||||
const mgr = new CodebuddySessionManager();
|
||||
const session = fakeSession([]);
|
||||
mgr.sessions.set("steer-key", {
|
||||
session,
|
||||
fingerprint: computeOptionsFingerprint({}),
|
||||
});
|
||||
|
||||
const result = await mgr.steer({
|
||||
sessionKey: "steer-key",
|
||||
prompt: "now do this",
|
||||
attachments: [],
|
||||
});
|
||||
|
||||
assert.deepEqual(result, { status: "unsupported" });
|
||||
assert.deepEqual(session.sentMessages, []);
|
||||
});
|
||||
|
||||
test("closeSession removes and closes the session", () => {
|
||||
const mgr = new CodebuddySessionManager();
|
||||
const session = fakeSession([]);
|
||||
mgr.sessions.set("close-key", { session, fingerprint: null });
|
||||
|
||||
mgr.closeSession("close-key");
|
||||
assert.ok(!mgr.sessions.has("close-key"));
|
||||
assert.ok(session.closed);
|
||||
});
|
||||
|
||||
test("closeForChat closes all sessions matching the chat prefix", () => {
|
||||
const mgr = new CodebuddySessionManager();
|
||||
const s1 = fakeSession([]);
|
||||
const s2 = fakeSession([]);
|
||||
const s3 = fakeSession([]);
|
||||
mgr.sessions.set("chat1\u0000codebuddy\u0000/bin/cb\u0000sdk", { session: s1, fingerprint: null });
|
||||
mgr.sessions.set("chat1\u0000codebuddy\u0000/other/cb\u0000sdk", { session: s2, fingerprint: null });
|
||||
mgr.sessions.set("chat2\u0000codebuddy\u0000/bin/cb\u0000sdk", { session: s3, fingerprint: null });
|
||||
|
||||
mgr.closeForChat("chat1");
|
||||
assert.ok(!mgr.sessions.has("chat1\u0000codebuddy\u0000/bin/cb\u0000sdk"));
|
||||
assert.ok(!mgr.sessions.has("chat1\u0000codebuddy\u0000/other/cb\u0000sdk"));
|
||||
assert.ok(mgr.sessions.has("chat2\u0000codebuddy\u0000/bin/cb\u0000sdk"));
|
||||
assert.ok(s1.closed);
|
||||
assert.ok(s2.closed);
|
||||
assert.ok(!s3.closed);
|
||||
});
|
||||
|
||||
test("closeForChat cancels pending elicitations scoped to the chat", () => {
|
||||
const mgr = new CodebuddySessionManager();
|
||||
const resolved = [];
|
||||
mgr.elicitationPending.set("el-chat1", {
|
||||
resolve: (v) => resolved.push(["el-chat1", v]),
|
||||
reject: () => {},
|
||||
chatSessionId: "chat1",
|
||||
});
|
||||
mgr.elicitationPending.set("el-chat2", {
|
||||
resolve: (v) => resolved.push(["el-chat2", v]),
|
||||
reject: () => {},
|
||||
chatSessionId: "chat2",
|
||||
});
|
||||
|
||||
mgr.closeForChat("chat1");
|
||||
|
||||
assert.deepEqual(resolved, [["el-chat1", { action: "cancel" }]]);
|
||||
assert.ok(!mgr.elicitationPending.has("el-chat1"));
|
||||
assert.ok(mgr.elicitationPending.has("el-chat2"));
|
||||
});
|
||||
|
||||
test("closeAll closes every session", () => {
|
||||
const mgr = new CodebuddySessionManager();
|
||||
const s1 = fakeSession([]);
|
||||
const s2 = fakeSession([]);
|
||||
mgr.sessions.set("a", { session: s1, fingerprint: null });
|
||||
mgr.sessions.set("b", { session: s2, fingerprint: null });
|
||||
|
||||
mgr.closeAll();
|
||||
assert.equal(mgr.sessions.size, 0);
|
||||
assert.ok(s1.closed);
|
||||
assert.ok(s2.closed);
|
||||
});
|
||||
|
||||
test("closeAll cancels every pending elicitation", () => {
|
||||
const mgr = new CodebuddySessionManager();
|
||||
const resolved = [];
|
||||
mgr.elicitationPending.set("el-a", {
|
||||
resolve: (v) => resolved.push(["el-a", v]),
|
||||
reject: () => {},
|
||||
chatSessionId: "chat1",
|
||||
});
|
||||
mgr.elicitationPending.set("el-b", {
|
||||
resolve: (v) => resolved.push(["el-b", v]),
|
||||
reject: () => {},
|
||||
chatSessionId: "chat2",
|
||||
});
|
||||
|
||||
mgr.closeAll();
|
||||
|
||||
assert.equal(mgr.elicitationPending.size, 0);
|
||||
assert.deepEqual(resolved, [
|
||||
["el-a", { action: "cancel" }],
|
||||
["el-b", { action: "cancel" }],
|
||||
]);
|
||||
});
|
||||
|
||||
test("setModel returns false when session does not exist", async () => {
|
||||
const mgr = new CodebuddySessionManager();
|
||||
const result = await mgr.setModel("missing", "new-model");
|
||||
assert.equal(result, false);
|
||||
});
|
||||
|
||||
test("setModel delegates to the session", async () => {
|
||||
const mgr = new CodebuddySessionManager();
|
||||
const session = fakeSession([]);
|
||||
mgr.sessions.set("model-key", { session, fingerprint: null });
|
||||
|
||||
const result = await mgr.setModel("model-key", "glm-5");
|
||||
assert.equal(result, true);
|
||||
assert.equal(session._model, "glm-5");
|
||||
});
|
||||
|
||||
test("resolveElicitation resolves pending and returns true", () => {
|
||||
const mgr = new CodebuddySessionManager();
|
||||
let resolved;
|
||||
mgr.elicitationPending.set("el-1", {
|
||||
resolve: (v) => { resolved = v; },
|
||||
reject: () => {},
|
||||
});
|
||||
|
||||
const ok = mgr.resolveElicitation("el-1", { action: "accept" });
|
||||
assert.equal(ok, true);
|
||||
assert.deepEqual(resolved, { action: "accept" });
|
||||
assert.ok(!mgr.elicitationPending.has("el-1"));
|
||||
});
|
||||
|
||||
test("resolveElicitation returns false for unknown id", () => {
|
||||
const mgr = new CodebuddySessionManager();
|
||||
const ok = mgr.resolveElicitation("unknown", { action: "cancel" });
|
||||
assert.equal(ok, false);
|
||||
});
|
||||
Reference in New Issue
Block a user