feat(mcp): add remote lua execution
This commit is contained in:
parent
e0439ac6c5
commit
e30f2af2fd
@ -9,6 +9,48 @@ local function formatLabel(label)
|
||||
return label;
|
||||
end
|
||||
|
||||
local function appendOutput(output, value)
|
||||
output[#output + 1] = tostring(value);
|
||||
end
|
||||
|
||||
local function serializeReturn(value)
|
||||
local valueType = type(value);
|
||||
if valueType == 'nil' then
|
||||
return { type = 'nil' };
|
||||
end
|
||||
|
||||
if valueType == 'string' or valueType == 'number' or valueType == 'boolean' then
|
||||
return { type = valueType, value = value };
|
||||
end
|
||||
|
||||
local ok, serialized = pcall(textutils.serialize, value);
|
||||
if ok then
|
||||
return { type = valueType, repr = serialized };
|
||||
end
|
||||
return { type = valueType, repr = tostring(value) };
|
||||
end
|
||||
|
||||
local function createExecEnv(output)
|
||||
local env = setmetatable({}, { __index = _G });
|
||||
|
||||
env.write = function(value)
|
||||
appendOutput(output, value);
|
||||
end;
|
||||
|
||||
env.print = function(...)
|
||||
local values = table.pack(...);
|
||||
for i = 1, values.n do
|
||||
if i > 1 then
|
||||
appendOutput(output, '\t');
|
||||
end
|
||||
appendOutput(output, values[i]);
|
||||
end
|
||||
appendOutput(output, '\n');
|
||||
end;
|
||||
|
||||
return env;
|
||||
end
|
||||
|
||||
local function createMcpComputer()
|
||||
local api = {};
|
||||
|
||||
@ -63,6 +105,29 @@ local function createMcpComputer()
|
||||
};
|
||||
end
|
||||
|
||||
function api.executeLua(code)
|
||||
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));
|
||||
if not fn then
|
||||
return { ok = false, error = tostring(loadErr), output = table.concat(output) };
|
||||
end
|
||||
|
||||
local values = table.pack(pcall(fn));
|
||||
if not values[1] then
|
||||
return { ok = false, error = tostring(values[2]), output = table.concat(output) };
|
||||
end
|
||||
|
||||
local returns = {};
|
||||
for i = 2, values.n do
|
||||
returns[#returns + 1] = serializeReturn(values[i]);
|
||||
end
|
||||
return { ok = true, returns = returns, output = table.concat(output) };
|
||||
end
|
||||
|
||||
function api.handleRequest(request, osLike)
|
||||
if type(request) ~= 'table' or request.type ~= 'request' or type(request.id) ~= 'string' then
|
||||
return nil;
|
||||
@ -77,6 +142,21 @@ local function createMcpComputer()
|
||||
};
|
||||
end
|
||||
|
||||
if request.method == 'exec-lua' then
|
||||
local params = request.params;
|
||||
local result = api.executeLua(type(params) == 'table' and params.code or nil);
|
||||
return {
|
||||
type = 'response',
|
||||
id = request.id,
|
||||
ok = result.ok,
|
||||
result = {
|
||||
returns = result.returns or {},
|
||||
output = result.output or '',
|
||||
},
|
||||
error = result.error,
|
||||
};
|
||||
end
|
||||
|
||||
return {
|
||||
type = 'response',
|
||||
id = request.id,
|
||||
|
||||
@ -15,5 +15,6 @@ Future ADRs can reuse the shape of the existing files when it is useful.
|
||||
- [`adr-0010-ccpm-package-manager.md`](adr-0010-ccpm-package-manager.md) — `ccpm` package manager (packages, registries, package-aware bootstrap).
|
||||
- [`adr-0011-repo-conventions.md`](adr-0011-repo-conventions.md) — Git hooks own commit/push verification; markdown link syntax for cross-references.
|
||||
- [`adr-0016-js-tool-verification.md`](adr-0016-js-tool-verification.md) — JavaScript/TypeScript tool build, check, test, CI, and future integration-test split.
|
||||
- [`adr-0017-mcp-remote-lua-execution.md`](adr-0017-mcp-remote-lua-execution.md) — MCP `exec-lua` remote execution for linked ComputerCraft computers.
|
||||
|
||||
Gaps in numbering (0003, 0004, 0006, 0008, 0009, 0012, 0013, 0014, 0015) are records that were either superseded by later decisions or consolidated into the surviving ADRs above.
|
||||
|
||||
51
docs/adrs/adr-0017-mcp-remote-lua-execution.md
Normal file
51
docs/adrs/adr-0017-mcp-remote-lua-execution.md
Normal file
@ -0,0 +1,51 @@
|
||||
# ADR 0017: MCP Remote Lua Execution
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Date
|
||||
|
||||
2026-06-11
|
||||
|
||||
## Context
|
||||
|
||||
`tools/mcp-bridge` links OpenCode MCP tools to ComputerCraft computers through the `mcp-computer` WebSocket program. The initial bridge only supported `probe-computers`, which proved that the host could reach linked computers but did not let the assistant inspect or modify in-game state directly.
|
||||
|
||||
For interactive TrapOS development, a remote Lua execution tool is useful: it can inspect files, peripherals, settings, labels, inventory-adjacent APIs, service state, and small runtime hypotheses without asking the player to type each command manually in-game.
|
||||
|
||||
This is also explicitly dangerous. A linked assistant can run arbitrary Lua with the same authority as the ComputerCraft computer. That includes file deletion, network traffic, peripheral operations, turtle movement, inventory changes, reboot/shutdown calls, and long-running or stuck programs.
|
||||
|
||||
## Decision
|
||||
|
||||
Add an MCP tool named `exec-lua` to `tools/mcp-bridge`.
|
||||
|
||||
The tool targets one linked computer by `computerId` and sends Lua source over the existing bridge request/response protocol:
|
||||
|
||||
```json
|
||||
{ "type": "request", "id": "...", "method": "exec-lua", "params": { "code": "..." } }
|
||||
```
|
||||
|
||||
The ComputerCraft side executes the code in `mcp-computer` and returns a normal bridge response with:
|
||||
|
||||
- `ok` for execution success or failure.
|
||||
- `result.returns` as JSON-safe descriptors of returned values.
|
||||
- `result.output` containing captured `print` and `write` output.
|
||||
- `error` for syntax or runtime failure.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Consequences
|
||||
|
||||
- OpenCode can now inspect and operate linked ComputerCraft computers without manual in-game command entry.
|
||||
- The trust boundary moves to the act of running `mcp-computer` against a bridge. Only run it against bridges and assistants you trust.
|
||||
- The bridge remains protocol-compatible with older clients for `probe-computers`; older `mcp-computer` clients will report `unknown method` for `exec-lua`.
|
||||
- Tests must cover both host-side MCP routing and the real CraftOS-PC `mcp-computer` path so regressions are caught across the Node/Lua boundary described in [ADR-0016](adr-0016-js-tool-verification.md).
|
||||
|
||||
## Future Work
|
||||
|
||||
- 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.
|
||||
@ -6,7 +6,7 @@
|
||||
"trapos-net": "0.3.0",
|
||||
"trapos-ui": "0.2.2",
|
||||
"trapos-ai": "0.6.4",
|
||||
"trapos-sandbox": "0.1.1",
|
||||
"trapos-sandbox": "0.1.2",
|
||||
"trapos": "0.8.5"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trapos-sandbox",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"description": "TrapOS sandbox programs for ccpm experiments and Lua learning",
|
||||
"dependencies": ["trapos-core"],
|
||||
"files": [
|
||||
|
||||
@ -72,6 +72,45 @@ testlib.test('handleRequest responds to ping', function()
|
||||
testlib.assertEquals(response.result, 'pong from 42 (Label: worker)');
|
||||
end);
|
||||
|
||||
testlib.test('executeLua captures output and return values', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local result = mcpComputer.executeLua("print('hello', 'world'); write('tail'); return 2 + 3, nil, 'ok'");
|
||||
|
||||
testlib.assertEquals(result.ok, true);
|
||||
testlib.assertEquals(result.output, 'hello\tworld\ntail');
|
||||
testlib.assertEquals(result.returns[1].type, 'number');
|
||||
testlib.assertEquals(result.returns[1].value, 5);
|
||||
testlib.assertEquals(result.returns[2].type, 'nil');
|
||||
testlib.assertEquals(result.returns[3].type, 'string');
|
||||
testlib.assertEquals(result.returns[3].value, 'ok');
|
||||
end);
|
||||
|
||||
testlib.test('executeLua reports runtime errors with captured output', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local result = mcpComputer.executeLua("print('before'); error('boom', 0)");
|
||||
|
||||
testlib.assertEquals(result.ok, false);
|
||||
testlib.assertEquals(result.output, 'before\n');
|
||||
testlib.assertTrue(string.find(result.error, 'boom', 1, true));
|
||||
end);
|
||||
|
||||
testlib.test('handleRequest executes lua code', function()
|
||||
local mcpComputer = createMcpComputer();
|
||||
local response = mcpComputer.handleRequest({
|
||||
type = 'request',
|
||||
id = 'req-exec',
|
||||
method = 'exec-lua',
|
||||
params = { code = "print('ran'); return true" },
|
||||
}, fakeOs(42, 'worker'));
|
||||
|
||||
testlib.assertEquals(response.type, 'response');
|
||||
testlib.assertEquals(response.id, 'req-exec');
|
||||
testlib.assertEquals(response.ok, true);
|
||||
testlib.assertEquals(response.result.output, 'ran\n');
|
||||
testlib.assertEquals(response.result.returns[1].type, 'boolean');
|
||||
testlib.assertEquals(response.result.returns[1].value, true);
|
||||
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.1.0",
|
||||
"version": "0.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mcp-bridge",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"dependencies": {
|
||||
"ws": "^8.17.1"
|
||||
},
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mcp-bridge",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -85,21 +85,18 @@ export class LinkRegistry {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async execLua(computerId: number, code: 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, "exec-lua", { code }, timeoutMs);
|
||||
return formatExecLuaResult(computer, result);
|
||||
}
|
||||
|
||||
private async probeComputer(computer: ComputerConnection, timeoutMs: number): Promise<string> {
|
||||
const id = randomUUID();
|
||||
const response = new Promise<ResponseMessage>((resolve) => {
|
||||
this.pending.set(id, { computerId: computer.computerId, resolve });
|
||||
computer.ws.send(JSON.stringify({ type: "request", id, method: "ping" }));
|
||||
});
|
||||
|
||||
const timeout = new Promise<null>((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.pending.delete(id);
|
||||
resolve(null);
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
const result = await Promise.race([response, timeout]);
|
||||
const result = await this.requestComputer(computer, "ping", undefined, timeoutMs);
|
||||
if (!result) {
|
||||
return `timeout from ${formatComputer(computer.computerId, computer.label)}`;
|
||||
}
|
||||
@ -110,6 +107,55 @@ export class LinkRegistry {
|
||||
|
||||
return `error from ${formatComputer(computer.computerId, computer.label)}: ${result.error ?? "unknown error"}`;
|
||||
}
|
||||
|
||||
private async requestComputer(
|
||||
computer: ComputerConnection,
|
||||
method: string,
|
||||
params: Record<string, unknown> | undefined,
|
||||
timeoutMs: number,
|
||||
): Promise<ResponseMessage | null> {
|
||||
const id = randomUUID();
|
||||
const response = new Promise<ResponseMessage>((resolve) => {
|
||||
this.pending.set(id, { computerId: computer.computerId, resolve });
|
||||
computer.ws.send(JSON.stringify(params ? { type: "request", id, method, params } : { type: "request", id, method }));
|
||||
});
|
||||
|
||||
const timeout = new Promise<null>((resolve) => {
|
||||
setTimeout(() => {
|
||||
this.pending.delete(id);
|
||||
resolve(null);
|
||||
}, timeoutMs);
|
||||
});
|
||||
|
||||
return Promise.race([response, timeout]);
|
||||
}
|
||||
}
|
||||
|
||||
function formatExecLuaResult(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 output = typeof payload.output === "string" ? payload.output : "";
|
||||
const lines = [`computer: ${computerText}`, `ok: ${response.ok ? "true" : "false"}`];
|
||||
|
||||
if (response.ok) {
|
||||
lines.push(`returns: ${JSON.stringify(Array.isArray(payload.returns) ? payload.returns : [])}`);
|
||||
} else {
|
||||
lines.push(`error: ${response.error ?? "unknown error"}`);
|
||||
}
|
||||
|
||||
lines.push("output:");
|
||||
if (output !== "") {
|
||||
lines.push(output.endsWith("\n") ? output.slice(0, -1) : output);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function startLinkServer(options: { host: string; port: number; registry: LinkRegistry }): WebSocketServer {
|
||||
|
||||
@ -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.1.0" },
|
||||
serverInfo: { name: "mcp-bridge", version: "0.2.0" },
|
||||
});
|
||||
}
|
||||
|
||||
@ -84,23 +84,68 @@ async function handleSingleRequest(request: JsonRpcRequest, registry: LinkRegist
|
||||
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,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (request.method === "tools/call") {
|
||||
const params = isRecord(request.params) ? request.params : {};
|
||||
if (params.name !== "probe-computers") {
|
||||
return jsonRpcError(id, -32602, "Unknown tool");
|
||||
}
|
||||
|
||||
if (params.name === "probe-computers") {
|
||||
const text = await registry.probeComputers(probeTimeoutMs);
|
||||
return jsonRpcResult(id, { content: [{ type: "text", text }] });
|
||||
}
|
||||
|
||||
if (params.name === "exec-lua") {
|
||||
const args = isRecord(params.arguments) ? params.arguments : {};
|
||||
const parsed = parseExecLuaArgs(args, probeTimeoutMs);
|
||||
if (!parsed.ok) {
|
||||
return jsonRpcError(id, -32602, parsed.error);
|
||||
}
|
||||
|
||||
const text = await registry.execLua(parsed.computerId, parsed.code, parsed.timeoutMs);
|
||||
return jsonRpcResult(id, { content: [{ type: "text", text }] });
|
||||
}
|
||||
|
||||
return jsonRpcError(id, -32602, "Unknown tool");
|
||||
}
|
||||
|
||||
return jsonRpcError(id, -32601, "Method not found");
|
||||
}
|
||||
|
||||
function parseExecLuaArgs(
|
||||
args: Record<string, unknown>,
|
||||
defaultTimeoutMs: number,
|
||||
): { ok: true; computerId: number; code: 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.code !== "string" || args.code.trim() === "") {
|
||||
return { ok: false, error: "code 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, code: args.code, timeoutMs };
|
||||
}
|
||||
|
||||
function jsonRpcResult(id: unknown, result: unknown): unknown {
|
||||
return { jsonrpc: "2.0", id, result };
|
||||
}
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { callExecLua, formatFailure, startBridge, startCraftos, waitForComputers } from "./harness.js";
|
||||
|
||||
test("exec-lua runs code 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 text = await callExecLua(bridge.mcpUrl, 0, "print('hello from exec'); return 2 + 3");
|
||||
assert.match(text, /^computer: 0 \(Label: null\)\nok: true\nreturns: \[\{.*\}\]\noutput:\nhello from exec$/);
|
||||
assert.match(text, /"type":"number"/);
|
||||
assert.match(text, /"value":5/);
|
||||
} 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();
|
||||
}
|
||||
});
|
||||
@ -65,6 +65,28 @@ export async function callProbeComputers(mcpUrl: string): Promise<string> {
|
||||
return text;
|
||||
}
|
||||
|
||||
export async function callExecLua(mcpUrl: string, computerId: number, code: string, timeoutMs?: number): Promise<string> {
|
||||
const body = {
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "tools/call",
|
||||
params: { name: "exec-lua", arguments: { computerId, code, 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;
|
||||
|
||||
@ -46,6 +46,47 @@ test("MCP tool call returns text content", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
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("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" },
|
||||
});
|
||||
});
|
||||
|
||||
class FakeSocket {
|
||||
sent: unknown[] = [];
|
||||
|
||||
@ -57,10 +98,14 @@ class FakeSocket {
|
||||
return;
|
||||
}
|
||||
|
||||
respond(registry: LinkRegistry, computerId: number, result: string): void {
|
||||
lastRequest(): { id: string; type: string; method: string; params?: unknown } {
|
||||
const last = this.sent.at(-1);
|
||||
assert.equal(typeof last, "string");
|
||||
const request = JSON.parse(last as string) as { id: 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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user