var test = require("node:test"); var assert = require("node:assert/strict"); var fs = require("node:fs"); var path = require("node:path"); var vm = require("node:vm"); var EventEmitter = require("node:events"); function loadWorker(relativePath) { var socket = new EventEmitter(); var sent = []; socket.write = function(line) { var message = JSON.parse(line); sent.push(message); socket.emit("daemon_message", message); }; var filename = path.join(__dirname, "..", relativePath); var context = vm.createContext({ require: function(name) { if (name === "net") return { connect: function() { return socket; } }; if (name === "fs") return { writeSync: function() {}, existsSync: function() { return false; } }; if (name === "child_process") return { execSync: function() { throw new Error("No CLI in test"); } }; if (name.indexOf("./") === 0 || name.indexOf("../") === 0) { return require(path.resolve(path.dirname(filename), name)); } return require(name); }, __dirname: path.dirname(filename), process: { env: {}, argv: ["node", filename, "/test.sock"], pid: 1, on: function() {} }, console: { log: function() {}, error: function() {} }, AbortController: AbortController, setInterval: function() {}, clearInterval: function() {}, setTimeout: setTimeout, }); vm.runInContext(fs.readFileSync(filename, "utf8"), context, { filename: filename }); var resolveSDK; var rejectSDK; context.sdkModule = new Promise(function(resolve, reject) { resolveSDK = resolve; rejectSDK = reject; }); var received = []; var signal; var sdk = { query: function(args) { signal = args.options.abortController.signal; return (async function*() { for await (var message of args.prompt) { received.push(message); yield { type: "assistant", message: message }; } })(); }, }; return { context: context, sent: sent, received: received, ready: function() { resolveSDK(sdk); }, fail: function() { rejectSDK(new Error("SDK unavailable")); }, signal: function() { return signal; }, send: function(message) { socket.emit("data", Buffer.from(JSON.stringify(message) + "\n")); }, sendBatch: function(messages) { socket.emit("data", Buffer.from(messages.map(function(message) { return JSON.stringify(message) + "\n"; }).join(""))); }, done: function() { return new Promise(function(resolve, reject) { socket.on("daemon_message", function(message) { if (message.type === "query_done") resolve(); if (message.type === "query_error") reject(new Error(message.error)); }); }); }, }; } ["lib/yoke/adapters/claude-worker.js"].forEach(function(workerPath) { test(workerPath + ": converts nullable schedule fields for worker MCP tools", function () { var worker = loadWorker(workerPath); var zod = require("zod"); var z = zod.z || zod; var shape = worker.context.buildZodShape(z, { type: "object", properties: { cron: { type: ["string", "null"] } }, required: ["cron"] }); assert.equal(shape.cron.parse(null), null); assert.equal(shape.cron.parse("0 9 * * *"), "0 9 * * *"); }); test(workerPath + ": handles query start and first input in one socket chunk", { timeout: 1000 }, async function() { var worker = loadWorker(workerPath); var done = worker.done(); worker.sendBatch([ { type: "query_start", options: {} }, { type: "push_message", content: "first message" }, { type: "end_messages" }, ]); worker.ready(); await done; assert.deepEqual(worker.received, ["first message"]); }); test(workerPath + ": preserves the first prompt while SDK loading is pending", async function() { var worker = loadWorker(workerPath); var run = worker.context.handleQueryStart({ options: {}, singleTurn: true }); worker.send({ type: "push_message", content: { type: "user", message: { role: "user", content: "first" } } }); worker.ready(); await run; assert.equal(worker.received.length, 1, "first message must reach SDK without a second send"); assert.equal(worker.received[0].message.content, "first"); assert.equal(worker.sent.at(-1).type, "query_done"); }); test(workerPath + ": preserves ordered messages and end during startup", async function() { var worker = loadWorker(workerPath); var run = worker.context.handleQueryStart({ options: {}, prompt: "initial" }); worker.send({ type: "push_message", content: "first" }); worker.send({ type: "push_message", content: "second" }); worker.send({ type: "end_messages" }); worker.ready(); await run; assert.deepEqual(worker.received, ["initial", "first", "second"]); // A reused worker must allocate its next queue before even a cached SDK await. run = worker.context.handleQueryStart({ options: {}, singleTurn: true }); worker.send({ type: "push_message", content: "next turn" }); await run; assert.deepEqual(worker.received, ["initial", "first", "second", "next turn"]); }); test(workerPath + ": retains Stop received during SDK loading", async function() { var worker = loadWorker(workerPath); var run = worker.context.handleQueryStart({ options: {}, singleTurn: true }); worker.send({ type: "abort" }); worker.ready(); await run; assert.equal(worker.signal(), undefined, "cancelled startup must not invoke the SDK"); assert.equal(worker.sent.at(-1).type, "query_done"); assert.equal(worker.context.messageQueue, null); assert.equal(worker.context.abortController, null); }); test(workerPath + ": releases startup state if SDK loading fails", async function() { var worker = loadWorker(workerPath); var run = worker.context.handleQueryStart({ options: {} }); worker.send({ type: "push_message", content: "first" }); worker.fail(); await run; assert.equal(worker.sent.at(-1).type, "query_error"); assert.match(worker.sent.at(-1).error, /SDK unavailable/); assert.equal(worker.context.messageQueue, null); assert.equal(worker.context.abortController, null); }); }); test("Worker IPC preserves SDK permission presentation hints", async function() { var worker = loadWorker("lib/yoke/adapters/claude-worker.js"); var pending = worker.context.canUseTool("Write", {}, { defaultToNo: true, suppressAlwaysAllowRule: true }); var message = worker.sent.find(function(item) { return item.type === "permission_request"; }); assert.equal(message.defaultToNo, true); assert.equal(message.suppressAlwaysAllowRule, true); worker.send({ type: "permission_response", requestId: message.requestId, result: { behavior: "deny" } }); assert.equal((await pending).behavior, "deny"); }); test("Worker IPC forwards the SDK-reported mcpServer name/source across the daemon boundary", async function() { var worker = loadWorker("lib/yoke/adapters/claude-worker.js"); var pending = worker.context.canUseTool("mcp__clay-notes__write_note", {}, { mcpServer: { name: "clay-notes", source: "sdk" }, }); var message = worker.sent.find(function(item) { return item.type === "permission_request"; }); assert.deepEqual(message.mcpServer, { name: "clay-notes", source: "sdk" }); worker.send({ type: "permission_response", requestId: message.requestId, result: { behavior: "deny" } }); await pending; }); test("Worker IPC sends mcpServer: null rather than omitting it when the SDK reports none", async function() { var worker = loadWorker("lib/yoke/adapters/claude-worker.js"); var pending = worker.context.canUseTool("Write", {}, {}); var message = worker.sent.find(function(item) { return item.type === "permission_request"; }); assert.equal(message.mcpServer, null); worker.send({ type: "permission_response", requestId: message.requestId, result: { behavior: "deny" } }); await pending; });