feat(mcp): add write-file tool
This commit is contained in:
parent
565fc98ce8
commit
2b2a5bb86a
@ -128,6 +128,38 @@ local function createMcpComputer()
|
||||
return { ok = true, returns = returns, output = table.concat(output) };
|
||||
end
|
||||
|
||||
function api.writeFile(path, content, fsLike)
|
||||
fsLike = fsLike or fs;
|
||||
if type(path) ~= 'string' or path == '' then
|
||||
return { ok = false, error = 'path must be a non-empty string' };
|
||||
end
|
||||
|
||||
if type(content) ~= 'string' then
|
||||
return { ok = false, error = 'content must be a string' };
|
||||
end
|
||||
|
||||
local handle, openErr = fsLike.open(path, 'w');
|
||||
if not handle then
|
||||
return { ok = false, error = tostring(openErr or 'failed to open file') };
|
||||
end
|
||||
|
||||
local ok, writeErr = pcall(function()
|
||||
handle.write(content);
|
||||
end);
|
||||
local closeOk, closeErr = pcall(function()
|
||||
handle.close();
|
||||
end);
|
||||
|
||||
if not ok then
|
||||
return { ok = false, error = tostring(writeErr) };
|
||||
end
|
||||
if not closeOk then
|
||||
return { ok = false, error = tostring(closeErr) };
|
||||
end
|
||||
|
||||
return { ok = true, path = path, bytes = string.len(content) };
|
||||
end
|
||||
|
||||
function api.handleRequest(request, osLike)
|
||||
if type(request) ~= 'table' or request.type ~= 'request' or type(request.id) ~= 'string' then
|
||||
return nil;
|
||||
@ -157,6 +189,24 @@ local function createMcpComputer()
|
||||
};
|
||||
end
|
||||
|
||||
if request.method == 'write-file' then
|
||||
local params = request.params;
|
||||
local result = api.writeFile(
|
||||
type(params) == 'table' and params.path or nil,
|
||||
type(params) == 'table' and params.content or nil
|
||||
);
|
||||
return {
|
||||
type = 'response',
|
||||
id = request.id,
|
||||
ok = result.ok,
|
||||
result = {
|
||||
path = result.path,
|
||||
bytes = result.bytes,
|
||||
},
|
||||
error = result.error,
|
||||
};
|
||||
end
|
||||
|
||||
return {
|
||||
type = 'response',
|
||||
id = request.id,
|
||||
|
||||
@ -51,4 +51,4 @@ Timeouts are host-side request timeouts. A timed-out MCP call stops waiting for
|
||||
|
||||
- Add a separate opt-in flag or per-computer policy if the bridge is used outside local trusted development.
|
||||
- Add cooperative cancellation for yielding chunks if long-running remote execution becomes common.
|
||||
- Add additional tools on top of `exec-lua` for common safe operations once usage patterns are clear.
|
||||
- Add more convenience tools on top of the trusted bridge once usage patterns are clear.
|
||||
|
||||
@ -122,12 +122,14 @@ term.write('visible on screen')
|
||||
|
||||
`exec-lua` is powerful and unsafe by design: it can do anything the linked computer can do, including file, peripheral, turtle, and reboot operations. Only run `mcp-computer` against a bridge you trust.
|
||||
|
||||
The bridge also exposes `write-file`, which writes content to a path on one linked computer by id and overwrites any existing file. It follows normal ComputerCraft filesystem behavior, so missing parent directories fail instead of being created automatically.
|
||||
|
||||
## Quick Fixes
|
||||
|
||||
- `ai` says missing `opencc.server_url`: run the `set opencc.server_url ...` command again.
|
||||
- `ai` cannot reach server: check `opencode serve`, public host, port `4242`, and ComputerCraft HTTP rules.
|
||||
- `mcp-computer` says WebSocket unavailable: enable ComputerCraft HTTP/WebSocket support.
|
||||
- MCP sees no computers: keep `mcp-computer ws://<public-host>:4243` running in-game.
|
||||
- `exec-lua` is missing after updating the bridge: restart OpenCode so it reloads the MCP tool list.
|
||||
- `exec-lua` or `write-file` is missing after updating the bridge: restart OpenCode so it reloads the MCP tool list.
|
||||
|
||||
More detail: [`opencode_server_guide.md`](opencode_server_guide.md), [`public-ports.md`](public-ports.md).
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"trapos-net": "0.3.0",
|
||||
"trapos-ui": "0.2.2",
|
||||
"trapos-ai": "0.6.6",
|
||||
"trapos-sandbox": "0.2.0",
|
||||
"trapos-sandbox": "0.2.1",
|
||||
"trapos": "0.8.8"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trapos-sandbox",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"description": "TrapOS sandbox programs for ccpm experiments and Lua learning",
|
||||
"dependencies": ["trapos-core"],
|
||||
"files": [
|
||||
|
||||
@ -20,6 +20,30 @@ local function fakeSettings(values)
|
||||
};
|
||||
end
|
||||
|
||||
local function fakeFs()
|
||||
local files = {};
|
||||
return {
|
||||
files = files,
|
||||
open = function(path, mode)
|
||||
if mode ~= 'w' then
|
||||
return nil, 'unsupported mode';
|
||||
end
|
||||
if string.find(path, 'missing-parent', 1, true) then
|
||||
return nil, 'No such file';
|
||||
end
|
||||
local buffer = {};
|
||||
return {
|
||||
write = function(value)
|
||||
buffer[#buffer + 1] = value;
|
||||
end,
|
||||
close = function()
|
||||
files[path] = table.concat(buffer);
|
||||
end,
|
||||
};
|
||||
end,
|
||||
};
|
||||
end
|
||||
|
||||
testlib.test('parseArgs accepts positional URL', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local config = mcpComputer.parseArgs(packed('ws://127.0.0.1:3001'));
|
||||
@ -223,6 +247,64 @@ testlib.test('handleRequest reports invalid exec-lua code', function()
|
||||
testlib.assertTrue(string.find(missing.error, 'non-empty string', 1, true));
|
||||
end);
|
||||
|
||||
testlib.test('writeFile writes and overwrites file content', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local fsLike = fakeFs();
|
||||
|
||||
local first = mcpComputer.writeFile('/tmp/note.txt', 'hello', fsLike);
|
||||
local second = mcpComputer.writeFile('/tmp/note.txt', 'goodbye', fsLike);
|
||||
|
||||
testlib.assertEquals(first.ok, true);
|
||||
testlib.assertEquals(first.path, '/tmp/note.txt');
|
||||
testlib.assertEquals(first.bytes, 5);
|
||||
testlib.assertEquals(second.ok, true);
|
||||
testlib.assertEquals(second.bytes, 7);
|
||||
testlib.assertEquals(fsLike.files['/tmp/note.txt'], 'goodbye');
|
||||
end);
|
||||
|
||||
testlib.test('writeFile allows empty content', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local fsLike = fakeFs();
|
||||
local result = mcpComputer.writeFile('/tmp/empty.txt', '', fsLike);
|
||||
|
||||
testlib.assertEquals(result.ok, true);
|
||||
testlib.assertEquals(result.bytes, 0);
|
||||
testlib.assertEquals(fsLike.files['/tmp/empty.txt'], '');
|
||||
end);
|
||||
|
||||
testlib.test('writeFile validates path and content', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local fsLike = fakeFs();
|
||||
|
||||
local missingPath = mcpComputer.writeFile('', 'hello', fsLike);
|
||||
local invalidContent = mcpComputer.writeFile('/tmp/note.txt', 12, fsLike);
|
||||
local missingParent = mcpComputer.writeFile('/missing-parent/note.txt', 'hello', fsLike);
|
||||
|
||||
testlib.assertEquals(missingPath.ok, false);
|
||||
testlib.assertTrue(string.find(missingPath.error, 'path', 1, true));
|
||||
testlib.assertEquals(invalidContent.ok, false);
|
||||
testlib.assertTrue(string.find(invalidContent.error, 'content', 1, true));
|
||||
testlib.assertEquals(missingParent.ok, false);
|
||||
testlib.assertTrue(string.find(missingParent.error, 'No such file', 1, true));
|
||||
end);
|
||||
|
||||
testlib.test('handleRequest writes files', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local response = mcpComputer.handleRequest({
|
||||
type = 'request',
|
||||
id = 'req-write',
|
||||
method = 'write-file',
|
||||
params = { path = 'mcp-write-test.txt', content = 'from mcp' },
|
||||
}, fakeOs(42, 'worker'));
|
||||
|
||||
testlib.assertEquals(response.type, 'response');
|
||||
testlib.assertEquals(response.id, 'req-write');
|
||||
testlib.assertEquals(response.ok, true);
|
||||
testlib.assertEquals(response.result.path, 'mcp-write-test.txt');
|
||||
testlib.assertEquals(response.result.bytes, 8);
|
||||
fs.delete('mcp-write-test.txt');
|
||||
end);
|
||||
|
||||
testlib.test('handleRequest reports unknown methods', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local response = mcpComputer.handleRequest({
|
||||
|
||||
4
tools/mcp-bridge/package-lock.json
generated
4
tools/mcp-bridge/package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mcp-bridge",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mcp-bridge",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"dependencies": {
|
||||
"ws": "^8.17.1"
|
||||
},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mcp-bridge",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -95,6 +95,16 @@ export class LinkRegistry {
|
||||
return formatExecLuaResult(computer, result);
|
||||
}
|
||||
|
||||
async writeFile(computerId: number, path: string, content: 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, "write-file", { path, content }, timeoutMs);
|
||||
return formatWriteFileResult(computer, result);
|
||||
}
|
||||
|
||||
private async probeComputer(computer: ComputerConnection, timeoutMs: number): Promise<string> {
|
||||
const result = await this.requestComputer(computer, "ping", undefined, timeoutMs);
|
||||
if (!result) {
|
||||
@ -154,6 +164,25 @@ function formatExecLuaResult(computer: ComputerConnection, response: ResponseMes
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatWriteFileResult(computer: ComputerConnection, response: ResponseMessage | null): string {
|
||||
const computerText = formatComputer(computer.computerId, computer.label);
|
||||
if (!response) {
|
||||
return `computer: ${computerText}\nok: false\nerror: timeout`;
|
||||
}
|
||||
|
||||
const payload = isRecord(response.result) ? response.result : {};
|
||||
const lines = [`computer: ${computerText}`, `ok: ${response.ok ? "true" : "false"}`];
|
||||
|
||||
if (response.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"}`);
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
@ -68,7 +68,7 @@ async function handleSingleRequest(request: JsonRpcRequest, registry: LinkRegist
|
||||
return jsonRpcResult(id, {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: "mcp-bridge", version: "0.2.0" },
|
||||
serverInfo: { name: "mcp-bridge", version: "0.3.0" },
|
||||
});
|
||||
}
|
||||
|
||||
@ -98,6 +98,21 @@ async function handleSingleRequest(request: JsonRpcRequest, registry: LinkRegist
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
@ -120,6 +135,17 @@ async function handleSingleRequest(request: JsonRpcRequest, registry: LinkRegist
|
||||
return jsonRpcResult(id, { content: [{ type: "text", text }] });
|
||||
}
|
||||
|
||||
if (params.name === "write-file") {
|
||||
const args = isRecord(params.arguments) ? params.arguments : {};
|
||||
const parsed = parseWriteFileArgs(args, probeTimeoutMs);
|
||||
if (!parsed.ok) {
|
||||
return jsonRpcError(id, -32602, parsed.error);
|
||||
}
|
||||
|
||||
const text = await registry.writeFile(parsed.computerId, parsed.path, parsed.content, parsed.timeoutMs);
|
||||
return jsonRpcResult(id, { content: [{ type: "text", text }] });
|
||||
}
|
||||
|
||||
return jsonRpcError(id, -32602, "Unknown tool");
|
||||
}
|
||||
|
||||
@ -146,6 +172,30 @@ function parseExecLuaArgs(
|
||||
return { ok: true, computerId: args.computerId, code: args.code, timeoutMs };
|
||||
}
|
||||
|
||||
function parseWriteFileArgs(
|
||||
args: Record<string, unknown>,
|
||||
defaultTimeoutMs: number,
|
||||
): { ok: true; computerId: number; path: string; content: 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 (typeof args.content !== "string") {
|
||||
return { ok: false, error: "content must be a 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, content: args.content, timeoutMs };
|
||||
}
|
||||
|
||||
function jsonRpcResult(id: unknown, result: unknown): unknown {
|
||||
return { jsonrpc: "2.0", id, result };
|
||||
}
|
||||
|
||||
@ -87,6 +87,34 @@ export async function callExecLua(mcpUrl: string, computerId: number, code: stri
|
||||
return text;
|
||||
}
|
||||
|
||||
export async function callWriteFile(
|
||||
mcpUrl: string,
|
||||
computerId: number,
|
||||
path: string,
|
||||
content: string,
|
||||
timeoutMs?: number,
|
||||
): Promise<string> {
|
||||
const body = {
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "tools/call",
|
||||
params: { name: "write-file", arguments: { computerId, path, content, timeoutMs } },
|
||||
};
|
||||
const response = await fetch(mcpUrl, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const payload = (await response.json()) as {
|
||||
result?: { content?: { type: string; text: string }[] };
|
||||
};
|
||||
const text = payload.result?.content?.[0]?.text;
|
||||
if (typeof text !== "string") {
|
||||
throw new Error(`Unexpected MCP response: ${JSON.stringify(payload)}`);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
export type CraftosResult = {
|
||||
status: number | null;
|
||||
signal: NodeJS.Signals | null;
|
||||
|
||||
@ -0,0 +1,43 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { callExecLua, callWriteFile, formatFailure, startBridge, startCraftos, waitForComputers } from "./harness.js";
|
||||
|
||||
test("write-file writes through the real TrapOS mcp-computer program", async () => {
|
||||
const bridge = await startBridge();
|
||||
const craftos = startCraftos("/programs/mcp-computer.lua", {
|
||||
mountRepo: true,
|
||||
shellArgs: [bridge.linkUrl],
|
||||
timeoutMs: 15_000,
|
||||
});
|
||||
try {
|
||||
await waitForComputers(bridge.registry, 1, 12_000);
|
||||
|
||||
const first = await callWriteFile(bridge.mcpUrl, 0, "mcp-write-test.txt", "hello\nworld");
|
||||
assert.equal(first, "computer: 0 (Label: null)\nok: true\npath: mcp-write-test.txt\nbytes: 11");
|
||||
|
||||
const readFirst = await callExecLua(
|
||||
bridge.mcpUrl,
|
||||
0,
|
||||
"local h = fs.open('mcp-write-test.txt', 'r'); local c = h.readAll(); h.close(); return c",
|
||||
);
|
||||
assert.match(readFirst, /"value":"hello\\nworld"/);
|
||||
|
||||
const second = await callWriteFile(bridge.mcpUrl, 0, "mcp-write-test.txt", "overwritten");
|
||||
assert.equal(second, "computer: 0 (Label: null)\nok: true\npath: mcp-write-test.txt\nbytes: 11");
|
||||
|
||||
const readSecond = await callExecLua(
|
||||
bridge.mcpUrl,
|
||||
0,
|
||||
"local h = fs.open('mcp-write-test.txt', 'r'); local c = h.readAll(); h.close(); fs.delete('mcp-write-test.txt'); return c",
|
||||
);
|
||||
assert.match(readSecond, /"value":"overwritten"/);
|
||||
} catch (error) {
|
||||
craftos.abort();
|
||||
const result = await craftos.done;
|
||||
throw new Error(formatFailure(error instanceof Error ? error.message : String(error), result.output), { cause: error });
|
||||
} finally {
|
||||
craftos.abort();
|
||||
await craftos.done;
|
||||
await bridge.close();
|
||||
}
|
||||
});
|
||||
@ -74,6 +74,21 @@ test("MCP tools/list includes exec-lua schema", async () => {
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
@ -153,6 +168,81 @@ test("MCP exec-lua validates timeoutMs", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
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[] = [];
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user