import assert from "node:assert/strict"; import test from "node:test"; import { WebSocket } from "ws"; import { LinkRegistry } from "../src/link-server.js"; import { handleMcpRequest } from "../src/mcp-server.js"; test("probe-computers returns no computers message when registry is empty", async () => { const registry = new LinkRegistry(); assert.equal(await registry.probeComputers(10), "No computers connected."); }); test("probe-computers aggregates multiple successful responses", async () => { const registry = new LinkRegistry(); const computer1 = new FakeSocket(); const computer2 = new FakeSocket(); registry.register({ computerId: 12, label: "base-turtle", ws: computer1 as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); registry.register({ computerId: 13, label: "miner-1", ws: computer2 as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); const promise = registry.probeComputers(50); computer1.respond(registry, 12, "pong from 12 (Label: base-turtle)"); computer2.respond(registry, 13, "pong from 13 (Label: miner-1)"); assert.equal(await promise, "pong from 12 (Label: base-turtle)\npong from 13 (Label: miner-1)"); }); test("probe-computers reports timeout for a connected computer that does not answer", async () => { const registry = new LinkRegistry(); registry.register({ computerId: 14, label: "farm-turtle", ws: new FakeSocket() as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); assert.equal(await registry.probeComputers(5), "timeout from 14 (Label: farm-turtle)"); }); test("MCP tool call returns text content", async () => { const registry = new LinkRegistry(); const response = await handleMcpRequest( { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "probe-computers", arguments: {} } }, registry, 10, ); assert.deepEqual(response, { jsonrpc: "2.0", id: 1, result: { content: [{ type: "text", text: "No computers connected." }] }, }); }); test("MCP tools/list includes exec-lua schema", async () => { const registry = new LinkRegistry(); const response = await handleMcpRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" }, registry, 10); assert.deepEqual(response, { jsonrpc: "2.0", id: 1, result: { tools: [ { name: "probe-computers", description: "Probe all linked ComputerCraft computers.", inputSchema: { type: "object", properties: {}, additionalProperties: false }, }, { name: "exec-lua", description: "Execute Lua code on a linked ComputerCraft computer.", inputSchema: { type: "object", properties: { computerId: { type: "number", description: "ComputerCraft computer id to execute on." }, code: { type: "string", description: "Lua source code to execute." }, timeoutMs: { type: "number", description: "Optional host-side timeout in milliseconds, max 30000." }, }, required: ["computerId", "code"], additionalProperties: false, }, }, { name: "write-file", description: "Write file content on a linked ComputerCraft computer, overwriting any existing file.", inputSchema: { type: "object", properties: { computerId: { type: "number", description: "ComputerCraft computer id to write on." }, path: { type: "string", description: "Target path on the ComputerCraft computer." }, content: { type: "string", description: "File content to write. Empty strings are allowed." }, timeoutMs: { type: "number", description: "Optional host-side timeout in milliseconds, max 30000." }, }, required: ["computerId", "path", "content"], additionalProperties: false, }, }, ], }, }); }); test("exec-lua sends code to the selected computer", async () => { const registry = new LinkRegistry(); const computer = new FakeSocket(); registry.register({ computerId: 12, label: "base-turtle", ws: computer as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); const promise = registry.execLua(12, "print('hi') return 2 + 3", 50); assert.deepEqual(computer.lastRequest(), { type: "request", id: computer.lastRequest().id, method: "exec-lua", params: { code: "print('hi') return 2 + 3" }, }); computer.respond(registry, 12, { returns: [{ type: "number", value: 5 }], output: "hi\n" }); assert.equal( await promise, 'computer: 12 (Label: base-turtle)\nok: true\nreturns: [{"type":"number","value":5}]\noutput:\nhi', ); }); test("exec-lua reports unknown computer", async () => { const registry = new LinkRegistry(); assert.equal(await registry.execLua(99, "return 1", 50), "No computer with id 99 connected."); }); test("exec-lua formats computer error responses", async () => { const registry = new LinkRegistry(); const computer = new FakeSocket(); registry.register({ computerId: 12, label: "base-turtle", ws: computer as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); const promise = registry.execLua(12, "print('before'); error('boom')", 50); computer.respondError(registry, 12, "boom", { output: "before\n" }); assert.equal(await promise, "computer: 12 (Label: base-turtle)\nok: false\nerror: boom\noutput:\nbefore"); }); test("exec-lua reports timeouts", async () => { const registry = new LinkRegistry(); registry.register({ computerId: 12, label: "base-turtle", ws: new FakeSocket() as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); assert.equal(await registry.execLua(12, "while true do end", 5), "computer: 12 (Label: base-turtle)\nok: false\nerror: timeout"); }); test("MCP exec-lua validates required arguments", async () => { const registry = new LinkRegistry(); const response = await handleMcpRequest( { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "exec-lua", arguments: { computerId: 1 } } }, registry, 10, ); assert.deepEqual(response, { jsonrpc: "2.0", id: 2, error: { code: -32602, message: "code must be a non-empty string" }, }); }); test("MCP exec-lua validates timeoutMs", async () => { const registry = new LinkRegistry(); const response = await handleMcpRequest( { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "exec-lua", arguments: { computerId: 1, code: "return 1", timeoutMs: 0 } } }, registry, 10, ); assert.deepEqual(response, { jsonrpc: "2.0", id: 2, error: { code: -32602, message: "timeoutMs must be a positive finite number" }, }); }); test("write-file sends path and content to the selected computer", async () => { const registry = new LinkRegistry(); const computer = new FakeSocket(); registry.register({ computerId: 12, label: "base-turtle", ws: computer as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); const promise = registry.writeFile(12, "notes/todo.txt", "hello\nworld", 50); assert.deepEqual(computer.lastRequest(), { type: "request", id: computer.lastRequest().id, method: "write-file", params: { path: "notes/todo.txt", content: "hello\nworld" }, }); computer.respond(registry, 12, { path: "notes/todo.txt", bytes: 11 }); assert.equal(await promise, "computer: 12 (Label: base-turtle)\nok: true\npath: notes/todo.txt\nbytes: 11"); }); test("write-file reports unknown computer", async () => { const registry = new LinkRegistry(); assert.equal(await registry.writeFile(99, "note.txt", "hello", 50), "No computer with id 99 connected."); }); test("write-file formats computer error responses", async () => { const registry = new LinkRegistry(); const computer = new FakeSocket(); registry.register({ computerId: 12, label: "base-turtle", ws: computer as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); const promise = registry.writeFile(12, "missing/note.txt", "hello", 50); computer.respondError(registry, 12, "No such file", {}); assert.equal(await promise, "computer: 12 (Label: base-turtle)\nok: false\nerror: No such file"); }); test("write-file reports timeouts", async () => { const registry = new LinkRegistry(); registry.register({ computerId: 12, label: "base-turtle", ws: new FakeSocket() as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); assert.equal(await registry.writeFile(12, "note.txt", "hello", 5), "computer: 12 (Label: base-turtle)\nok: false\nerror: timeout"); }); test("MCP write-file validates required arguments", async () => { const registry = new LinkRegistry(); const response = await handleMcpRequest( { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "write-file", arguments: { computerId: 1, content: "hello" } } }, registry, 10, ); assert.deepEqual(response, { jsonrpc: "2.0", id: 2, error: { code: -32602, message: "path must be a non-empty string" }, }); }); test("MCP write-file allows empty content", async () => { const registry = new LinkRegistry(); const computer = new FakeSocket(); registry.register({ computerId: 12, label: "base-turtle", ws: computer as unknown as WebSocket, connectedAt: 1, lastSeenAt: 1 }); const responsePromise = handleMcpRequest( { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "write-file", arguments: { computerId: 12, path: "empty.txt", content: "" } } }, registry, 10, ); computer.respond(registry, 12, { path: "empty.txt", bytes: 0 }); assert.deepEqual(await responsePromise, { jsonrpc: "2.0", id: 2, result: { content: [{ type: "text", text: "computer: 12 (Label: base-turtle)\nok: true\npath: empty.txt\nbytes: 0" }] }, }); }); class FakeSocket { sent: unknown[] = []; send(data: unknown): void { this.sent.push(data); } close(): void { return; } lastRequest(): { id: string; type: string; method: string; params?: unknown } { const last = this.sent.at(-1); assert.equal(typeof last, "string"); return JSON.parse(last as string) as { id: string; type: string; method: string; params?: unknown }; } respond(registry: LinkRegistry, computerId: number, result: unknown): void { const request = this.lastRequest(); registry.handleFrame(this as unknown as WebSocket, JSON.stringify({ type: "response", id: request.id, ok: true, result })); assert.equal(computerId > 0, true); } respondError(registry: LinkRegistry, computerId: number, error: string, result: unknown): void { const request = this.lastRequest(); registry.handleFrame(this as unknown as WebSocket, JSON.stringify({ type: "response", id: request.id, ok: false, error, result })); assert.equal(computerId > 0, true); } }