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
105 lines
3.2 KiB
JavaScript
105 lines
3.2 KiB
JavaScript
"use strict";
|
|
|
|
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const { EventEmitter } = require("node:events");
|
|
const { setTimeout: delay } = require("node:timers/promises");
|
|
|
|
const mcpServerBridge = require("./mcpServerBridge.cjs");
|
|
|
|
function createFakeWindow() {
|
|
const contents = new EventEmitter();
|
|
contents.id = Math.floor(Math.random() * 1e6);
|
|
contents.send = (channel, payload) => {
|
|
contents.emit("send", channel, payload);
|
|
};
|
|
contents.isDestroyed = () => false;
|
|
return {
|
|
isDestroyed: () => false,
|
|
webContents: contents,
|
|
};
|
|
}
|
|
|
|
function stubNow(t, startMs) {
|
|
const realNow = Date.now;
|
|
let now = startMs;
|
|
Date.now = () => now;
|
|
t.after(() => {
|
|
Date.now = realNow;
|
|
});
|
|
return {
|
|
advance: (deltaMs) => {
|
|
now += deltaMs;
|
|
},
|
|
};
|
|
}
|
|
|
|
test("cancelApprovalTimeoutFromRenderer keeps the absolute MCP deadline armed", async (t) => {
|
|
const win = createFakeWindow();
|
|
mcpServerBridge.setMainWindowGetter(() => win);
|
|
t.after(() => {
|
|
mcpServerBridge.clearPendingApprovals();
|
|
mcpServerBridge.setMainWindowGetter(() => null);
|
|
});
|
|
|
|
const requests = [];
|
|
const cleared = [];
|
|
win.webContents.on("send", (channel, payload) => {
|
|
if (channel === "netcatty:ai:mcp:approval-request") requests.push(payload);
|
|
if (channel === "netcatty:ai:mcp:approval-cleared") {
|
|
cleared.push(...(payload?.approvalIds || []));
|
|
}
|
|
});
|
|
|
|
const clock = stubNow(t, 1_000_000);
|
|
const approvalPromise = mcpServerBridge.requestApprovalFromRenderer(
|
|
"terminal_execute",
|
|
{ command: "echo hi" },
|
|
"chat-mcp-absolute",
|
|
);
|
|
|
|
assert.equal(requests.length, 1);
|
|
const approvalId = requests[0].approvalId;
|
|
|
|
// Jump close to the 110s absolute ceiling, then cancel idle. Remaining absolute ~40ms.
|
|
clock.advance(109_960);
|
|
assert.equal(mcpServerBridge.cancelApprovalTimeoutFromRenderer(approvalId), true);
|
|
assert.equal(mcpServerBridge.cancelApprovalTimeoutFromRenderer(approvalId), false);
|
|
|
|
await delay(20);
|
|
assert.equal(cleared.includes(approvalId), false, "must stay pending before absolute expiry");
|
|
|
|
const outcome = await Promise.race([
|
|
approvalPromise.then((approved) => ({ approved })),
|
|
delay(120).then(() => ({ approved: "timeout-wait" })),
|
|
]);
|
|
assert.deepEqual(outcome, { approved: false });
|
|
assert.ok(cleared.includes(approvalId));
|
|
});
|
|
|
|
test("cancelApprovalTimeoutFromRenderer still allows explicit approve before absolute expiry", async (t) => {
|
|
const win = createFakeWindow();
|
|
mcpServerBridge.setMainWindowGetter(() => win);
|
|
t.after(() => {
|
|
mcpServerBridge.clearPendingApprovals();
|
|
mcpServerBridge.setMainWindowGetter(() => null);
|
|
});
|
|
|
|
const requests = [];
|
|
win.webContents.on("send", (channel, payload) => {
|
|
if (channel === "netcatty:ai:mcp:approval-request") requests.push(payload);
|
|
});
|
|
|
|
stubNow(t, 2_000_000);
|
|
const approvalPromise = mcpServerBridge.requestApprovalFromRenderer(
|
|
"sftp_write",
|
|
{ path: "/tmp/x" },
|
|
"chat-mcp-approve",
|
|
);
|
|
const approvalId = requests[0].approvalId;
|
|
|
|
assert.equal(mcpServerBridge.cancelApprovalTimeoutFromRenderer(approvalId), true);
|
|
mcpServerBridge.resolveApprovalFromRenderer(approvalId, true);
|
|
assert.equal(await approvalPromise, true);
|
|
});
|