local createLibTest = require('/apis/libtest'); local createAiHelloWorld = require('/apis/libaihelloworld'); 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-helloworld' })); 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('pong') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' }); settingsStub.values['opencc.password'] = 'pass'; settingsStub.values['opencc.username'] = 'user'; createAiHelloWorld({ http = httpStub, settings = settingsStub }).askHello(); 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('pong') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' }); settingsStub.values['opencc.password'] = 'x'; settingsStub.values['opencc.username'] = 'a'; createAiHelloWorld({ http = httpStub, settings = settingsStub }).askHello(); -- 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 = createAiHelloWorld({ 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 = createAiHelloWorld({ 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.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 = createAiHelloWorld({ 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 = createAiHelloWorld({ http = httpStub, settings = settingsStub }); local ok, err = ai.listSessions(); testlib.assertTrue(not ok); testlib.assertTrue(string.find(err, 'HTTP 401', 1, true) ~= nil); end); -- askHello -- testlib.test('askHello creates session then sends message when no session_id', function() local httpStub = fakeHttp( { sessionResp('ses_new'), messageResp('pong') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host:4096' }); local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub }); local ok, result = ai.askHello(); testlib.assertTrue(ok, tostring(result)); testlib.assertEquals(result.reply, 'pong'); 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('askHello saves new session_id to settings', function() local httpStub = fakeHttp( { sessionResp('ses_abc'), messageResp('pong') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' }); local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub }); ai.askHello(); testlib.assertEquals(settingsStub.values['opencc.session_id'], 'ses_abc'); testlib.assertEquals(settingsStub.saveCount(), 1); end); testlib.test('askHello reuses existing session_id without creating a new session', function() local httpStub = fakeHttp( { messageResp('pong') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host', ['opencc.session_id'] = 'ses_existing', }); local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub }); local ok = ai.askHello(); 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('askHello sends correct message body', function() local httpStub = fakeHttp( { messageResp('pong') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host', ['opencc.session_id'] = 'ses_1', }); local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub, prompt = 'my prompt' }); ai.askHello(); 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('askHello 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 = createAiHelloWorld({ http = httpStub, settings = settingsStub }); local ok, result = ai.askHello(); testlib.assertTrue(ok, tostring(result)); testlib.assertEquals(result.reply, 'hello world'); end); testlib.test('askHello fails with missing server_url', function() local httpStub = fakeHttp({}, {}); local ai = createAiHelloWorld({ http = httpStub, settings = fakeSettings() }); local ok, err = ai.askHello(); testlib.assertTrue(not ok); testlib.assertTrue(string.find(err, 'opencc.server_url', 1, true) ~= nil); testlib.assertEquals(#httpStub.postCalls, 0); end); testlib.test('askHello fails when server unreachable on session create', function() local httpStub = fakeHttp({ nil }, {}); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' }); local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub }); local ok, err = ai.askHello(); testlib.assertTrue(not ok); testlib.assertTrue(string.find(err, 'injoignable', 1, true) ~= nil); end); testlib.test('askHello 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 = createAiHelloWorld({ http = httpStub, settings = settingsStub }); local ok, err = ai.askHello(); testlib.assertTrue(not ok); testlib.assertTrue(string.find(err, 'injoignable', 1, true) ~= nil); end); testlib.test('askHello maps 401 on message send', function() local httpStub = fakeHttp( { sessionResp('ses_1'), response(401, '{}') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' }); local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub }); local ok, err = ai.askHello(); testlib.assertTrue(not ok); testlib.assertTrue(string.find(err, 'HTTP 401', 1, true) ~= nil); end); testlib.test('askHello 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 = createAiHelloWorld({ http = httpStub, settings = settingsStub }); local ok, err = ai.askHello(); testlib.assertTrue(not ok); testlib.assertTrue(string.find(err, 'HTTP 401', 1, true) ~= nil); end); testlib.test('askHello on 404 clears session_id and suggests --new', function() local httpStub = fakeHttp( { response(404, '{}') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host', ['opencc.session_id'] = 'ses_stale', }); local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub }); local ok, err = ai.askHello(); testlib.assertTrue(not ok); testlib.assertTrue(string.find(err, '--new', 1, true) ~= nil); testlib.assertEquals(settingsStub.values['opencc.session_id'], nil); end); testlib.test('askHello 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 = createAiHelloWorld({ http = httpStub, settings = settingsStub }); local ok, err = ai.askHello(); testlib.assertTrue(not ok); testlib.assertTrue(string.find(err, '--new', 1, true) ~= nil); testlib.assertEquals(settingsStub.values['opencc.session_id'], nil); end); testlib.test('askHello omits Authorization header when no password', function() local httpStub = fakeHttp( { sessionResp('ses_1'), messageResp('pong') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' }); local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub }); ai.askHello(); testlib.assertEquals(httpStub.postCalls[1].headers['Authorization'], nil); end); -- clearSession -- testlib.test('clearSession unsets persisted session id', function() local settingsStub = fakeSettings({ ['opencc.session_id'] = 'ses_old' }); local ai = createAiHelloWorld({ http = fakeHttp({}, {}), settings = settingsStub }); ai.clearSession(); testlib.assertEquals(settingsStub.values['opencc.session_id'], nil); testlib.assertEquals(settingsStub.saveCount(), 1); end); testlib.run();