330 lines
12 KiB
Lua
330 lines
12 KiB
Lua
local createLibTest = require('/apis/libtest');
|
|
local createMcpComputer = require('/apis/libmcpcomputer');
|
|
|
|
local testlib = createLibTest({ ... });
|
|
|
|
local function packed(...)
|
|
return table.pack(...);
|
|
end
|
|
|
|
local function fakeOs(id, label)
|
|
return {
|
|
getComputerID = function() return id; end,
|
|
getComputerLabel = function() return label; end,
|
|
};
|
|
end
|
|
|
|
local function fakeSettings(values)
|
|
return {
|
|
get = function(key) return values[key]; end,
|
|
};
|
|
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'));
|
|
|
|
testlib.assertEquals(config.url, 'ws://127.0.0.1:3001');
|
|
end);
|
|
|
|
testlib.test('parseArgs accepts -url option', function()
|
|
local mcpComputer = createMcpComputer();
|
|
local config = mcpComputer.parseArgs(packed('-url', 'ws://bridge:3001'));
|
|
|
|
testlib.assertEquals(config.url, 'ws://bridge:3001');
|
|
end);
|
|
|
|
testlib.test('parseArgs rejects missing URL', function()
|
|
local mcpComputer = createMcpComputer();
|
|
local config, err = mcpComputer.parseArgs(packed());
|
|
|
|
testlib.assertEquals(config, nil);
|
|
testlib.assertTrue(string.find(err, 'missing websocket URL', 1, true));
|
|
end);
|
|
|
|
testlib.test('formatPong includes computer id and label', function()
|
|
local mcpComputer = createMcpComputer();
|
|
|
|
testlib.assertEquals(mcpComputer.formatPong(fakeOs(12, 'base-turtle')), 'pong from 12 (Label: base-turtle)');
|
|
end);
|
|
|
|
testlib.test('formatPong renders missing or empty labels as null', function()
|
|
local mcpComputer = createMcpComputer();
|
|
|
|
testlib.assertEquals(mcpComputer.formatPong(fakeOs(7, nil)), 'pong from 7 (Label: null)');
|
|
testlib.assertEquals(mcpComputer.formatPong(fakeOs(8, '')), 'pong from 8 (Label: null)');
|
|
end);
|
|
|
|
testlib.test('hello identifies the current computer', function()
|
|
local mcpComputer = createMcpComputer();
|
|
local hello = mcpComputer.hello(fakeOs(3, 'agent'));
|
|
|
|
testlib.assertEquals(hello.type, 'hello');
|
|
testlib.assertEquals(hello.computerId, 3);
|
|
testlib.assertEquals(hello.computerLabel, 'agent');
|
|
end);
|
|
|
|
testlib.test('resolveUrl prefers a positional argument over the setting', function()
|
|
local mcpComputer = createMcpComputer();
|
|
local settingsLib = fakeSettings({ ['mcp-computer.ws-url'] = 'ws://from-setting:3001' });
|
|
local config = mcpComputer.resolveUrl(packed('ws://from-arg:3001'), settingsLib);
|
|
|
|
testlib.assertEquals(config.url, 'ws://from-arg:3001');
|
|
end);
|
|
|
|
testlib.test('resolveUrl falls back to the setting when no argument is given', function()
|
|
local mcpComputer = createMcpComputer();
|
|
local settingsLib = fakeSettings({ ['mcp-computer.ws-url'] = 'ws://from-setting:3001' });
|
|
local config = mcpComputer.resolveUrl(packed(), settingsLib);
|
|
|
|
testlib.assertEquals(config.url, 'ws://from-setting:3001');
|
|
end);
|
|
|
|
testlib.test('resolveUrl errors when neither argument nor setting provides a URL', function()
|
|
local mcpComputer = createMcpComputer();
|
|
local config, err = mcpComputer.resolveUrl(packed(), fakeSettings({}));
|
|
|
|
testlib.assertEquals(config, nil);
|
|
testlib.assertTrue(string.find(err, 'mcp-computer.ws-url', 1, true));
|
|
end);
|
|
|
|
testlib.test('onMessage replies to a request frame through sendFn', function()
|
|
local mcpComputer = createMcpComputer();
|
|
local sent = {};
|
|
mcpComputer.onMessage('{"type":"request","id":"req-7","method":"ping"}', fakeOs(5, 'node'), function(response)
|
|
sent[#sent + 1] = response;
|
|
end);
|
|
|
|
testlib.assertEquals(#sent, 1);
|
|
testlib.assertEquals(sent[1].id, 'req-7');
|
|
testlib.assertEquals(sent[1].result, 'pong from 5 (Label: node)');
|
|
end);
|
|
|
|
testlib.test('onMessage stays silent on non-request and malformed frames', function()
|
|
local mcpComputer = createMcpComputer();
|
|
local sent = {};
|
|
local function capture(response) sent[#sent + 1] = response; end
|
|
|
|
mcpComputer.onMessage('{"type":"hello-ok"}', fakeOs(5, 'node'), capture);
|
|
mcpComputer.onMessage('not json', fakeOs(5, 'node'), capture);
|
|
|
|
testlib.assertEquals(#sent, 0);
|
|
end);
|
|
|
|
testlib.test('handleRequest responds to ping', function()
|
|
local mcpComputer = createMcpComputer();
|
|
local response = mcpComputer.handleRequest({
|
|
type = 'request',
|
|
id = 'req-1',
|
|
method = 'ping',
|
|
}, fakeOs(42, 'worker'));
|
|
|
|
testlib.assertEquals(response.type, 'response');
|
|
testlib.assertEquals(response.id, 'req-1');
|
|
testlib.assertEquals(response.ok, true);
|
|
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('executeLua reports syntax errors', function()
|
|
local mcpComputer = createMcpComputer();
|
|
local result = mcpComputer.executeLua("print('unterminated'\nreturn 1");
|
|
|
|
testlib.assertEquals(result.ok, false);
|
|
testlib.assertEquals(result.output, '');
|
|
testlib.assertTrue(string.find(result.error, 'expected', 1, true)
|
|
or string.find(result.error, 'near', 1, true));
|
|
end);
|
|
|
|
testlib.test('executeLua serializes non-json return values as descriptors', function()
|
|
local mcpComputer = createMcpComputer();
|
|
local result = mcpComputer.executeLua('return { answer = 42 }, function() end, coroutine.create(function() end)');
|
|
|
|
testlib.assertEquals(result.ok, true);
|
|
testlib.assertEquals(result.returns[1].type, 'table');
|
|
testlib.assertTrue(type(result.returns[1].repr) == 'string');
|
|
testlib.assertEquals(result.returns[2].type, 'function');
|
|
testlib.assertTrue(type(result.returns[2].repr) == 'string');
|
|
testlib.assertEquals(result.returns[3].type, 'thread');
|
|
testlib.assertTrue(type(result.returns[3].repr) == 'string');
|
|
end);
|
|
|
|
testlib.test('executeLua leaves direct terminal writes out of captured output', function()
|
|
local mcpComputer = createMcpComputer();
|
|
local result = mcpComputer.executeLua("term.write('visible'); return 'done'");
|
|
|
|
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('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 invalid exec-lua code', function()
|
|
local mcpComputer = createMcpComputer();
|
|
local response = mcpComputer.handleRequest({
|
|
type = 'request',
|
|
id = 'req-empty-exec',
|
|
method = 'exec-lua',
|
|
params = { code = '' },
|
|
}, fakeOs(42, 'worker'));
|
|
|
|
testlib.assertEquals(response.type, 'response');
|
|
testlib.assertEquals(response.id, 'req-empty-exec');
|
|
testlib.assertEquals(response.ok, false);
|
|
testlib.assertEquals(response.result.output, '');
|
|
testlib.assertTrue(string.find(response.error, 'non-empty string', 1, true));
|
|
|
|
local missing = mcpComputer.handleRequest({
|
|
type = 'request',
|
|
id = 'req-missing-exec',
|
|
method = 'exec-lua',
|
|
params = {},
|
|
}, fakeOs(42, 'worker'));
|
|
|
|
testlib.assertEquals(missing.ok, false);
|
|
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({
|
|
type = 'request',
|
|
id = 'req-2',
|
|
method = 'dance',
|
|
}, fakeOs(42, 'worker'));
|
|
|
|
testlib.assertEquals(response.type, 'response');
|
|
testlib.assertEquals(response.id, 'req-2');
|
|
testlib.assertEquals(response.ok, false);
|
|
testlib.assertEquals(response.error, 'unknown method');
|
|
end);
|
|
|
|
testlib.test('handleRequest ignores malformed frames', function()
|
|
local mcpComputer = createMcpComputer();
|
|
|
|
testlib.assertEquals(mcpComputer.handleRequest({ type = 'request', method = 'ping' }, fakeOs(1, nil)), nil);
|
|
testlib.assertEquals(mcpComputer.handleRequest({ type = 'hello' }, fakeOs(1, nil)), nil);
|
|
end);
|
|
|
|
testlib.run();
|