1329 lines
44 KiB
Lua
1329 lines
44 KiB
Lua
local createLibTest = require('/apis/libtest');
|
|
local createAi = require('/apis/libai');
|
|
|
|
local testlib = createLibTest({ ... });
|
|
|
|
local function fakeSettings(initial)
|
|
local values = initial or {};
|
|
local getCounts = {};
|
|
local saveCount = 0;
|
|
return {
|
|
get = function(key)
|
|
getCounts[key] = (getCounts[key] or 0) + 1;
|
|
return values[key];
|
|
end,
|
|
set = function(key, value) values[key] = value; end,
|
|
unset = function(key) values[key] = nil; end,
|
|
save = function() saveCount = saveCount + 1; end,
|
|
values = values,
|
|
getCount = function(key) return getCounts[key] or 0; end,
|
|
saveCount = function() return saveCount; end,
|
|
};
|
|
end
|
|
|
|
local function fakeOs(computerId, computerLabel)
|
|
return {
|
|
getComputerID = function() return computerId; end,
|
|
getComputerLabel = function() return computerLabel; end,
|
|
};
|
|
end
|
|
|
|
local function response(code, body)
|
|
return {
|
|
getResponseCode = function() return code; end,
|
|
readAll = function() return body; end,
|
|
close = function() end,
|
|
};
|
|
end
|
|
|
|
-- postResults: list of responses returned in order for each POST call.
|
|
-- getResults: list of responses returned in order for each GET call.
|
|
local function fakeHttp(postResults, getResults)
|
|
postResults = postResults or {};
|
|
getResults = getResults or {};
|
|
local postCalls = {};
|
|
local getCalls = {};
|
|
local postIdx = 0;
|
|
local getIdx = 0;
|
|
return {
|
|
post = function(req)
|
|
local url, body, headers, timeout = req.url, req.body, req.headers, req.timeout;
|
|
postCalls[#postCalls + 1] = { url = url, body = body, headers = headers, timeout = timeout };
|
|
postIdx = postIdx + 1;
|
|
local r = postResults[postIdx];
|
|
if type(r) == 'function' then return r(url, body, headers); end
|
|
return r;
|
|
end,
|
|
get = function(req)
|
|
local url, headers, timeout = req.url, req.headers, req.timeout;
|
|
getCalls[#getCalls + 1] = { url = url, headers = headers, timeout = timeout };
|
|
getIdx = getIdx + 1;
|
|
local r = getResults[getIdx];
|
|
if type(r) == 'function' then return r(url, headers); end
|
|
return r;
|
|
end,
|
|
postCalls = postCalls,
|
|
getCalls = getCalls,
|
|
};
|
|
end
|
|
|
|
-- Fake CC http library exposing websocket(), for the bridge ws transport.
|
|
-- wsResults: list of { status=, body=, error= } returned per round-trip in order.
|
|
-- A nil entry simulates ws.receive timing out. opts.connectFail makes
|
|
-- http.websocket return (nil, err).
|
|
local function fakeWsHttp(wsResults, opts)
|
|
wsResults = wsResults or {};
|
|
opts = opts or {};
|
|
local sent = {};
|
|
local receiveTimeouts = {};
|
|
local idx = 0;
|
|
local lastId = nil;
|
|
local closed = false;
|
|
local connectUrl = nil;
|
|
|
|
local ws = {
|
|
send = function(text)
|
|
local frame = textutils.unserializeJSON(text);
|
|
sent[#sent + 1] = frame;
|
|
lastId = frame and frame.id;
|
|
end,
|
|
receive = function(timeout)
|
|
receiveTimeouts[#receiveTimeouts + 1] = timeout;
|
|
idx = idx + 1;
|
|
local r = wsResults[idx];
|
|
if r == nil then return nil; end
|
|
return textutils.serializeJSON({
|
|
type = 'http-response',
|
|
id = lastId,
|
|
status = r.status,
|
|
body = r.body,
|
|
error = r.error,
|
|
});
|
|
end,
|
|
close = function() closed = true; end,
|
|
};
|
|
|
|
local httpLib = {
|
|
websocket = function(url)
|
|
connectUrl = url;
|
|
if opts.connectFail then return nil, 'refused'; end
|
|
return ws;
|
|
end,
|
|
};
|
|
|
|
return {
|
|
http = httpLib,
|
|
sent = sent,
|
|
isClosed = function() return closed; end,
|
|
connectUrl = function() return connectUrl; end,
|
|
lastReceiveTimeout = function() return receiveTimeouts[#receiveTimeouts]; end,
|
|
};
|
|
end
|
|
|
|
local function httpError(code, body)
|
|
return function()
|
|
return nil, 'HTTP response code ' .. tostring(code), response(code, body);
|
|
end;
|
|
end
|
|
|
|
local function sessionResp(id)
|
|
return response(200, textutils.serializeJSON({ id = id, title = 'cc-ai' }));
|
|
end
|
|
|
|
local function messageResp(reply)
|
|
return response(200, textutils.serializeJSON({
|
|
info = { time = { completed = 1 } },
|
|
parts = { { type = 'text', text = reply } },
|
|
}));
|
|
end
|
|
|
|
local function wsResult(status, body)
|
|
return { status = status, body = body };
|
|
end
|
|
|
|
local function wsSessionResult(id)
|
|
return wsResult(200, textutils.serializeJSON({ id = id, title = 'cc-ai' }));
|
|
end
|
|
|
|
local function wsMessageResult(reply)
|
|
return wsResult(200, textutils.serializeJSON({
|
|
info = { time = { completed = 1 } },
|
|
parts = { { type = 'text', text = reply } },
|
|
}));
|
|
end
|
|
|
|
local function postedText(call)
|
|
local body = textutils.unserializeJSON(call.body);
|
|
return body.parts[1].text;
|
|
end
|
|
|
|
-- base64 --
|
|
|
|
testlib.test('base64encode encodes simple ascii', function()
|
|
-- "Man" -> "TWFu" is the canonical base64 test vector; tested indirectly via Authorization header
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_1'), messageResp('ok') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
settingsStub.values['opencc.password'] = 'pass';
|
|
settingsStub.values['opencc.username'] = 'user';
|
|
createAi({ http = httpStub, settings = settingsStub }).ask('hello');
|
|
local auth = httpStub.postCalls[1].headers['Authorization'];
|
|
-- base64('user:pass') = 'dXNlcjpwYXNz'
|
|
testlib.assertEquals(auth, 'Basic dXNlcjpwYXNz');
|
|
end);
|
|
|
|
testlib.test('base64encode handles padding with one remainder byte', function()
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_1'), messageResp('ok') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
settingsStub.values['opencc.password'] = 'x';
|
|
settingsStub.values['opencc.username'] = 'a';
|
|
createAi({ http = httpStub, settings = settingsStub }).ask('hello');
|
|
-- base64('a:x') = 'YTp4'
|
|
testlib.assertEquals(httpStub.postCalls[1].headers['Authorization'], 'Basic YTp4');
|
|
end);
|
|
|
|
-- listSessions --
|
|
|
|
testlib.test('listSessions returns newest sessions first', function()
|
|
local sessions = {
|
|
{ id = 'ses_old', title = 'old', time = { updated = 10 } },
|
|
{ id = 'ses_created', title = 'created', time = { created = 20 } },
|
|
{ id = 'ses_new', title = 'new', time = { updated = 30 } },
|
|
{ id = 'ses_missing', title = 'missing' },
|
|
};
|
|
local httpStub = fakeHttp({}, { response(200, textutils.serializeJSON(sessions)) });
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, result = ai.listSessions();
|
|
|
|
testlib.assertTrue(ok, tostring(result));
|
|
testlib.assertEquals(#result, 4);
|
|
testlib.assertEquals(result[1].id, 'ses_new');
|
|
testlib.assertEquals(result[2].id, 'ses_created');
|
|
testlib.assertEquals(result[3].id, 'ses_old');
|
|
testlib.assertEquals(result[4].id, 'ses_missing');
|
|
end);
|
|
|
|
testlib.test('listSessions logs session count when verbose', function()
|
|
local sessions = {
|
|
{ id = 'ses_1', title = 'one', time = { updated = 10 } },
|
|
{ id = 'ses_2', title = 'two', time = { updated = 20 } },
|
|
};
|
|
local httpStub = fakeHttp({}, { response(200, textutils.serializeJSON(sessions)) });
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local logs = {};
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok = ai.listSessions({
|
|
log = function(message) logs[#logs + 1] = message; end,
|
|
});
|
|
|
|
testlib.assertTrue(ok);
|
|
testlib.assertTrue(string.find(logs[1], 'http://host', 1, true) ~= nil);
|
|
testlib.assertEquals(logs[2], 'sessions returned: 2');
|
|
end);
|
|
|
|
testlib.test('listSessions sends configured directory query', function()
|
|
local sessions = {
|
|
{ id = 'ses_1', title = 'one', time = { updated = 10 } },
|
|
};
|
|
local httpStub = fakeHttp({}, { response(200, textutils.serializeJSON(sessions)) });
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.directory'] = '/Users/garm/trap/cc-libs',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, result = ai.listSessions();
|
|
|
|
testlib.assertTrue(ok, tostring(result));
|
|
testlib.assertEquals(#result, 1);
|
|
testlib.assertEquals(httpStub.getCalls[1].url,
|
|
'http://host/session?directory=%2FUsers%2Fgarm%2Ftrap%2Fcc-libs');
|
|
end);
|
|
|
|
testlib.test('listSessions caps HTTP timeout at one minute', function()
|
|
local httpStub = fakeHttp({}, { response(200, '[]') });
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.timeout_seconds'] = 120,
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok = ai.listSessions();
|
|
|
|
testlib.assertTrue(ok);
|
|
testlib.assertEquals(httpStub.getCalls[1].timeout, 60);
|
|
end);
|
|
|
|
testlib.test('listSessions retries with persisted session directory when list is empty', function()
|
|
local scopedSessions = {
|
|
{ id = 'ses_existing', title = 'existing', time = { updated = 20 } },
|
|
};
|
|
local session = {
|
|
id = 'ses_existing',
|
|
title = 'existing',
|
|
directory = '/Users/garm/trap/cc-libs',
|
|
};
|
|
local httpStub = fakeHttp({}, {
|
|
response(200, '[]'),
|
|
response(200, textutils.serializeJSON(session)),
|
|
response(200, textutils.serializeJSON(scopedSessions)),
|
|
});
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_existing',
|
|
});
|
|
local logs = {};
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, result = ai.listSessions({
|
|
log = function(message) logs[#logs + 1] = message; end,
|
|
});
|
|
|
|
testlib.assertTrue(ok, tostring(result));
|
|
testlib.assertEquals(#result, 1);
|
|
testlib.assertEquals(result[1].id, 'ses_existing');
|
|
testlib.assertEquals(httpStub.getCalls[1].url, 'http://host/session');
|
|
testlib.assertEquals(httpStub.getCalls[2].url, 'http://host/session/ses_existing');
|
|
testlib.assertEquals(httpStub.getCalls[3].url,
|
|
'http://host/session?directory=%2FUsers%2Fgarm%2Ftrap%2Fcc-libs');
|
|
testlib.assertTrue(string.find(logs[2], 'resolving directory', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('listSessions fails when server_url missing', function()
|
|
local httpStub = fakeHttp({}, {});
|
|
local ai = createAi({ http = httpStub, settings = fakeSettings() });
|
|
|
|
local ok, err = ai.listSessions();
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'opencc.server_url', 1, true) ~= nil);
|
|
testlib.assertTrue(string.find(err, 'set opencc.server_url', 1, true) ~= nil);
|
|
testlib.assertTrue(string.find(err, 'settings set', 1, true) == nil);
|
|
testlib.assertEquals(#httpStub.getCalls, 0);
|
|
end);
|
|
|
|
testlib.test('listSessions fails when server unreachable', function()
|
|
local httpStub = fakeHttp({}, { nil });
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, err = ai.listSessions();
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'injoignable', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('listSessions maps HTTP error response codes', function()
|
|
local httpStub = fakeHttp({}, { httpError(401, '{}') });
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, err = ai.listSessions();
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'HTTP 401', 1, true) ~= nil);
|
|
end);
|
|
|
|
-- ask --
|
|
|
|
testlib.test('ask creates session then sends message when no session_id', function()
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_new'), messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host:4096' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, result = ai.ask('hello');
|
|
|
|
testlib.assertTrue(ok, tostring(result));
|
|
testlib.assertEquals(result.reply, 'reply');
|
|
testlib.assertEquals(result.sessionId, 'ses_new');
|
|
testlib.assertEquals(#httpStub.postCalls, 2);
|
|
testlib.assertTrue(string.find(httpStub.postCalls[1].url, '/session', 1, true) ~= nil);
|
|
testlib.assertTrue(string.find(httpStub.postCalls[2].url, '/session/ses_new/message', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('ask creates cc-ai titled sessions', function()
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_new'), messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello');
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(body.title, 'cc-ai');
|
|
end);
|
|
|
|
testlib.test('ask saves new session_id to settings', function()
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_abc'), messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello');
|
|
|
|
testlib.assertEquals(settingsStub.values['opencc.session_id'], 'ses_abc');
|
|
testlib.assertEquals(settingsStub.saveCount(), 1);
|
|
end);
|
|
|
|
testlib.test('ask saves custom sessionSettingKey to settings', function()
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_trapgpt'), messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello', { sessionSettingKey = 'trapgpt.opencc.session_id' });
|
|
|
|
testlib.assertEquals(settingsStub.values['trapgpt.opencc.session_id'], 'ses_trapgpt');
|
|
testlib.assertEquals(settingsStub.values['opencc.session_id'], nil);
|
|
testlib.assertEquals(settingsStub.saveCount(), 1);
|
|
end);
|
|
|
|
testlib.test('ask reuses existing session_id without creating a new session', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_existing',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok = ai.ask('hello');
|
|
|
|
testlib.assertTrue(ok);
|
|
testlib.assertEquals(#httpStub.postCalls, 1);
|
|
testlib.assertTrue(string.find(httpStub.postCalls[1].url, '/session/ses_existing/message', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('ask reuses custom sessionSettingKey without creating a new session', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_other',
|
|
['trapgpt.opencc.session_id'] = 'ses_trapgpt',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok = ai.ask('hello', { sessionSettingKey = 'trapgpt.opencc.session_id' });
|
|
|
|
testlib.assertTrue(ok);
|
|
testlib.assertEquals(#httpStub.postCalls, 1);
|
|
testlib.assertTrue(string.find(httpStub.postCalls[1].url, '/session/ses_trapgpt/message', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('ask uses synchronous message endpoint by default', function()
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_sync'), messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, result = ai.ask('hello');
|
|
|
|
testlib.assertTrue(ok, tostring(result));
|
|
testlib.assertEquals(result.reply, 'reply');
|
|
testlib.assertEquals(#httpStub.postCalls, 2);
|
|
testlib.assertTrue(string.find(httpStub.postCalls[2].url, '/session/ses_sync/message', 1, true) ~= nil);
|
|
testlib.assertTrue(string.find(httpStub.postCalls[2].url, 'prompt_async', 1, true) == nil);
|
|
testlib.assertEquals(#httpStub.getCalls, 0);
|
|
end);
|
|
|
|
testlib.test('ask wraps prompt with caller context', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
});
|
|
local ai = createAi({
|
|
http = httpStub,
|
|
settings = settingsStub,
|
|
os = fakeOs(42, 'storage-main'),
|
|
});
|
|
|
|
ai.ask('my prompt');
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(#body.parts, 1);
|
|
testlib.assertEquals(body.parts[1].type, 'text');
|
|
testlib.assertTrue(string.find(body.parts[1].text, '<caller-context hidden="true">', 1, true) ~= nil);
|
|
testlib.assertTrue(string.find(body.parts[1].text, 'computer id: 42', 1, true) ~= nil);
|
|
testlib.assertTrue(string.find(body.parts[1].text, 'computer label: storage-main', 1, true) ~= nil);
|
|
testlib.assertTrue(string.find(body.parts[1].text, 'User prompt:\nmy prompt', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('ask omits blank caller label', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
});
|
|
local ai = createAi({
|
|
http = httpStub,
|
|
settings = settingsStub,
|
|
os = fakeOs(7, ''),
|
|
});
|
|
|
|
ai.ask('my prompt');
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertTrue(string.find(body.parts[1].text, 'computer id: 7', 1, true) ~= nil);
|
|
testlib.assertEquals(string.find(body.parts[1].text, 'computer label:', 1, true), nil);
|
|
end);
|
|
|
|
testlib.test('ask includes agent from settings', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
['opencc.agent'] = 'atm10-expert',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello');
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(body.agent, 'atm10-expert');
|
|
end);
|
|
|
|
testlib.test('ask option agent overrides settings', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
['opencc.agent'] = 'build',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello', { agent = 'atm10-expert' });
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(body.agent, 'atm10-expert');
|
|
end);
|
|
|
|
testlib.test('ask omits blank agent setting', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
['opencc.agent'] = ' ',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello');
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(body.agent, nil);
|
|
end);
|
|
|
|
testlib.test('ask includes variant when setting is set', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
['opencc.variant'] = 'low',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello');
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(body.variant, 'low');
|
|
end);
|
|
|
|
testlib.test('ask omits blank variant setting', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
['opencc.variant'] = ' ',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello');
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(body.variant, nil);
|
|
end);
|
|
|
|
testlib.test('ask includes model when provider_id and model_id are set', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
['opencc.provider_id'] = 'anthropic',
|
|
['opencc.model_id'] = 'claude-opus-4-7',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello');
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(body.model.providerID, 'anthropic');
|
|
testlib.assertEquals(body.model.modelID, 'claude-opus-4-7');
|
|
end);
|
|
|
|
testlib.test('ask omits model when provider_id is missing', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
['opencc.model_id'] = 'claude-opus-4-7',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello');
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(body.model, nil);
|
|
end);
|
|
|
|
testlib.test('ask omits model when model_id is missing', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
['opencc.provider_id'] = 'anthropic',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello');
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(body.model, nil);
|
|
end);
|
|
|
|
testlib.test('ask omits model when neither provider_id nor model_id is set', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello');
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(body.model, nil);
|
|
end);
|
|
|
|
testlib.test('ask options providerID/modelID override settings', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
['opencc.provider_id'] = 'anthropic',
|
|
['opencc.model_id'] = 'claude-opus-4-7',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello', { providerID = 'openai', modelID = 'gpt-5' });
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(body.model.providerID, 'openai');
|
|
testlib.assertEquals(body.model.modelID, 'gpt-5');
|
|
end);
|
|
|
|
testlib.test('ask options variant overrides setting', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
['opencc.variant'] = 'high',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello', { variant = 'low' });
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(body.variant, 'low');
|
|
end);
|
|
|
|
testlib.test('ask uses one minute HTTP timeout by default', function()
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_1'), messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok = ai.ask('hello');
|
|
|
|
testlib.assertTrue(ok);
|
|
testlib.assertEquals(httpStub.postCalls[1].timeout, 60);
|
|
testlib.assertEquals(httpStub.postCalls[2].timeout, 60);
|
|
end);
|
|
|
|
testlib.test('ask caps per-call HTTP timeout at one minute', function()
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_1'), messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.timeout_seconds'] = 500,
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok = ai.ask('hello');
|
|
|
|
testlib.assertTrue(ok);
|
|
testlib.assertEquals(httpStub.postCalls[1].timeout, 60);
|
|
testlib.assertEquals(httpStub.postCalls[2].timeout, 60);
|
|
end);
|
|
|
|
testlib.test('ask rejects missing prompt without HTTP calls', function()
|
|
local httpStub = fakeHttp({}, {});
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, err = ai.ask();
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'missing prompt', 1, true) ~= nil);
|
|
testlib.assertEquals(#httpStub.postCalls, 0);
|
|
testlib.assertEquals(#httpStub.getCalls, 0);
|
|
end);
|
|
|
|
testlib.test('ask rejects blank prompt without HTTP calls', function()
|
|
local httpStub = fakeHttp({}, {});
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, err = ai.ask(' ');
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'missing prompt', 1, true) ~= nil);
|
|
testlib.assertEquals(#httpStub.postCalls, 0);
|
|
testlib.assertEquals(#httpStub.getCalls, 0);
|
|
end);
|
|
|
|
testlib.test('ask concatenates multiple text parts', function()
|
|
local httpStub = fakeHttp(
|
|
{ response(200, textutils.serializeJSON({
|
|
info = {},
|
|
parts = {
|
|
{ type = 'step-start' },
|
|
{ type = 'text', text = 'hello ' },
|
|
{ type = 'tool-call' },
|
|
{ type = 'text', text = 'world' },
|
|
},
|
|
})) },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, result = ai.ask('hello');
|
|
|
|
testlib.assertTrue(ok, tostring(result));
|
|
testlib.assertEquals(result.reply, 'hello world');
|
|
end);
|
|
|
|
testlib.test('ask fails with missing server_url', function()
|
|
local httpStub = fakeHttp({}, {});
|
|
local ai = createAi({ http = httpStub, settings = fakeSettings() });
|
|
|
|
local ok, err = ai.ask('hello');
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'opencc.server_url', 1, true) ~= nil);
|
|
testlib.assertTrue(string.find(err, 'set opencc.server_url', 1, true) ~= nil);
|
|
testlib.assertTrue(string.find(err, 'settings set', 1, true) == nil);
|
|
testlib.assertEquals(#httpStub.postCalls, 0);
|
|
end);
|
|
|
|
testlib.test('ask fails when server unreachable on session create', function()
|
|
local httpStub = fakeHttp({ nil }, {});
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, err = ai.ask('hello');
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'injoignable', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('ask fails when server unreachable on message send', function()
|
|
local httpStub = fakeHttp({ sessionResp('ses_1'), nil }, {});
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, err = ai.ask('hello');
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'injoignable', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('ask maps 401 on message send', function()
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_1'), response(401, '{}') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, err = ai.ask('hello');
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'HTTP 401', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('ask maps HTTP error response on message send', function()
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_1'), httpError(401, '{}') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, err = ai.ask('hello');
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'HTTP 401', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('ask on 404 clears session_id and suggests ai new', function()
|
|
local httpStub = fakeHttp(
|
|
{ response(404, '{}') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_stale',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, err = ai.ask('hello');
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'ai new', 1, true) ~= nil);
|
|
testlib.assertEquals(settingsStub.values['opencc.session_id'], nil);
|
|
end);
|
|
|
|
testlib.test('ask on HTTP error 404 clears session_id', function()
|
|
local httpStub = fakeHttp(
|
|
{ httpError(404, '{}') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_stale',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, err = ai.ask('hello');
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'ai new', 1, true) ~= nil);
|
|
testlib.assertEquals(settingsStub.values['opencc.session_id'], nil);
|
|
end);
|
|
|
|
testlib.test('ask persist=false creates a session without saving session_id', function()
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_ephemeral'), messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_persisted',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, result = ai.ask('hello', { persist = false });
|
|
|
|
testlib.assertTrue(ok, tostring(result));
|
|
testlib.assertEquals(result.sessionId, 'ses_ephemeral');
|
|
testlib.assertEquals(settingsStub.values['opencc.session_id'], 'ses_persisted');
|
|
testlib.assertEquals(settingsStub.getCount('opencc.session_id'), 0);
|
|
testlib.assertEquals(settingsStub.saveCount(), 0);
|
|
testlib.assertEquals(#httpStub.postCalls, 2);
|
|
testlib.assertTrue(string.find(httpStub.postCalls[1].url, '/session', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('ask persist=false does not clear session_id on 404', function()
|
|
local httpStub = fakeHttp(
|
|
{ response(404, '{}') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_persisted',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, err = ai.ask('hello', { persist = false, sessionId = 'ses_missing' });
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'session introuvable', 1, true) ~= nil);
|
|
testlib.assertEquals(settingsStub.values['opencc.session_id'], 'ses_persisted');
|
|
testlib.assertEquals(settingsStub.getCount('opencc.session_id'), 0);
|
|
testlib.assertEquals(settingsStub.saveCount(), 0);
|
|
end);
|
|
|
|
testlib.test('ask omits Authorization header when no password', function()
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_1'), messageResp('reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
ai.ask('hello');
|
|
|
|
testlib.assertEquals(httpStub.postCalls[1].headers['Authorization'], nil);
|
|
end);
|
|
|
|
-- ask over the bridge ws transport --
|
|
|
|
testlib.test('ask over bridge uses synchronous message endpoint', function()
|
|
local ws = fakeWsHttp({
|
|
wsSessionResult('ses_ws'),
|
|
wsMessageResult('reply'),
|
|
});
|
|
local settingsStub = fakeSettings({ ['opencc.bridge_url'] = 'ws://bridge' });
|
|
local ai = createAi({ http = ws.http, settings = settingsStub });
|
|
|
|
local ok, result = ai.ask('hello');
|
|
|
|
testlib.assertTrue(ok, tostring(result));
|
|
testlib.assertEquals(result.reply, 'reply');
|
|
testlib.assertEquals(result.sessionId, 'ses_ws');
|
|
testlib.assertEquals(ws.connectUrl(), 'ws://bridge');
|
|
testlib.assertEquals(#ws.sent, 2);
|
|
testlib.assertEquals(ws.sent[1].method, 'POST');
|
|
testlib.assertEquals(ws.sent[1].path, '/session');
|
|
testlib.assertEquals(ws.sent[2].method, 'POST');
|
|
testlib.assertEquals(ws.sent[2].path, '/session/ses_ws/message');
|
|
testlib.assertTrue(string.find(ws.sent[2].path, 'prompt_async', 1, true) == nil);
|
|
end);
|
|
|
|
testlib.test('ask uses bridge when server_url has ws scheme', function()
|
|
local ws = fakeWsHttp({
|
|
wsSessionResult('ses_ws'),
|
|
wsMessageResult('reply'),
|
|
});
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'ws://bridgehost' });
|
|
local ai = createAi({ http = ws.http, settings = settingsStub });
|
|
|
|
local ok, result = ai.ask('hello');
|
|
|
|
testlib.assertTrue(ok, tostring(result));
|
|
testlib.assertEquals(result.reply, 'reply');
|
|
testlib.assertEquals(ws.connectUrl(), 'ws://bridgehost');
|
|
testlib.assertTrue(#ws.sent >= 1);
|
|
end);
|
|
|
|
testlib.test('ask over bridge sends body as serialized json', function()
|
|
local ws = fakeWsHttp({ wsMessageResult('reply') });
|
|
local settingsStub = fakeSettings({
|
|
['opencc.bridge_url'] = 'ws://bridge',
|
|
['opencc.session_id'] = 'ses_1',
|
|
['opencc.agent'] = 'atm10-expert',
|
|
});
|
|
local ai = createAi({ http = ws.http, settings = settingsStub, os = fakeOs(5, 'turtle') });
|
|
|
|
local ok = ai.ask('hello');
|
|
|
|
testlib.assertTrue(ok);
|
|
testlib.assertEquals(#ws.sent, 1);
|
|
testlib.assertEquals(ws.sent[1].path, '/session/ses_1/message');
|
|
local body = textutils.unserializeJSON(ws.sent[1].body);
|
|
testlib.assertEquals(body.agent, 'atm10-expert');
|
|
testlib.assertTrue(string.find(body.parts[1].text, 'computer id: 5', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('ask over bridge surfaces a transport error frame', function()
|
|
local ws = fakeWsHttp({ { status = 0, error = 'fetch failed: ECONNREFUSED' } });
|
|
local settingsStub = fakeSettings({
|
|
['opencc.bridge_url'] = 'ws://bridge',
|
|
['opencc.session_id'] = 'ses_1',
|
|
});
|
|
local ai = createAi({ http = ws.http, settings = settingsStub });
|
|
|
|
local ok, err = ai.ask('hello');
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'fetch failed', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('ask over bridge times out when no reply', function()
|
|
local ws = fakeWsHttp({});
|
|
local settingsStub = fakeSettings({
|
|
['opencc.bridge_url'] = 'ws://bridge',
|
|
['opencc.session_id'] = 'ses_1',
|
|
});
|
|
local ai = createAi({ http = ws.http, settings = settingsStub });
|
|
|
|
local ok, err = ai.ask('hello');
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'timeout', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('ask over bridge maps a 404 frame to missing session', function()
|
|
local ws = fakeWsHttp({ wsResult(404, '{}') });
|
|
local settingsStub = fakeSettings({
|
|
['opencc.bridge_url'] = 'ws://bridge',
|
|
['opencc.session_id'] = 'ses_stale',
|
|
});
|
|
local ai = createAi({ http = ws.http, settings = settingsStub });
|
|
|
|
local ok, err = ai.ask('hello');
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'session introuvable', 1, true) ~= nil);
|
|
testlib.assertEquals(settingsStub.values['opencc.session_id'], nil);
|
|
end);
|
|
|
|
testlib.test('ask over bridge fails when websocket connect fails', function()
|
|
local ws = fakeWsHttp({}, { connectFail = true });
|
|
local settingsStub = fakeSettings({
|
|
['opencc.bridge_url'] = 'ws://bridge',
|
|
['opencc.session_id'] = 'ses_1',
|
|
});
|
|
local ai = createAi({ http = ws.http, settings = settingsStub });
|
|
|
|
local ok, err = ai.ask('hello');
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertTrue(string.find(err, 'bridge unreachable', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('ask over bridge uses request_timeout_seconds for ws receive', function()
|
|
local ws = fakeWsHttp({ wsMessageResult('reply') });
|
|
local settingsStub = fakeSettings({
|
|
['opencc.bridge_url'] = 'ws://bridge',
|
|
['opencc.session_id'] = 'ses_1',
|
|
['opencc.request_timeout_seconds'] = 120,
|
|
});
|
|
local ai = createAi({ http = ws.http, settings = settingsStub });
|
|
|
|
local ok = ai.ask('hello');
|
|
|
|
testlib.assertTrue(ok);
|
|
testlib.assertEquals(ws.lastReceiveTimeout(), 120);
|
|
end);
|
|
|
|
testlib.test('ask over bridge caps request_timeout_seconds at ten minutes', function()
|
|
local ws = fakeWsHttp({ wsMessageResult('reply') });
|
|
local settingsStub = fakeSettings({
|
|
['opencc.bridge_url'] = 'ws://bridge',
|
|
['opencc.session_id'] = 'ses_1',
|
|
['opencc.request_timeout_seconds'] = 5000,
|
|
});
|
|
local ai = createAi({ http = ws.http, settings = settingsStub });
|
|
|
|
local ok = ai.ask('hello');
|
|
|
|
testlib.assertTrue(ok);
|
|
testlib.assertEquals(ws.lastReceiveTimeout(), 600);
|
|
end);
|
|
|
|
-- lua executor --
|
|
|
|
testlib.test('lua executor captures print write and returned values', function()
|
|
local ai = createAi({ http = fakeHttp({}, {}), settings = fakeSettings() });
|
|
local executor = ai.createLuaExecutor({ live = false, luaTimeoutSeconds = false });
|
|
|
|
local ok, output, kind = executor('print("hello", "world")\nwrite("x")\nreturn 7, "z"');
|
|
|
|
testlib.assertTrue(ok, tostring(output));
|
|
testlib.assertEquals(kind, nil);
|
|
testlib.assertEquals(output, 'hello\tworld\nx\n7\tz\n');
|
|
end);
|
|
|
|
testlib.test('lua executor classifies syntax and identifier errors', function()
|
|
local ai = createAi({ http = fakeHttp({}, {}), settings = fakeSettings() });
|
|
local executor = ai.createLuaExecutor({ live = false, luaTimeoutSeconds = false });
|
|
|
|
local syntaxOk, _, syntaxKind = executor('if true then');
|
|
local identifierOk, identifierErr, identifierKind = executor('definitely_missing()');
|
|
|
|
testlib.assertTrue(not syntaxOk);
|
|
testlib.assertEquals(syntaxKind, 'syntax');
|
|
testlib.assertTrue(not identifierOk);
|
|
testlib.assertEquals(identifierKind, 'identifier');
|
|
testlib.assertTrue(string.find(identifierErr, 'nil', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('lua executor classifies non-identifier runtime errors as other', function()
|
|
local ai = createAi({ http = fakeHttp({}, {}), settings = fakeSettings() });
|
|
local executor = ai.createLuaExecutor({ live = false, luaTimeoutSeconds = false });
|
|
|
|
local ok, err, kind = executor('error("boom")');
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertEquals(kind, 'other');
|
|
testlib.assertTrue(string.find(err, 'boom', 1, true) ~= nil);
|
|
end);
|
|
|
|
-- luaExec --
|
|
|
|
testlib.test('luaExec succeeds on first attempt and sends output for final reply', function()
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_lua'), messageResp('print("hello")'), messageResp('final reply') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_persisted',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
local execCalls = {};
|
|
|
|
local ok, result = ai.luaExec('say hello', {
|
|
executor = function(code)
|
|
execCalls[#execCalls + 1] = code;
|
|
return true, 'hello\n', nil;
|
|
end,
|
|
});
|
|
|
|
testlib.assertTrue(ok, tostring(result));
|
|
testlib.assertEquals(result.reply, 'final reply');
|
|
testlib.assertEquals(result.output, 'hello\n');
|
|
testlib.assertEquals(result.attempts, 1);
|
|
testlib.assertEquals(#execCalls, 1);
|
|
testlib.assertEquals(settingsStub.values['opencc.session_id'], 'ses_persisted');
|
|
testlib.assertEquals(settingsStub.getCount('opencc.session_id'), 0);
|
|
testlib.assertEquals(settingsStub.saveCount(), 0);
|
|
testlib.assertEquals(#httpStub.postCalls, 3);
|
|
|
|
local sessionBody = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(sessionBody.title, 'cc-ai lua-exec');
|
|
testlib.assertTrue(string.find(postedText(httpStub.postCalls[2]), 'raw Lua code only', 1, true) ~= nil);
|
|
testlib.assertTrue(string.find(postedText(httpStub.postCalls[2]), 'say hello', 1, true) ~= nil);
|
|
testlib.assertTrue(string.find(postedText(httpStub.postCalls[3]), 'Lua output:', 1, true) ~= nil);
|
|
testlib.assertTrue(string.find(postedText(httpStub.postCalls[3]), 'natural language', 1, true) ~= nil);
|
|
testlib.assertTrue(string.find(postedText(httpStub.postCalls[3]), 'hello', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('luaExec retries on identifier error and succeeds', function()
|
|
local httpStub = fakeHttp(
|
|
{
|
|
sessionResp('ses_lua'),
|
|
messageResp('fo0()'),
|
|
messageResp('print("fixed")'),
|
|
messageResp('fixed reply'),
|
|
},
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
local execCalls = 0;
|
|
|
|
local ok, result = ai.luaExec('fix name', {
|
|
executor = function()
|
|
execCalls = execCalls + 1;
|
|
if execCalls == 1 then
|
|
return false, 'attempt to call a nil value (global "fo0")', 'identifier';
|
|
end
|
|
return true, 'fixed\n', nil;
|
|
end,
|
|
});
|
|
|
|
testlib.assertTrue(ok, tostring(result));
|
|
testlib.assertEquals(result.reply, 'fixed reply');
|
|
testlib.assertEquals(result.attempts, 2);
|
|
testlib.assertEquals(execCalls, 2);
|
|
testlib.assertEquals(#httpStub.postCalls, 4);
|
|
testlib.assertTrue(string.find(postedText(httpStub.postCalls[3]), 'Error kind: identifier', 1, true) ~= nil);
|
|
testlib.assertTrue(string.find(postedText(httpStub.postCalls[3]), 'fo0()', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('luaExec retries on syntax error and succeeds', function()
|
|
local httpStub = fakeHttp(
|
|
{
|
|
sessionResp('ses_lua'),
|
|
messageResp('if true then'),
|
|
messageResp('print("ok")'),
|
|
messageResp('ok reply'),
|
|
},
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
local execCalls = 0;
|
|
|
|
local ok, result = ai.luaExec('write code', {
|
|
executor = function()
|
|
execCalls = execCalls + 1;
|
|
if execCalls == 1 then
|
|
return false, "'end' expected", 'syntax';
|
|
end
|
|
return true, 'ok\n', nil;
|
|
end,
|
|
maxRetries = 1,
|
|
});
|
|
|
|
testlib.assertTrue(ok, tostring(result));
|
|
testlib.assertEquals(result.reply, 'ok reply');
|
|
testlib.assertEquals(result.attempts, 2);
|
|
testlib.assertEquals(execCalls, 2);
|
|
testlib.assertEquals(#httpStub.postCalls, 4);
|
|
testlib.assertTrue(string.find(postedText(httpStub.postCalls[3]), 'Error kind: syntax', 1, true) ~= nil);
|
|
end);
|
|
|
|
testlib.test('luaExec aborts on non-identifier runtime error', function()
|
|
local httpStub = fakeHttp(
|
|
{ sessionResp('ses_lua'), messageResp('peripheral.wrap("left").call()') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, result = ai.luaExec('use peripheral', {
|
|
executor = function()
|
|
return false, 'peripheral not present', 'other';
|
|
end,
|
|
});
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertEquals(result.error, 'peripheral not present');
|
|
testlib.assertEquals(result.errorKind, 'other');
|
|
testlib.assertEquals(result.attempts, 1);
|
|
testlib.assertEquals(#httpStub.postCalls, 2);
|
|
end);
|
|
|
|
testlib.test('luaExec exhausts retry budget', function()
|
|
local httpStub = fakeHttp(
|
|
{
|
|
sessionResp('ses_lua'),
|
|
messageResp('bad1()'),
|
|
messageResp('bad2()'),
|
|
messageResp('bad3()'),
|
|
},
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' });
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
local execCalls = 0;
|
|
|
|
local ok, result = ai.luaExec('keep failing', {
|
|
executor = function()
|
|
execCalls = execCalls + 1;
|
|
return false, 'attempt to call a nil value (global "bad")', 'identifier';
|
|
end,
|
|
maxRetries = 2,
|
|
});
|
|
|
|
testlib.assertTrue(not ok);
|
|
testlib.assertEquals(result.errorKind, 'identifier');
|
|
testlib.assertEquals(result.attempts, 3);
|
|
testlib.assertTrue(result.retryExhausted);
|
|
testlib.assertEquals(execCalls, 3);
|
|
testlib.assertEquals(#httpStub.postCalls, 4);
|
|
end);
|
|
|
|
-- ping --
|
|
|
|
testlib.test('ping sends pong prompt', function()
|
|
local httpStub = fakeHttp(
|
|
{ messageResp('pong') },
|
|
{}
|
|
);
|
|
local settingsStub = fakeSettings({
|
|
['opencc.server_url'] = 'http://host',
|
|
['opencc.session_id'] = 'ses_1',
|
|
});
|
|
local ai = createAi({ http = httpStub, settings = settingsStub });
|
|
|
|
local ok, result = ai.ping();
|
|
|
|
testlib.assertTrue(ok, tostring(result));
|
|
testlib.assertEquals(result.reply, 'pong');
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(body.parts[1].text, 'reply with exactly: pong');
|
|
end);
|
|
|
|
-- clearSession --
|
|
|
|
testlib.test('clearSession unsets persisted session id', function()
|
|
local settingsStub = fakeSettings({ ['opencc.session_id'] = 'ses_old' });
|
|
local ai = createAi({ http = fakeHttp({}, {}), settings = settingsStub });
|
|
|
|
ai.clearSession();
|
|
|
|
testlib.assertEquals(settingsStub.values['opencc.session_id'], nil);
|
|
testlib.assertEquals(settingsStub.saveCount(), 1);
|
|
end);
|
|
|
|
testlib.test('clearSession unsets custom sessionSettingKey', function()
|
|
local settingsStub = fakeSettings({
|
|
['opencc.session_id'] = 'ses_old',
|
|
['trapgpt.opencc.session_id'] = 'ses_trapgpt',
|
|
});
|
|
local ai = createAi({ http = fakeHttp({}, {}), settings = settingsStub });
|
|
|
|
ai.clearSession({ sessionSettingKey = 'trapgpt.opencc.session_id' });
|
|
|
|
testlib.assertEquals(settingsStub.values['opencc.session_id'], 'ses_old');
|
|
testlib.assertEquals(settingsStub.values['trapgpt.opencc.session_id'], nil);
|
|
testlib.assertEquals(settingsStub.saveCount(), 1);
|
|
end);
|
|
|
|
testlib.run();
|