feat(mcp): add run-file capture
This commit is contained in:
parent
cb6ceaf575
commit
4eaa985458
@ -58,24 +58,10 @@ local function createExecEnv(output)
|
||||
return env;
|
||||
end
|
||||
|
||||
local function createMcpComputer()
|
||||
local api = {};
|
||||
|
||||
api.formatLabel = formatLabel;
|
||||
|
||||
function api.formatPong(osLike)
|
||||
osLike = osLike or defaultOs();
|
||||
return 'pong from ' .. tostring(osLike.getComputerID())
|
||||
.. ' (Label: ' .. formatLabel(osLike.getComputerLabel()) .. ')';
|
||||
end
|
||||
|
||||
function api.executeLua(code)
|
||||
local function executeSource(code, chunkName)
|
||||
local output = {};
|
||||
if type(code) ~= 'string' or code == '' then
|
||||
return { ok = false, error = 'code must be a non-empty string', output = '' };
|
||||
end
|
||||
|
||||
local fn, loadErr = load(code, 'mcp-exec', 't', createExecEnv(output));
|
||||
local fn, loadErr = load(code, chunkName, 't', createExecEnv(output));
|
||||
if not fn then
|
||||
return { ok = false, error = tostring(loadErr), output = table.concat(output) };
|
||||
end
|
||||
@ -92,6 +78,53 @@ local function createMcpComputer()
|
||||
return { ok = true, returns = returns, output = table.concat(output) };
|
||||
end
|
||||
|
||||
local function createMcpComputer()
|
||||
local api = {};
|
||||
|
||||
api.formatLabel = formatLabel;
|
||||
|
||||
function api.formatPong(osLike)
|
||||
osLike = osLike or defaultOs();
|
||||
return 'pong from ' .. tostring(osLike.getComputerID())
|
||||
.. ' (Label: ' .. formatLabel(osLike.getComputerLabel()) .. ')';
|
||||
end
|
||||
|
||||
function api.executeLua(code)
|
||||
if type(code) ~= 'string' or code == '' then
|
||||
return { ok = false, error = 'code must be a non-empty string', output = '' };
|
||||
end
|
||||
|
||||
return executeSource(code, 'mcp-exec');
|
||||
end
|
||||
|
||||
function api.executeFile(path, fsLike)
|
||||
fsLike = fsLike or fs;
|
||||
if type(path) ~= 'string' or path == '' then
|
||||
return { ok = false, error = 'path must be a non-empty string', output = '' };
|
||||
end
|
||||
|
||||
local handle, openErr = fsLike.open(path, 'r');
|
||||
if not handle then
|
||||
return { ok = false, error = tostring(openErr or 'failed to open file'), output = '' };
|
||||
end
|
||||
|
||||
local ok, contentOrErr = pcall(function()
|
||||
return handle.readAll();
|
||||
end);
|
||||
local closeOk, closeErr = pcall(function()
|
||||
handle.close();
|
||||
end);
|
||||
|
||||
if not ok then
|
||||
return { ok = false, error = tostring(contentOrErr), output = '' };
|
||||
end
|
||||
if not closeOk then
|
||||
return { ok = false, error = tostring(closeErr), output = '' };
|
||||
end
|
||||
|
||||
return executeSource(contentOrErr, '@' .. path);
|
||||
end
|
||||
|
||||
function api.writeFile(path, content, fsLike)
|
||||
fsLike = fsLike or fs;
|
||||
if type(path) ~= 'string' or path == '' then
|
||||
|
||||
@ -20,7 +20,7 @@ This is also explicitly dangerous. A linked assistant can run arbitrary Lua with
|
||||
|
||||
## Decision
|
||||
|
||||
Add an MCP tool named `exec-lua` to `tools/mcp-bridge`.
|
||||
Add MCP tools named `exec-lua` and `run-file` to `tools/mcp-bridge`.
|
||||
|
||||
The tool targets one linked computer by `computerId` and sends Lua source over the existing bridge request/response protocol:
|
||||
|
||||
@ -35,9 +35,13 @@ The ComputerCraft side executes the code in `mcp-computer` and returns a normal
|
||||
- `result.output` containing captured `print` and `write` output.
|
||||
- `error` for syntax or runtime failure.
|
||||
|
||||
For existing Lua files, `run-file` sends a target path instead of source code. The
|
||||
ComputerCraft side reads that file and loads the source into the same captured execution
|
||||
environment as `exec-lua`, using the file path as the chunk name for useful errors.
|
||||
|
||||
Execution is enabled by default for any computer running the updated `mcp-computer` program. We intentionally do not add an `--allow-exec` flag for this first version because the current workflow is a local, explicitly trusted development bridge and the user accepts the risk.
|
||||
|
||||
The execution environment overrides `print` and `write` so their text is captured in the MCP response instead of being emitted to the visible terminal. Code that intentionally wants to affect the ComputerCraft screen should call terminal APIs such as `term.clear`, `term.setCursorPos`, and `term.write` directly.
|
||||
The execution environment overrides `print` and `write` so their text is captured in the MCP response instead of being emitted to the visible terminal. This applies to direct `exec-lua` chunks and to files run through `run-file`. Code that intentionally wants to affect the ComputerCraft screen should call terminal APIs such as `term.clear`, `term.setCursorPos`, and `term.write` directly.
|
||||
|
||||
Timeouts are host-side request timeouts. A timed-out MCP call stops waiting for the response, but it does not preempt a running Lua chunk inside ComputerCraft. Avoid infinite loops and long blocking calls unless the in-game computer can be restarted.
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "TrapOS",
|
||||
"version": "0.11.0",
|
||||
"version": "0.11.1",
|
||||
"branch": "master",
|
||||
"packages": [
|
||||
"trapos"
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
"trapos-net": "0.3.0",
|
||||
"trapos-ui": "0.2.2",
|
||||
"trapos-ai": "0.7.0",
|
||||
"trapos-sandbox": "0.3.0",
|
||||
"trapos-sandbox": "0.3.1",
|
||||
"trapos-sandbox-legacy": "0.3.0",
|
||||
"trapos": "0.11.0"
|
||||
"trapos": "0.11.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trapos-sandbox",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"description": "TrapOS sandbox gateway client: WS daemon (servers/sandbox) + sandbox command + MCP exec/write server",
|
||||
"dependencies": ["trapos-core"],
|
||||
"files": [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trapos",
|
||||
"version": "0.11.0",
|
||||
"version": "0.11.1",
|
||||
"description": "TrapOS full install meta-package",
|
||||
"dependencies": [
|
||||
"trapos-boot",
|
||||
|
||||
@ -40,6 +40,16 @@ sandbox.serve('exec-lua', function(payload)
|
||||
};
|
||||
end, { eventloop = el });
|
||||
|
||||
sandbox.serve('run-file', function(payload)
|
||||
local result = mcp.executeFile(payload and payload.path);
|
||||
return true, {
|
||||
ok = result.ok,
|
||||
returns = result.returns or {},
|
||||
output = result.output or '',
|
||||
error = result.error,
|
||||
};
|
||||
end, { eventloop = el });
|
||||
|
||||
sandbox.serve('write-file', function(payload)
|
||||
local result = mcp.writeFile(payload and payload.path, payload and payload.content);
|
||||
return true, {
|
||||
@ -51,4 +61,4 @@ sandbox.serve('write-file', function(payload)
|
||||
end, { eventloop = el });
|
||||
|
||||
print('mcp-computer-server v' .. createVersion().forSelf()
|
||||
.. ' started (serving exec-lua/write-file over sandbox).');
|
||||
.. ' started (serving exec-lua/run-file/write-file over sandbox).');
|
||||
|
||||
@ -15,6 +15,18 @@ local function fakeFs()
|
||||
return {
|
||||
files = files,
|
||||
open = function(path, mode)
|
||||
if mode == 'r' then
|
||||
if files[path] == nil then
|
||||
return nil, 'No such file';
|
||||
end
|
||||
return {
|
||||
readAll = function()
|
||||
return files[path];
|
||||
end,
|
||||
close = function() end,
|
||||
};
|
||||
end
|
||||
|
||||
if mode ~= 'w' then
|
||||
return nil, 'unsupported mode';
|
||||
end
|
||||
@ -34,6 +46,14 @@ local function fakeFs()
|
||||
};
|
||||
end
|
||||
|
||||
local function fsWithFiles(files)
|
||||
local fsLike = fakeFs();
|
||||
for path, content in pairs(files) do
|
||||
fsLike.files[path] = content;
|
||||
end
|
||||
return fsLike;
|
||||
end
|
||||
|
||||
testlib.test('formatPong includes computer id and label', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
|
||||
@ -102,6 +122,77 @@ testlib.test('executeLua leaves direct terminal writes out of captured output',
|
||||
testlib.assertEquals(result.returns[1].value, 'done');
|
||||
end);
|
||||
|
||||
testlib.test('executeFile captures output and return values from file', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local fsLike = fsWithFiles({
|
||||
['/helloworld.lua'] = "print('hello', 'file'); write('tail'); return 42, 'ok'",
|
||||
});
|
||||
|
||||
local result = mcpComputer.executeFile('/helloworld.lua', fsLike);
|
||||
|
||||
testlib.assertEquals(result.ok, true);
|
||||
testlib.assertEquals(result.output, 'hello\tfile\ntail');
|
||||
testlib.assertEquals(result.returns[1].type, 'number');
|
||||
testlib.assertEquals(result.returns[1].value, 42);
|
||||
testlib.assertEquals(result.returns[2].type, 'string');
|
||||
testlib.assertEquals(result.returns[2].value, 'ok');
|
||||
end);
|
||||
|
||||
testlib.test('executeFile reports validation and missing file errors', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local fsLike = fakeFs();
|
||||
|
||||
local missingPath = mcpComputer.executeFile('', fsLike);
|
||||
local missingFile = mcpComputer.executeFile('/missing.lua', fsLike);
|
||||
|
||||
testlib.assertEquals(missingPath.ok, false);
|
||||
testlib.assertTrue(string.find(missingPath.error, 'path', 1, true));
|
||||
testlib.assertEquals(missingPath.output, '');
|
||||
testlib.assertEquals(missingFile.ok, false);
|
||||
testlib.assertTrue(string.find(missingFile.error, 'No such file', 1, true));
|
||||
testlib.assertEquals(missingFile.output, '');
|
||||
end);
|
||||
|
||||
testlib.test('executeFile reports syntax errors with file chunk name', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local fsLike = fsWithFiles({
|
||||
['/broken.lua'] = "print('unterminated'\nreturn 1",
|
||||
});
|
||||
|
||||
local result = mcpComputer.executeFile('/broken.lua', fsLike);
|
||||
|
||||
testlib.assertEquals(result.ok, false);
|
||||
testlib.assertEquals(result.output, '');
|
||||
testlib.assertTrue(string.find(result.error, '/broken.lua', 1, true));
|
||||
end);
|
||||
|
||||
testlib.test('executeFile reports runtime errors with captured output', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local fsLike = fsWithFiles({
|
||||
['/boom.lua'] = "print('before'); error('boom', 0)",
|
||||
});
|
||||
|
||||
local result = mcpComputer.executeFile('/boom.lua', fsLike);
|
||||
|
||||
testlib.assertEquals(result.ok, false);
|
||||
testlib.assertEquals(result.output, 'before\n');
|
||||
testlib.assertTrue(string.find(result.error, 'boom', 1, true));
|
||||
end);
|
||||
|
||||
testlib.test('executeFile leaves direct terminal writes out of captured output', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local fsLike = fsWithFiles({
|
||||
['/screen.lua'] = "term.write('visible'); return 'done'",
|
||||
});
|
||||
|
||||
local result = mcpComputer.executeFile('/screen.lua', fsLike);
|
||||
|
||||
testlib.assertEquals(result.ok, true);
|
||||
testlib.assertEquals(result.output, '');
|
||||
testlib.assertEquals(result.returns[1].type, 'string');
|
||||
testlib.assertEquals(result.returns[1].value, 'done');
|
||||
end);
|
||||
|
||||
testlib.test('writeFile writes and overwrites file content', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local fsLike = fakeFs();
|
||||
|
||||
@ -92,7 +92,17 @@ export class LinkRegistry {
|
||||
}
|
||||
|
||||
const result = await this.requestComputer(computer, "exec-lua", { code }, timeoutMs);
|
||||
return formatExecLuaResult(computer, result);
|
||||
return formatExecutionResult(computer, result);
|
||||
}
|
||||
|
||||
async runFile(computerId: number, path: string, timeoutMs: number): Promise<string> {
|
||||
const computer = this.computers.get(computerId);
|
||||
if (!computer) {
|
||||
return `No computer with id ${computerId} connected.`;
|
||||
}
|
||||
|
||||
const result = await this.requestComputer(computer, "run-file", { path }, timeoutMs);
|
||||
return formatExecutionResult(computer, result);
|
||||
}
|
||||
|
||||
async writeFile(computerId: number, path: string, content: string, timeoutMs: number): Promise<string> {
|
||||
@ -141,7 +151,7 @@ export class LinkRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
function formatExecLuaResult(computer: ComputerConnection, response: ResponseMessage | null): string {
|
||||
function formatExecutionResult(computer: ComputerConnection, response: ResponseMessage | null): string {
|
||||
const computerText = formatComputer(computer.computerId, computer.label);
|
||||
if (!response) {
|
||||
return `computer: ${computerText}\nok: false\nerror: timeout`;
|
||||
@ -149,12 +159,13 @@ function formatExecLuaResult(computer: ComputerConnection, response: ResponseMes
|
||||
|
||||
const payload = isRecord(response.result) ? response.result : {};
|
||||
const output = typeof payload.output === "string" ? payload.output : "";
|
||||
const lines = [`computer: ${computerText}`, `ok: ${response.ok ? "true" : "false"}`];
|
||||
const ok = response.ok && payload.ok !== false;
|
||||
const lines = [`computer: ${computerText}`, `ok: ${ok ? "true" : "false"}`];
|
||||
|
||||
if (response.ok) {
|
||||
if (ok) {
|
||||
lines.push(`returns: ${JSON.stringify(Array.isArray(payload.returns) ? payload.returns : [])}`);
|
||||
} else {
|
||||
lines.push(`error: ${response.error ?? "unknown error"}`);
|
||||
lines.push(`error: ${typeof payload.error === "string" ? payload.error : response.error ?? "unknown error"}`);
|
||||
}
|
||||
|
||||
lines.push("output:");
|
||||
@ -171,13 +182,14 @@ function formatWriteFileResult(computer: ComputerConnection, response: ResponseM
|
||||
}
|
||||
|
||||
const payload = isRecord(response.result) ? response.result : {};
|
||||
const lines = [`computer: ${computerText}`, `ok: ${response.ok ? "true" : "false"}`];
|
||||
const ok = response.ok && payload.ok !== false;
|
||||
const lines = [`computer: ${computerText}`, `ok: ${ok ? "true" : "false"}`];
|
||||
|
||||
if (response.ok) {
|
||||
if (ok) {
|
||||
lines.push(`path: ${typeof payload.path === "string" ? payload.path : ""}`);
|
||||
lines.push(`bytes: ${typeof payload.bytes === "number" ? payload.bytes : 0}`);
|
||||
} else {
|
||||
lines.push(`error: ${response.error ?? "unknown error"}`);
|
||||
lines.push(`error: ${typeof payload.error === "string" ? payload.error : response.error ?? "unknown error"}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
|
||||
@ -113,6 +113,20 @@ async function handleSingleRequest(request: JsonRpcRequest, registry: LinkRegist
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "run-file",
|
||||
description: "Run a Lua file on a linked ComputerCraft computer with captured print/write output.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
computerId: { type: "number", description: "ComputerCraft computer id to run on." },
|
||||
path: { type: "string", description: "Lua file path on the ComputerCraft computer." },
|
||||
timeoutMs: { type: "number", description: "Optional host-side timeout in milliseconds, max 30000." },
|
||||
},
|
||||
required: ["computerId", "path"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
@ -146,6 +160,17 @@ async function handleSingleRequest(request: JsonRpcRequest, registry: LinkRegist
|
||||
return jsonRpcResult(id, { content: [{ type: "text", text }] });
|
||||
}
|
||||
|
||||
if (params.name === "run-file") {
|
||||
const args = isRecord(params.arguments) ? params.arguments : {};
|
||||
const parsed = parseRunFileArgs(args, probeTimeoutMs);
|
||||
if (!parsed.ok) {
|
||||
return jsonRpcError(id, -32602, parsed.error);
|
||||
}
|
||||
|
||||
const text = await registry.runFile(parsed.computerId, parsed.path, parsed.timeoutMs);
|
||||
return jsonRpcResult(id, { content: [{ type: "text", text }] });
|
||||
}
|
||||
|
||||
return jsonRpcError(id, -32602, "Unknown tool");
|
||||
}
|
||||
|
||||
@ -196,6 +221,26 @@ function parseWriteFileArgs(
|
||||
return { ok: true, computerId: args.computerId, path: args.path, content: args.content, timeoutMs };
|
||||
}
|
||||
|
||||
function parseRunFileArgs(
|
||||
args: Record<string, unknown>,
|
||||
defaultTimeoutMs: number,
|
||||
): { ok: true; computerId: number; path: string; timeoutMs: number } | { ok: false; error: string } {
|
||||
if (typeof args.computerId !== "number" || !Number.isFinite(args.computerId)) {
|
||||
return { ok: false, error: "computerId must be a finite number" };
|
||||
}
|
||||
|
||||
if (typeof args.path !== "string" || args.path.trim() === "") {
|
||||
return { ok: false, error: "path must be a non-empty string" };
|
||||
}
|
||||
|
||||
if (args.timeoutMs !== undefined && (typeof args.timeoutMs !== "number" || !Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) {
|
||||
return { ok: false, error: "timeoutMs must be a positive finite number" };
|
||||
}
|
||||
|
||||
const timeoutMs = Math.min(args.timeoutMs ?? defaultTimeoutMs, 30_000);
|
||||
return { ok: true, computerId: args.computerId, path: args.path, timeoutMs };
|
||||
}
|
||||
|
||||
function jsonRpcResult(id: unknown, result: unknown): unknown {
|
||||
return { jsonrpc: "2.0", id, result };
|
||||
}
|
||||
|
||||
@ -89,6 +89,20 @@ test("MCP tools/list includes exec-lua schema", async () => {
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "run-file",
|
||||
description: "Run a Lua file on a linked ComputerCraft computer with captured print/write output.",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
computerId: { type: "number", description: "ComputerCraft computer id to run on." },
|
||||
path: { type: "string", description: "Lua file path on the ComputerCraft computer." },
|
||||
timeoutMs: { type: "number", description: "Optional host-side timeout in milliseconds, max 30000." },
|
||||
},
|
||||
required: ["computerId", "path"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
@ -131,6 +145,17 @@ test("exec-lua formats computer error responses", async () => {
|
||||
assert.equal(await promise, "computer: 12 (Label: base-turtle)\nok: false\nerror: boom\noutput:\nbefore");
|
||||
});
|
||||
|
||||
test("exec-lua formats ok false execution results", 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.respond(registry, 12, { ok: false, error: "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 });
|
||||
@ -138,6 +163,63 @@ test("exec-lua reports timeouts", async () => {
|
||||
assert.equal(await registry.execLua(12, "while true do end", 5), "computer: 12 (Label: base-turtle)\nok: false\nerror: timeout");
|
||||
});
|
||||
|
||||
test("run-file sends path 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.runFile(12, "/helloworld.lua", 50);
|
||||
assert.deepEqual(computer.lastRequest(), {
|
||||
type: "request",
|
||||
id: computer.lastRequest().id,
|
||||
method: "run-file",
|
||||
params: { path: "/helloworld.lua" },
|
||||
});
|
||||
|
||||
computer.respond(registry, 12, { returns: [], output: "Blah!\n" });
|
||||
assert.equal(await promise, "computer: 12 (Label: base-turtle)\nok: true\nreturns: []\noutput:\nBlah!");
|
||||
});
|
||||
|
||||
test("run-file reports unknown computer", async () => {
|
||||
const registry = new LinkRegistry();
|
||||
|
||||
assert.equal(await registry.runFile(99, "/helloworld.lua", 50), "No computer with id 99 connected.");
|
||||
});
|
||||
|
||||
test("MCP run-file validates required arguments", async () => {
|
||||
const registry = new LinkRegistry();
|
||||
const response = await handleMcpRequest(
|
||||
{ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "run-file", arguments: { computerId: 1 } } },
|
||||
registry,
|
||||
10,
|
||||
);
|
||||
|
||||
assert.deepEqual(response, {
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
error: { code: -32602, message: "path must be a non-empty string" },
|
||||
});
|
||||
});
|
||||
|
||||
test("MCP run-file returns text 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: "run-file", arguments: { computerId: 12, path: "/helloworld.lua" } } },
|
||||
registry,
|
||||
10,
|
||||
);
|
||||
computer.respond(registry, 12, { returns: [], output: "Blah!\n" });
|
||||
|
||||
assert.deepEqual(await responsePromise, {
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
result: { content: [{ type: "text", text: "computer: 12 (Label: base-turtle)\nok: true\nreturns: []\noutput:\nBlah!" }] },
|
||||
});
|
||||
});
|
||||
|
||||
test("MCP exec-lua validates required arguments", async () => {
|
||||
const registry = new LinkRegistry();
|
||||
const response = await handleMcpRequest(
|
||||
@ -202,6 +284,17 @@ test("write-file formats computer error responses", async () => {
|
||||
assert.equal(await promise, "computer: 12 (Label: base-turtle)\nok: false\nerror: No such file");
|
||||
});
|
||||
|
||||
test("write-file formats ok false operation results", 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.respond(registry, 12, { ok: false, error: "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 });
|
||||
|
||||
Loading…
Reference in New Issue
Block a user