Files
stack/packages/harness/tests/mcp-server.test.mjs
T
jason.woltjeandClaude Opus 5.5 ce104b27cc bus, harness: PM task reads that fit a session (#1583)
Row 89, round 1. The broker's `tasks` read takes optional filter and
page arguments and returns a short form. The harness adds `list_tasks`
arguments (open tasks by default), a `get_task` tool, and compact JSON
replies capped below the 50000-character threshold where Claude Code
2.1.296 saves a tool result to a file outside the workspace, which was
the PM's fault in Jason's test. The bundle's tools line names the
harness's own tools.

Candidate manifest b8c5bb3a (agents/dewey/work/queue-89/), base
a3f6e9e6, no drift on the 12 paths. Builder Dewey. Approved by
Darkwing (comment 27940). Sage's gate on f04185fc plus the patch
2026-10-12T22:00:55Z-22:03:38Z: bus 108, harness 65, webui 86, cli
115, control-board 124, every scripts/test-*.sh green.

Live at the next bus restart (broker) and role session launch
(harness), both Mos's call (decision 109); Mos had the notice.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 17:04:15 -05:00

112 lines
5.4 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import { createInterface } from "node:readline";
import { SOURCES } from "../src/bundle.mjs";
import { REPLY_CAP, typedTools } from "../src/tools.mjs";
import { fakeToolSocket, resolvedFor, scratch } from "./helpers.mjs";
// The server over stdio: send(message) resolves with the reply to its id.
function serve(t, toolsFile, socket) {
const child = spawn(process.execPath, [SOURCES.mcpServer, toolsFile, socket], { stdio: ["pipe", "pipe", "pipe"] });
t.after(() => child.kill());
const waiting = new Map();
const orphans = [];
createInterface({ input: child.stdout }).on("line", (line) => {
const m = JSON.parse(line);
const w = waiting.get(m.id);
if (w) {
waiting.delete(m.id);
w(m);
} else orphans.push(m);
});
let next = 1;
return {
orphans,
send(method, params) {
const id = next++;
return new Promise((resolve) => {
waiting.set(id, resolve);
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
});
},
raw: (text) => child.stdin.write(text),
};
}
async function setup(t) {
const dir = scratch(t);
const tools = typedTools({ resolved: resolvedFor(t, "pm"), tracker: true });
const toolsFile = join(dir, "tools.json");
writeFileSync(toolsFile, JSON.stringify(tools));
const sock = await fakeToolSocket(t, dir, (tool, args) => {
if (tool === "launch") throw new Error("capacity-full");
// trail answers a string `size` characters long once JSON-encoded, for the reply cap.
if (tool === "trail" && /^size:/.test(args.subject)) return "x".repeat(Number(args.subject.slice(5)) - 2);
return { tool, args };
});
return { dir, tools, sock, server: serve(t, toolsFile, sock.path) };
}
test("initialize, ping and tools/list", async (t) => {
const { tools, server } = await setup(t);
const init = await server.send("initialize", { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "x", version: "0" } });
assert.deepEqual(init.result, { protocolVersion: "2025-03-26", capabilities: { tools: {} }, serverInfo: { name: "mosaic", version: "1" } });
assert.deepEqual((await server.send("ping")).result, {});
const list = (await server.send("tools/list")).result.tools;
assert.deepEqual(list.map((x) => x.name), tools.map((x) => x.name));
const launch = list.find((x) => x.name === "launch");
assert.deepEqual(launch.inputSchema, tools.find((x) => x.name === "launch").parameters);
});
test("tools/call goes through the tool socket; a refusal is an isError result", async (t) => {
const { sock, server } = await setup(t);
const ok = (await server.send("tools/call", { name: "list_agents", arguments: { scope: "all" } })).result;
assert.equal(ok.isError, undefined);
assert.deepEqual(JSON.parse(ok.content[0].text), { tool: "list_agents", args: { scope: "all" } });
const refused = (await server.send("tools/call", { name: "launch", arguments: { instance: "coder" } })).result;
assert.equal(refused.isError, true);
assert.equal(refused.content[0].text, "refused: capacity-full");
assert.deepEqual(sock.calls.map((c) => c.tool), ["list_agents", "launch"]);
});
test("replies are compact JSON; over the cap a tool error names the size and the cap", async (t) => {
const { sock, server } = await setup(t);
const call = async (name, args) => (await server.send("tools/call", { name, arguments: args })).result;
const ok = await call("get_task", { task_ref: "vikunja:3/7" });
assert.equal(ok.content[0].text, '{"tool":"get_task","args":{"task_ref":"vikunja:3/7"}}');
// list_tasks with no arguments asks the broker for open tasks.
assert.equal((await call("list_tasks", {})).content[0].text, '{"tool":"list_tasks","args":{"done":false}}');
const fits = await call("trail", { subject: `size:${REPLY_CAP}` });
assert.equal(fits.isError, undefined);
assert.equal(fits.content[0].text.length, REPLY_CAP);
const over = await call("trail", { subject: `size:${REPLY_CAP + 1}` });
assert.equal(over.isError, true);
assert.deepEqual(over.content, [{
type: "text",
text: `reply too large: ${REPLY_CAP + 1} characters, over the ${REPLY_CAP}-character cap. Narrow the query: a filter, a smaller limit, or get_task for one task.`,
}]);
assert.deepEqual(sock.calls.map((c) => c.tool), ["get_task", "list_tasks", "trail", "trail"]);
});
test("unknown tools and methods are JSON-RPC errors and never reach the socket", async (t) => {
const { sock, server } = await setup(t);
const unknown = await server.send("tools/call", { name: "resolve_decision", arguments: {} });
assert.equal(unknown.error.code, -32602);
assert.equal((await server.send("resources/list")).error.code, -32601);
server.raw("not json\n");
// A notification gets no reply; the next request still answers.
server.raw(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
assert.deepEqual((await server.send("ping")).result, {});
assert.deepEqual(server.orphans, [{ jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } }]);
assert.equal(sock.calls.length, 0);
});
test("a missing argument is a usage error", async (t) => {
const child = spawn(process.execPath, [SOURCES.mcpServer], { stdio: "pipe" });
const code = await new Promise((r) => child.on("exit", r));
assert.equal(code, 2);
});