mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-09 20:50:20 +08:00
210 lines
7.8 KiB
JavaScript
210 lines
7.8 KiB
JavaScript
"use strict";
|
|
|
|
const assert = require("node:assert/strict");
|
|
const net = require("node:net");
|
|
const os = require("node:os");
|
|
const path = require("node:path");
|
|
const { mkdtempSync, rmSync } = require("node:fs");
|
|
const test = require("node:test");
|
|
|
|
const {
|
|
CODEX_IPC_METHOD_VERSIONS,
|
|
CodexIpcClient,
|
|
IpcFrameDecoder,
|
|
applyIpcPatches,
|
|
encodeIpcFrame,
|
|
} = require("../vscode-extension/dist/codexIpc.js");
|
|
|
|
function waitFor(predicate, timeoutMs = 2_000) {
|
|
const started = Date.now();
|
|
return new Promise((resolve, reject) => {
|
|
const poll = () => {
|
|
if (predicate()) return resolve();
|
|
if (Date.now() - started >= timeoutMs) return reject(new Error("timed out waiting for fixture"));
|
|
setTimeout(poll, 5);
|
|
};
|
|
poll();
|
|
});
|
|
}
|
|
|
|
test("private IPC framing handles split UTF-8 frames", () => {
|
|
const message = {
|
|
type: "broadcast",
|
|
method: "thread-stream-following-changed",
|
|
sourceClientId: "client-1",
|
|
version: 1,
|
|
params: { conversationId: "thread-1", hostId: "local", following: true, text: "中文" },
|
|
};
|
|
const frame = encodeIpcFrame(message);
|
|
const decoder = new IpcFrameDecoder();
|
|
const first = decoder.push(frame.subarray(0, 3));
|
|
assert.deepEqual(first, []);
|
|
const second = decoder.push(frame.subarray(3, frame.length - 1));
|
|
assert.deepEqual(second, []);
|
|
assert.deepEqual(decoder.push(frame.subarray(frame.length - 1)), [message]);
|
|
});
|
|
|
|
test("applyIpcPatches updates a conversation snapshot", () => {
|
|
const initial = { turns: [{ items: [{ text: "old" }] }], status: "idle" };
|
|
const next = applyIpcPatches(initial, [
|
|
{ op: "replace", path: ["turns", 0, "items", 0, "text"], value: "new" },
|
|
{ op: "add", path: ["turns", 0, "items", 1], value: { text: "second" } },
|
|
{ op: "replace", path: ["status"], value: "active" },
|
|
]);
|
|
assert.deepEqual(next, {
|
|
turns: [{ items: [{ text: "new" }, { text: "second" }] }],
|
|
status: "active",
|
|
});
|
|
});
|
|
|
|
test("fixture owner receives follow/start/steer/interrupt/approval requests", async () => {
|
|
const temp = mkdtempSync(path.join(os.tmpdir(), "codex-ipc-fixture-"));
|
|
const socketPath = path.join(temp, "ipc.sock");
|
|
const threadId = "11111111-1111-4111-8111-111111111111";
|
|
const ownerId = "owner-client";
|
|
const requests = [];
|
|
const followingBroadcasts = [];
|
|
let fixtureSocket;
|
|
const server = net.createServer((socket) => {
|
|
fixtureSocket = socket;
|
|
const decoder = new IpcFrameDecoder();
|
|
socket.on("data", (chunk) => {
|
|
for (const message of decoder.push(chunk)) {
|
|
if (message.type === "request" && message.method === "initialize") {
|
|
socket.write(encodeIpcFrame({
|
|
type: "response",
|
|
requestId: message.requestId,
|
|
resultType: "success",
|
|
method: "initialize",
|
|
handledByClientId: "fixture-client",
|
|
result: { clientId: "fixture-client" },
|
|
}));
|
|
continue;
|
|
}
|
|
if (message.type === "broadcast" && message.method === "thread-stream-following-changed") {
|
|
followingBroadcasts.push(message);
|
|
const target = message.sourceClientId;
|
|
socket.write(encodeIpcFrame({
|
|
type: "broadcast",
|
|
method: "thread-stream-state-changed",
|
|
sourceClientId: ownerId,
|
|
targetClientIds: [target],
|
|
version: CODEX_IPC_METHOD_VERSIONS["thread-stream-state-changed"],
|
|
params: {
|
|
conversationId: threadId,
|
|
hostId: "local",
|
|
change: {
|
|
type: "snapshot",
|
|
revision: 1,
|
|
conversationState: { id: threadId, title: "fixture", turns: [], requests: [] },
|
|
},
|
|
},
|
|
}));
|
|
continue;
|
|
}
|
|
if (message.type === "request") {
|
|
requests.push(message);
|
|
socket.write(encodeIpcFrame({
|
|
type: "response",
|
|
requestId: message.requestId,
|
|
resultType: "success",
|
|
method: message.method,
|
|
handledByClientId: ownerId,
|
|
result: { method: message.method, ok: true },
|
|
}));
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
try {
|
|
await new Promise((resolve, reject) => {
|
|
server.once("error", reject);
|
|
server.listen(socketPath, resolve);
|
|
});
|
|
const client = new CodexIpcClient({ socketPath, autoReconnect: false });
|
|
const streamEvents = [];
|
|
client.onStreamEvent((event) => streamEvents.push(event));
|
|
await client.connect();
|
|
await client.followConversation(threadId);
|
|
await waitFor(() => streamEvents.some((event) => event.kind === "snapshot"));
|
|
assert.equal(client.getConversationState(threadId).ownerClientId, ownerId);
|
|
fixtureSocket.write(encodeIpcFrame({
|
|
type: "broadcast",
|
|
method: "thread-stream-following-status-requested",
|
|
sourceClientId: ownerId,
|
|
targetClientIds: ["fixture-client"],
|
|
version: CODEX_IPC_METHOD_VERSIONS["thread-stream-following-status-requested"],
|
|
params: { conversationId: threadId, hostId: "local" },
|
|
}));
|
|
await waitFor(() => followingBroadcasts.length >= 2);
|
|
assert.deepEqual(followingBroadcasts[1].targetClientIds, [ownerId]);
|
|
assert.deepEqual(followingBroadcasts[1].params, {
|
|
conversationId: threadId,
|
|
hostId: "local",
|
|
following: true,
|
|
});
|
|
|
|
await client.startTurn(threadId, "hello", { ownerClientId: ownerId });
|
|
await client.steerTurn(threadId, "follow-up", { ownerClientId: ownerId });
|
|
await client.updateThreadSettings(threadId, {
|
|
model: "gpt-5.6-sol",
|
|
effort: "ultra",
|
|
multiAgentMode: "explicitRequestOnly",
|
|
}, { ownerClientId: ownerId });
|
|
await client.interruptTurn(threadId, { mode: "user-stop", expectedTurnId: "turn-1", ownerClientId: ownerId });
|
|
await client.respondCommandApproval(threadId, 7, "decline", { ownerClientId: ownerId });
|
|
await client.respondFileApproval(threadId, "8", "cancel", { ownerClientId: ownerId });
|
|
await client.respondPermissionsApproval(threadId, 9, { permissions: {}, scope: "turn" }, { ownerClientId: ownerId });
|
|
await client.respondUserInput(threadId, 10, { answers: {} }, { ownerClientId: ownerId });
|
|
await client.respondMcpElicitation(threadId, 11, { action: "decline", content: null, _meta: null }, { ownerClientId: ownerId });
|
|
|
|
assert.deepEqual(requests.map((request) => request.method), [
|
|
"thread-follower-start-turn",
|
|
"thread-follower-steer-turn",
|
|
"thread-follower-update-thread-settings",
|
|
"thread-follower-interrupt-turn",
|
|
"thread-follower-command-approval-decision",
|
|
"thread-follower-file-approval-decision",
|
|
"thread-follower-permissions-request-approval-response",
|
|
"thread-follower-submit-user-input",
|
|
"thread-follower-submit-mcp-server-elicitation-response",
|
|
]);
|
|
assert.deepEqual(requests[0].params, {
|
|
conversationId: threadId,
|
|
turnStart: {
|
|
request: {
|
|
threadId,
|
|
input: [{ type: "text", text: "hello", text_elements: [] }],
|
|
},
|
|
context: { inheritThreadSettings: true },
|
|
},
|
|
});
|
|
assert.deepEqual(requests[2].params, {
|
|
conversationId: threadId,
|
|
threadSettings: {
|
|
model: "gpt-5.6-sol",
|
|
effort: "ultra",
|
|
multiAgentMode: "explicitRequestOnly",
|
|
},
|
|
});
|
|
assert.equal(requests[2].version, 1);
|
|
assert.deepEqual(requests[3].params, {
|
|
conversationId: threadId,
|
|
mode: "user-stop",
|
|
expectedTurnId: "turn-1",
|
|
});
|
|
assert.equal(requests[3].version, 4);
|
|
assert.deepEqual(requests[4].params, { conversationId: threadId, requestId: 7, decision: "decline" });
|
|
assert.deepEqual(requests[8].params, {
|
|
conversationId: threadId,
|
|
requestId: 11,
|
|
response: { action: "decline", content: null, _meta: null },
|
|
});
|
|
await client.dispose();
|
|
} finally {
|
|
await new Promise((resolve) => server.close(resolve));
|
|
rmSync(temp, { recursive: true, force: true });
|
|
}
|
|
});
|