Files
NetMesh/electron/bridges/mcpServerBridge.hostOpenScope.test.cjs
zhaolei 3c72efcb7f
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
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

817 lines
24 KiB
JavaScript

"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
function loadFreshBridge() {
const bridgePath = require.resolve("./mcpServerBridge.cjs");
delete require.cache[bridgePath];
// Also clear retain/ownership deps so a fresh bridge gets fresh module state.
for (const key of Object.keys(require.cache)) {
if (key.includes(`${require("node:path").sep}mcpServerBridge${require("node:path").sep}`)) {
delete require.cache[key];
}
}
return require("./mcpServerBridge.cjs");
}
test("sidebar scope replace keeps host_open-owned sessions in the chat scope", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
bridge.init({
sessions: new Map(),
electronModule: null,
});
bridge.setPermissionMode("auto");
bridge.setVaultAgentInvoker(async (op) => {
if (op === "host.open") {
return { ok: true, sessionId: "sess-opened", hostId: "host-b", status: "connecting" };
}
return { ok: false, error: `unexpected op ${op}` };
});
bridge.updateSessionMetadata([
{
sessionId: "sess-original",
hostname: "10.0.0.1",
label: "server-a",
connected: true,
hostId: "host-a",
},
{
sessionId: "sess-opened",
hostname: "10.0.0.2",
label: "server-b",
connected: false,
hostId: "host-b",
protocol: "ssh",
},
], "chat-1");
const opened = await bridge.dispatchBuiltinRpc("public/vault/hosts/open", {
chatSessionId: "chat-1",
hostId: "host-b",
});
assert.equal(opened.ok, true);
assert.equal(opened.sessionId, "sess-opened");
// AIChatSidePanel-style full replace of only the focused tab.
bridge.updateSessionMetadata([
{
sessionId: "sess-original",
hostname: "10.0.0.1",
label: "server-a",
connected: true,
hostId: "host-a",
},
], "chat-1");
assert.deepEqual(
bridge.getScopedSessionIds("chat-1").sort(),
["sess-opened", "sess-original"],
);
});
test("authoritative empty scope replace still clears host_open-owned sessions", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
bridge.init({
sessions: new Map(),
electronModule: null,
});
bridge.setPermissionMode("auto");
bridge.setVaultAgentInvoker(async () => ({
ok: true,
sessionId: "sess-opened",
hostId: "host-b",
status: "connecting",
}));
bridge.updateSessionMetadata([
{ sessionId: "sess-opened", hostname: "10.0.0.2", label: "server-b", connected: false },
], "chat-1");
await bridge.dispatchBuiltinRpc("public/vault/hosts/open", {
chatSessionId: "chat-1",
hostId: "host-b",
});
bridge.updateSessionMetadata([], "chat-1");
assert.deepEqual(bridge.getScopedSessionIds("chat-1"), []);
// A later non-empty sync must not resurrect the cleared owned session via
// cross-scope fallback / stale ownership.
bridge.updateSessionMetadata([
{
sessionId: "sess-opened",
hostname: "10.0.0.2",
label: "server-b",
connected: true,
hostId: "host-b",
},
], "__external_mcp__");
bridge.updateSessionMetadata([
{
sessionId: "sess-original",
hostname: "10.0.0.1",
label: "server-a",
connected: true,
hostId: "host-a",
},
], "chat-1");
assert.deepEqual(bridge.getScopedSessionIds("chat-1"), ["sess-original"]);
assert.equal(bridge.getSessionMeta("sess-opened", "chat-1"), null);
});
test("retained host_open metadata refreshes connected from another scope", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
bridge.init({
sessions: new Map(),
electronModule: null,
});
bridge.setPermissionMode("auto");
bridge.setVaultAgentInvoker(async () => ({
ok: true,
sessionId: "sess-opened",
hostId: "host-b",
status: "connecting",
}));
bridge.updateSessionMetadata([
{
sessionId: "sess-opened",
hostname: "10.0.0.2",
label: "server-b",
connected: false,
hostId: "host-b",
protocol: "ssh",
},
], "chat-1");
await bridge.dispatchBuiltinRpc("public/vault/hosts/open", {
chatSessionId: "chat-1",
hostId: "host-b",
});
// Another surface (External MCP / opened tab) learns the session connected.
bridge.updateSessionMetadata([
{
sessionId: "sess-opened",
hostname: "10.0.0.2",
label: "server-b",
connected: true,
hostId: "host-b",
protocol: "ssh",
username: "root",
},
], "__external_mcp__");
// Original chat sidebar still pushes only the focused tab.
bridge.updateSessionMetadata([
{
sessionId: "sess-original",
hostname: "10.0.0.1",
label: "server-a",
connected: true,
hostId: "host-a",
},
], "chat-1");
assert.equal(bridge.getSessionMeta("sess-opened", "chat-1")?.connected, true);
assert.equal(bridge.getSessionMeta("sess-opened", "chat-1")?.username, "root");
});
test("retained host_open metadata accepts a later disconnect from another scope", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
bridge.init({
sessions: new Map(),
electronModule: null,
});
bridge.setPermissionMode("auto");
bridge.setVaultAgentInvoker(async () => ({
ok: true,
sessionId: "sess-opened",
hostId: "host-b",
status: "connecting",
}));
bridge.updateSessionMetadata([
{
sessionId: "sess-opened",
hostname: "10.0.0.2",
label: "server-b",
connected: true,
hostId: "host-b",
protocol: "ssh",
},
], "chat-1");
await bridge.dispatchBuiltinRpc("public/vault/hosts/open", {
chatSessionId: "chat-1",
hostId: "host-b",
});
// The focused tab moved away; External MCP later learns the host dropped.
bridge.updateSessionMetadata([
{
sessionId: "sess-opened",
hostname: "10.0.0.2",
label: "server-b",
connected: false,
hostId: "host-b",
protocol: "ssh",
},
], "__external_mcp__");
bridge.updateSessionMetadata([
{
sessionId: "sess-original",
hostname: "10.0.0.1",
label: "server-a",
connected: true,
hostId: "host-a",
},
], "chat-1");
assert.equal(bridge.getSessionMeta("sess-opened", "chat-1")?.connected, false);
});
test("getSessionMeta clears stopped port forwards from another scope while owned session stays connected", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
bridge.init({
sessions: new Map(),
electronModule: null,
});
bridge.setPermissionMode("auto");
bridge.setVaultAgentInvoker(async () => ({
ok: true,
sessionId: "sess-opened",
hostId: "host-b",
status: "connecting",
}));
const forwards = [{ ruleId: "fwd-1", localPort: 8080, status: "active" }];
bridge.updateSessionMetadata([
{
sessionId: "sess-opened",
hostname: "10.0.0.2",
label: "server-b",
connected: true,
hostId: "host-b",
protocol: "ssh",
activePortForwards: forwards,
},
], "chat-1");
await bridge.dispatchBuiltinRpc("public/vault/hosts/open", {
chatSessionId: "chat-1",
hostId: "host-b",
});
// Another surface reports the session still up, but forwards already stopped.
// chat-1 does not get another metadata replace (focused tab stayed elsewhere).
bridge.updateSessionMetadata([
{
sessionId: "sess-opened",
hostname: "10.0.0.2",
label: "server-b",
connected: true,
hostId: "host-b",
protocol: "ssh",
activePortForwards: [],
},
], "__external_mcp__");
assert.equal(bridge.getSessionMeta("sess-opened", "chat-1")?.connected, true);
assert.deepEqual(bridge.getSessionMeta("sess-opened", "chat-1")?.activePortForwards, []);
});
test("getSessionMeta does not let another scope clear forwards on a live non-owned session", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
bridge.init({
sessions: new Map(),
electronModule: null,
});
const forwards = [{ ruleId: "fwd-1", localPort: 8080, status: "active" }];
bridge.updateSessionMetadata([
{
sessionId: "sess-live",
hostname: "10.0.0.1",
label: "server-a",
connected: true,
hostId: "host-a",
activePortForwards: forwards,
},
], "chat-1");
bridge.updateSessionMetadata([
{
sessionId: "sess-live",
hostname: "10.0.0.1",
label: "server-a",
connected: true,
hostId: "host-a",
activePortForwards: [],
},
], "__external_mcp__");
assert.deepEqual(
bridge.getSessionMeta("sess-live", "chat-1")?.activePortForwards,
forwards,
);
});
test("ordinary tab close forgets host_open ownership so sidebar sync cannot revive ghosts", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
const closedListeners = new Set();
bridge.init({
sessions: new Map(),
electronModule: null,
terminalWorkerManager: {
onSessionClosed(listener) {
closedListeners.add(listener);
return {
dispose: () => closedListeners.delete(listener),
};
},
},
});
bridge.setPermissionMode("auto");
bridge.setVaultAgentInvoker(async () => ({
ok: true,
sessionId: "sess-opened",
hostId: "host-b",
status: "connecting",
}));
bridge.updateSessionMetadata([
{
sessionId: "sess-opened",
hostname: "10.0.0.2",
label: "server-b",
connected: true,
hostId: "host-b",
},
], "chat-1");
await bridge.dispatchBuiltinRpc("public/vault/hosts/open", {
chatSessionId: "chat-1",
hostId: "host-b",
});
// User closes the host_open tab through the normal UI / worker path.
for (const listener of closedListeners) {
listener({ sessionId: "sess-opened", reason: "closed", explicit: true });
}
bridge.updateSessionMetadata([
{
sessionId: "sess-original",
hostname: "10.0.0.1",
label: "server-a",
connected: true,
hostId: "host-a",
},
], "chat-1");
assert.deepEqual(bridge.getScopedSessionIds("chat-1"), ["sess-original"]);
assert.equal(bridge.getSessionMeta("sess-opened", "chat-1"), null);
});
test("recoverable worker exits keep host_open ownership for reconnect", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
const closedListeners = new Set();
bridge.init({
sessions: new Map(),
electronModule: null,
terminalWorkerManager: {
onSessionClosed(listener) {
closedListeners.add(listener);
return {
dispose: () => closedListeners.delete(listener),
};
},
},
});
bridge.setPermissionMode("auto");
bridge.setVaultAgentInvoker(async () => ({
ok: true,
sessionId: "sess-opened",
hostId: "host-b",
status: "connecting",
}));
bridge.updateSessionMetadata([
{
sessionId: "sess-opened",
hostname: "10.0.0.2",
label: "server-b",
connected: true,
hostId: "host-b",
},
], "chat-1");
await bridge.dispatchBuiltinRpc("public/vault/hosts/open", {
chatSessionId: "chat-1",
hostId: "host-b",
});
for (const event of [
{ reason: "error" },
{ reason: "worker-exit" },
{ reason: "superseded" },
{ reason: "closed" },
// Shell exits that leave the tab for reconnect (missing/nonzero exitCode).
{ reason: "exited" },
{ reason: "exited", exitCode: 1 },
]) {
for (const listener of closedListeners) {
listener({ sessionId: "sess-opened", ...event });
}
}
// After reconnect, sidebar may push only the focused tab; ownership must
// still retain the host_open session.
bridge.updateSessionMetadata([
{
sessionId: "sess-original",
hostname: "10.0.0.1",
label: "server-a",
connected: true,
hostId: "host-a",
},
], "chat-1");
assert.deepEqual(
bridge.getScopedSessionIds("chat-1").sort(),
["sess-opened", "sess-original"],
);
});
test("clean shell exit keeps ownership until the tab is explicitly closed", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
const closedListeners = new Set();
bridge.init({
sessions: new Map(),
electronModule: null,
terminalWorkerManager: {
onSessionClosed(listener) {
closedListeners.add(listener);
return {
dispose: () => closedListeners.delete(listener),
};
},
},
});
bridge.setPermissionMode("auto");
bridge.setVaultAgentInvoker(async () => ({
ok: true,
sessionId: "sess-opened",
hostId: "host-b",
status: "connecting",
}));
bridge.updateSessionMetadata([
{
sessionId: "sess-opened",
hostname: "10.0.0.2",
label: "server-b",
connected: true,
hostId: "host-b",
},
], "chat-1");
await bridge.dispatchBuiltinRpc("public/vault/hosts/open", {
chatSessionId: "chat-1",
hostId: "host-b",
});
// Worker reports a clean exit; auto-close may still leave the tab briefly,
// and disabled auto-close keeps it for reconnect — ownership stays.
for (const listener of closedListeners) {
listener({ sessionId: "sess-opened", reason: "exited", exitCode: 0 });
}
bridge.updateSessionMetadata([
{
sessionId: "sess-original",
hostname: "10.0.0.1",
label: "server-a",
connected: true,
hostId: "host-a",
},
], "chat-1");
assert.deepEqual(
bridge.getScopedSessionIds("chat-1").sort(),
["sess-opened", "sess-original"],
);
// Renderer tab close is the authoritative ownership drop.
for (const listener of closedListeners) {
listener({ sessionId: "sess-opened", reason: "closed", explicit: true });
}
bridge.updateSessionMetadata([
{
sessionId: "sess-original",
hostname: "10.0.0.1",
label: "server-a",
connected: true,
hostId: "host-a",
},
], "chat-1");
assert.deepEqual(bridge.getScopedSessionIds("chat-1"), ["sess-original"]);
});
test("latest metadata revision wins across multiple chat and external scopes", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
bridge.init({ sessions: new Map(), electronModule: null });
bridge.setPermissionMode("auto");
bridge.setVaultAgentInvoker(async () => ({
ok: true,
sessionId: "sess-opened",
hostId: "host-b",
status: "connecting",
}));
bridge.updateSessionMetadata([{
sessionId: "sess-opened",
connected: true,
activePortForwards: [{ ruleId: "old-forward" }],
}], "chat-stale");
bridge.updateSessionMetadata([{
sessionId: "sess-opened",
connected: true,
activePortForwards: [],
}], "chat-owner");
await bridge.dispatchBuiltinRpc("public/vault/hosts/open", {
chatSessionId: "chat-owner",
hostId: "host-b",
});
bridge.updateSessionMetadata([{
sessionId: "sess-opened",
connected: false,
activePortForwards: [],
}], "__external_mcp__");
assert.equal(bridge.getSessionMeta("sess-opened", "chat-owner")?.connected, false);
assert.deepEqual(bridge.getSessionMeta("sess-opened", "chat-owner")?.activePortForwards, []);
bridge.updateSessionMetadata([{
sessionId: "sess-opened",
connected: true,
username: "reconnected-user",
activePortForwards: [{ ruleId: "new-forward" }],
}], "chat-live");
assert.equal(bridge.getSessionMeta("sess-opened", "chat-owner")?.connected, true);
assert.equal(bridge.getSessionMeta("sess-opened", "chat-owner")?.username, "reconnected-user");
assert.deepEqual(
bridge.getSessionMeta("sess-opened", "chat-owner")?.activePortForwards,
[{ ruleId: "new-forward" }],
);
});
test("host_open finishing after an empty scope replace restores its returned session", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
bridge.init({ sessions: new Map(), electronModule: null });
bridge.setPermissionMode("auto");
let finishOpen;
bridge.setVaultAgentInvoker(() => new Promise((resolve) => {
finishOpen = resolve;
}));
bridge.updateSessionMetadata([{ sessionId: "sess-original", connected: true }], "chat-race");
const opening = bridge.dispatchBuiltinRpc("public/vault/hosts/open", {
chatSessionId: "chat-race",
hostId: "host-b",
});
bridge.updateSessionMetadata([], "chat-race");
finishOpen({
ok: true,
sessionId: "sess-opened",
hostId: "host-b",
status: "connecting",
protocol: "ssh",
host: { id: "host-b", hostname: "10.0.0.2", label: "server-b", username: "root" },
});
const opened = await opening;
assert.equal(opened.ok, true);
assert.deepEqual(bridge.getScopedSessionIds("chat-race"), ["sess-opened"]);
assert.equal(bridge.getSessionMeta("sess-opened", "chat-race")?.label, "server-b");
assert.equal(bridge.getSessionMeta("sess-opened", "chat-race")?.connected, false);
});
test("initial external scope seed selects the latest session snapshot", (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
bridge.init({ sessions: new Map(), electronModule: null });
bridge.updateSessionMetadata([{
sessionId: "sess-shared",
connected: true,
username: "old-user",
}], "chat-old");
bridge.updateSessionMetadata([{
sessionId: "sess-shared",
connected: false,
username: "new-user",
}], "chat-new");
const seeded = bridge.syncLiveSessionsToExternalScope();
assert.equal(seeded.seeded, true);
assert.equal(bridge.getSessionMeta("sess-shared", "__external_mcp__")?.connected, false);
assert.equal(bridge.getSessionMeta("sess-shared", "__external_mcp__")?.username, "new-user");
});
test("explicit close before host_open returns cannot revive the closed session", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
bridge.init({ sessions: new Map(), electronModule: null });
bridge.setPermissionMode("auto");
let finishOpen;
bridge.setVaultAgentInvoker(() => new Promise((resolve) => {
finishOpen = resolve;
}));
bridge.updateSessionMetadata([{ sessionId: "sess-original", connected: true }], "chat-close-race");
const opening = bridge.dispatchBuiltinRpc("public/vault/hosts/open", {
chatSessionId: "chat-close-race",
hostId: "host-b",
});
// Renderer-side host.open creates and merges the tab before returning its
// result to the main process. Simulate the user closing that tab immediately.
bridge.mergeSessionMetadata([{
sessionId: "sess-opened",
hostId: "host-b",
connected: false,
}], "chat-close-race");
bridge.forgetClosedTerminalSession("sess-opened");
finishOpen({
ok: true,
sessionId: "sess-opened",
hostId: "host-b",
status: "connecting",
protocol: "ssh",
});
assert.equal((await opening).ok, true);
bridge.updateSessionMetadata([{ sessionId: "sess-original", connected: true }], "chat-close-race");
assert.deepEqual(bridge.getScopedSessionIds("chat-close-race"), ["sess-original"]);
assert.equal(bridge.getSessionMeta("sess-opened", "chat-close-race"), null);
});
test("app live sync makes host_open session usable without External MCP or a second AI panel", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
const requests = [];
bridge.init({
sessions: new Map(),
electronModule: null,
terminalWorkerManager: {
request(channel, payload, options) {
requests.push({ channel, payload, options });
if (channel === "netcatty:portforward:list") return Promise.resolve([]);
if (channel === "netcatty:ai:exec") {
return Promise.resolve({ ok: true, stdout: "ready\n", stderr: "", exitCode: 0 });
}
throw new Error(`unexpected worker request: ${channel}`);
},
},
});
bridge.setPermissionMode("auto");
bridge.setVaultAgentInvoker(async () => ({
ok: true,
sessionId: "sess-opened",
hostId: "host-b",
status: "connecting",
protocol: "ssh",
host: { id: "host-b", hostname: "10.0.0.2", label: "server-b", username: "root" },
}));
bridge.updateSessionMetadata([{ sessionId: "sess-original", connected: true }], "chat-owner");
await bridge.dispatchBuiltinRpc("public/vault/hosts/open", {
chatSessionId: "chat-owner",
hostId: "host-b",
});
// The app-level session state observes the connection. No External MCP scope
// and no AI panel for the opened tab are involved.
bridge.updateLiveSessionMetadata([{
sessionId: "sess-opened",
hostId: "host-b",
hostname: "10.0.0.2",
label: "server-b",
username: "root",
protocol: "ssh",
connected: true,
}]);
// Returning to the original tab replaces its sidebar payload with only A.
bridge.updateSessionMetadata([{ sessionId: "sess-original", connected: true }], "chat-owner");
assert.deepEqual(
bridge.getScopedSessionIds("chat-owner").sort(),
["sess-opened", "sess-original"],
);
assert.equal(bridge.getSessionMeta("sess-opened", "chat-owner")?.connected, true);
const environment = await bridge.dispatchBuiltinRpc("netcatty/getContext", {
chatSessionId: "chat-owner",
});
const openedHost = environment.hosts.find((host) => host.sessionId === "sess-opened");
assert.equal(openedHost?.connected, true);
const executed = await bridge.dispatchBuiltinRpc("netcatty/exec", {
chatSessionId: "chat-owner",
sessionId: "sess-opened",
command: "pwd",
});
assert.deepEqual(executed, { ok: true, stdout: "ready\n", stderr: "", exitCode: 0 });
assert.equal(requests.some((entry) => entry.channel === "netcatty:ai:exec"), true);
});
test("chat cleanup during host_open closes the late-created unowned session", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
bridge.init({ sessions: new Map(), electronModule: null });
bridge.setPermissionMode("auto");
let finishOpen;
const calls = [];
bridge.setVaultAgentInvoker((op, params) => {
calls.push({ op, params });
if (op === "host.open") {
return new Promise((resolve) => {
finishOpen = resolve;
});
}
if (op === "session.close") return Promise.resolve({ ok: true, sessionId: params.sessionId });
throw new Error(`unexpected op ${op}`);
});
bridge.updateSessionMetadata([{ sessionId: "sess-original", connected: true }], "chat-deleted");
const opening = bridge.dispatchBuiltinRpc("public/vault/hosts/open", {
chatSessionId: "chat-deleted",
hostId: "host-b",
});
await bridge.cleanupScopedMetadata("chat-deleted");
// Renderer host.open can still finish its own merge after chat cleanup.
bridge.mergeSessionMetadata([{
sessionId: "sess-opened",
hostId: "host-b",
connected: false,
}], "chat-deleted");
finishOpen({
ok: true,
sessionId: "sess-opened",
hostId: "host-b",
status: "connecting",
protocol: "ssh",
});
assert.equal((await opening).ok, true);
assert.deepEqual(calls.map(({ op }) => op), ["host.open", "session.close"]);
assert.deepEqual(bridge.getScopedSessionIds("chat-deleted"), []);
assert.equal(bridge.getSessionMeta("sess-opened", "chat-deleted"), null);
});
test("failed compensating close keeps the late-created session tracked for retry", async (t) => {
const bridge = loadFreshBridge();
t.after(() => bridge.cleanup());
bridge.init({ sessions: new Map(), electronModule: null });
bridge.setPermissionMode("auto");
let finishOpen;
bridge.setVaultAgentInvoker((op) => {
if (op === "host.open") {
return new Promise((resolve) => {
finishOpen = resolve;
});
}
if (op === "session.close") {
return Promise.resolve({ ok: false, error: "renderer unavailable" });
}
throw new Error(`unexpected op ${op}`);
});
const opening = bridge.dispatchBuiltinRpc("public/vault/hosts/open", {
chatSessionId: "chat-deleted",
hostId: "host-b",
});
await bridge.cleanupScopedMetadata("chat-deleted");
bridge.mergeSessionMetadata([{
sessionId: "sess-opened",
hostId: "host-b",
connected: false,
}], "chat-deleted");
finishOpen({ ok: true, sessionId: "sess-opened", hostId: "host-b", status: "connecting" });
assert.equal((await opening).ok, true);
assert.deepEqual(bridge.getScopedSessionIds("chat-deleted"), []);
assert.equal(bridge.getSessionMeta("sess-opened", "chat-deleted"), null);
assert.equal(
bridge.reportOpenedSessionActivity({ sessionId: "sess-opened", phase: "touch" }),
true,
);
});