435 lines
14 KiB
Lua
435 lines
14 KiB
Lua
local createLibTest = require('/apis/libtest');
|
|
local createAi = require('/apis/libai');
|
|
|
|
local testlib = createLibTest({ ... });
|
|
|
|
local function fakeSettings(initial)
|
|
local values = initial or {};
|
|
local saveCount = 0;
|
|
return {
|
|
get = function(key) 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,
|
|
saveCount = function() return saveCount; 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(url, body, headers)
|
|
postCalls[#postCalls + 1] = { url = url, body = body, headers = headers };
|
|
postIdx = postIdx + 1;
|
|
local r = postResults[postIdx];
|
|
if type(r) == 'function' then return r(url, body, headers); end
|
|
return r;
|
|
end,
|
|
get = function(url, headers)
|
|
getCalls[#getCalls + 1] = { url = url, headers = headers };
|
|
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
|
|
|
|
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 = {},
|
|
parts = { { type = 'text', text = reply } },
|
|
}));
|
|
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 parsed session list', function()
|
|
local sessions = { { id = 'ses_1', title = 'hello' }, { id = 'ses_2', title = 'world' } };
|
|
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, 2);
|
|
testlib.assertEquals(result[1].id, 'ses_1');
|
|
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 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 sends exact prompt text', 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('my prompt');
|
|
|
|
local body = textutils.unserializeJSON(httpStub.postCalls[1].body);
|
|
testlib.assertEquals(#body.parts, 1);
|
|
testlib.assertEquals(body.parts[1].type, 'text');
|
|
testlib.assertEquals(body.parts[1].text, 'my prompt');
|
|
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 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);
|
|
|
|
-- 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.run();
|