diff --git a/apis/libaihelloworld.lua b/apis/libai.lua similarity index 90% rename from apis/libaihelloworld.lua rename to apis/libai.lua index 88bed37..986dc35 100644 --- a/apis/libaihelloworld.lua +++ b/apis/libai.lua @@ -1,6 +1,6 @@ -local _VERSION = '0.2.2'; +local _VERSION = '0.3.0'; -local DEFAULT_PROMPT = 'reply with exactly: pong'; +local PING_PROMPT = 'reply with exactly: pong'; local B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; @@ -27,6 +27,10 @@ local function trimTrailingSlash(s) return (s:gsub('/+$', '')); end +local function isBlank(s) + return type(s) ~= 'string' or string.match(s, '^%s*$') ~= nil; +end + local function readAllAndClose(response) local body = response.readAll(); response.close(); @@ -50,12 +54,11 @@ local function extractTextParts(parts) return table.concat(texts, ''); end -local function createAiHelloWorld(opts) +local function createAi(opts) opts = opts or {}; local httpLib = opts.http or http; local settingsLib = opts.settings or settings; - local prompt = opts.prompt or DEFAULT_PROMPT; local api = {}; @@ -133,8 +136,12 @@ local function createAiHelloWorld(opts) return true, decoded; end - function api.askHello(options) + function api.ask(prompt, options) options = options or {}; + if isBlank(prompt) then + return false, 'missing prompt; usage: ai '; + end + local cfg, err = resolveConfig(options); if not cfg then return false, err; end @@ -144,7 +151,7 @@ local function createAiHelloWorld(opts) end if not sessionId or sessionId == '' then - local body, code = doPost(cfg, '/session', { title = 'cc-helloworld' }); + local body, code = doPost(cfg, '/session', { title = 'cc-ai' }); if not body then return false, code; end if code and code ~= 200 then return false, 'impossible de creer une session: HTTP ' .. tostring(code); @@ -165,7 +172,7 @@ local function createAiHelloWorld(opts) if code == 404 then settingsLib.unset('opencc.session_id'); if settingsLib.save then settingsLib.save(); end - return false, 'session introuvable; lance: ai-helloworld --new'; + return false, 'session introuvable; lance: ai new '; end if code and code ~= 200 then return false, 'erreur message: HTTP ' .. tostring(code); @@ -184,7 +191,11 @@ local function createAiHelloWorld(opts) return true, { reply = reply, sessionId = sessionId }; end + function api.ping(options) + return api.ask(PING_PROMPT, options); + end + return api; end -return createAiHelloWorld; +return createAi; diff --git a/docs/README.md b/docs/README.md index f2fdcb9..dd88f49 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,6 +8,8 @@ Start here when looking up ComputerCraft-related APIs, CraftOS-PC behavior, peri - [`craftos_pc_glossary.md`](craftos_pc_glossary.md) - CraftOS-PC emulator setup, CLI flags, mounting, peripheral emulation, troubleshooting, and related references. - [`advanced_peripherals_glossary.md`](advanced_peripherals_glossary.md) - Advanced Peripherals 0.7 guides, peripherals, turtles, integrations, and changelog pages. - [`create_cc_tweaked_glossary.md`](create_cc_tweaked_glossary.md) - Create CC:Tweaked integration pages. +- [`opencode_server_guide.md`](opencode_server_guide.md) - Running `opencode serve` for the TrapOS `ai` client. +- [`opencode_api.md`](opencode_api.md) - Minimal opencode HTTP API reference used by TrapOS. - [`adrs/`](adrs/) - Lightweight Architecture Decision Records for this repository. ## Notes diff --git a/docs/opencode_api.md b/docs/opencode_api.md index 1581eb2..07eb16e 100644 --- a/docs/opencode_api.md +++ b/docs/opencode_api.md @@ -1,6 +1,6 @@ # opencode serve — HTTP API reference -Minimal reference for the endpoints used by `libaihelloworld.lua`. Full spec served at `GET /doc` when the server is running. +Minimal reference for the endpoints used by `libai.lua`. Full spec served at `GET /doc` when the server is running. ## Running the server @@ -133,7 +133,7 @@ opencode attach http://127.0.0.1:4096 opencode attach http://127.0.0.1:4096 --session ses_abc123 ``` -To send messages to the session currently open in the TUI, set `opencc.session_id` in CC to the session ID shown in the TUI, then run `ai-helloworld`: +To send messages to the session currently open in the TUI, set `opencc.session_id` in CC to the session ID shown in the TUI, then run `ai`: ```sh set opencc.session_id ses_abc123 diff --git a/docs/opencode_server_guide.md b/docs/opencode_server_guide.md index 837df60..1986bce 100644 --- a/docs/opencode_server_guide.md +++ b/docs/opencode_server_guide.md @@ -1,6 +1,6 @@ # opencode server guide -How to run `opencode serve` and test `ai-helloworld` from ComputerCraft directly — no proxy. +How to run `opencode serve` and use `ai` from ComputerCraft directly, no proxy. See [`opencode_api.md`](opencode_api.md) for the full API reference. @@ -8,7 +8,7 @@ See [`opencode_api.md`](opencode_api.md) for the full API reference. ``` CC Computer - └─ ai-helloworld.lua (libaihelloworld.lua) + └─ ai.lua (libai.lua) └─ POST /session/:id/message → opencode serve ``` @@ -67,6 +67,12 @@ Use the CraftOS shell `set` program at the ComputerCraft console or CraftOS-PC t set opencc.server_url http://:4096 ``` +For example locally: + +```sh +set opencc.server_url http://127.0.0.1:4096 +``` + With auth: ```sh @@ -82,22 +88,23 @@ set opencc.username myuser - **CraftOS-PC (localhost):** `http://127.0.0.1:4096` - **In-game ATM10:** use your LAN IP (e.g. `192.168.x.x`) — add it to `http.rules` in `config/computercraft-server.toml` -## 4. Run `ai-helloworld` +## 4. Run `ai` ``` -ai-helloworld -- ping, reuses existing session -ai-helloworld --new -- forget current session, start fresh -ai-helloworld --sessions -- list all server sessions with their IDs -ai-helloworld --help -ai-helloworld --version +ai ping -- ping, reuses existing session +ai "explain what a turtle is" +ai new "start a fresh topic" -- forget current session, start fresh +ai sessions -- list all server sessions with their IDs +ai --help +ai --version ``` -Expected output on a working setup: `pong` (default prompt is `reply with exactly: pong`). +Expected `ai ping` output on a working setup: `pong`. ## 5. CraftOS-PC (no Minecraft) ```bash -just trapos --headless -- /programs/ai-helloworld.lua +just trapos --headless -- /programs/ai.lua ping ``` Set settings inside the harness before running, or inject them via the test API. @@ -109,6 +116,7 @@ Set settings inside the harness before running, or inject them via the test API. | `missing opencc.server_url` | Setting not set | `set opencc.server_url http://...` | | `serveur injoignable` | Server not running or wrong URL | Start `opencode serve`, check URL/port | | `erreur message: HTTP 401` | Wrong password | Check `opencc.password` matches `OPENCODE_SERVER_PASSWORD` | -| `session introuvable; lance: ai-helloworld --new` | Session was deleted or server restarted | Run `ai-helloworld --new` | +| `missing prompt` | No prompt was passed | Run `ai ` or `ai ping` | +| `session introuvable; lance: ai new ` | Session was deleted or server restarted | Run `ai new ` | | `erreur message: HTTP 504` | AI took too long | Retry; consider a faster model | | `reponse vide` | Reply had no text parts | Check opencode logs | diff --git a/packages/index.json b/packages/index.json index 0ef25a1..e629b0f 100644 --- a/packages/index.json +++ b/packages/index.json @@ -5,7 +5,7 @@ "tos-boot": "0.2.1", "tos-net": "0.2.0", "tos-ui": "0.2.0", - "tos-ai": "0.2.0", + "tos-ai": "0.3.0", "trapos": "0.5.1" } } diff --git a/packages/tos-ai/ccpm.json b/packages/tos-ai/ccpm.json index b23cc35..666d4df 100644 --- a/packages/tos-ai/ccpm.json +++ b/packages/tos-ai/ccpm.json @@ -1,11 +1,11 @@ { "name": "tos-ai", - "version": "0.2.0", - "description": "TrapOS AI hello-world client (opencode proxy)", + "version": "0.3.0", + "description": "TrapOS AI client for opencode serve", "dependencies": ["tos-core"], "files": [ - "apis/libaihelloworld.lua", - "programs/ai-helloworld.lua" + "apis/libai.lua", + "programs/ai.lua" ], "autostart": [] } diff --git a/programs/ai-helloworld.lua b/programs/ai-helloworld.lua deleted file mode 100644 index 963bc4c..0000000 --- a/programs/ai-helloworld.lua +++ /dev/null @@ -1,65 +0,0 @@ -local _VERSION = '0.2.0'; - -local createAiHelloWorld = require('/apis/libaihelloworld'); - -local args = table.pack(...); -local command = args[1]; - -local function printUsage() - print('ai-helloworld usage:'); - print(); - print(' ai-helloworld'); - print(' ai-helloworld --new'); - print(' ai-helloworld --sessions'); - print(' ai-helloworld --version'); - print(' ai-helloworld --help'); - print(); - print('settings required:'); - print(' opencc.server_url'); - print(); - print('settings optional:'); - print(' opencc.username (default: opencode)'); - print(' opencc.password (Basic Auth password)'); - print(' opencc.session_id (auto-managed)'); -end - -if command == '--version' or command == '-version' or command == 'version' then - print('ai-helloworld v' .. _VERSION); - return; -end - -if command == '--help' or command == '-help' or command == 'help' then - printUsage(); - return; -end - -local ai = createAiHelloWorld(); - -if command == '--new' then - ai.clearSession(); -elseif command == '--sessions' then - local ok, result = ai.listSessions(); - if not ok then - print(result); - return; - end - if #result == 0 then - print('no sessions'); - else - for _, s in ipairs(result) do - print((s.id or '?') .. ' ' .. (s.title or '(untitled)')); - end - end - return; -elseif command ~= nil and command ~= '' then - printUsage(); - return; -end - -local ok, result = ai.askHello(); -if not ok then - print(result); - return; -end - -print(result.reply); diff --git a/programs/ai.lua b/programs/ai.lua new file mode 100644 index 0000000..33a5fbd --- /dev/null +++ b/programs/ai.lua @@ -0,0 +1,110 @@ +local _VERSION = '0.3.0'; + +local createAi = require('/apis/libai'); + +local args = table.pack(...); + +local function printUsage() + print('ai usage:'); + print(); + print(' ai '); + print(' ai ping'); + print(' ai new '); + print(' ai --new '); + print(' ai sessions'); + print(' ai --sessions'); + print(' ai --version'); + print(' ai --help'); + print(); + print('settings required:'); + print(' opencc.server_url'); + print(); + print('settings optional:'); + print(' opencc.username (default: opencode)'); + print(' opencc.password (Basic Auth password)'); + print(' opencc.session_id (auto-managed)'); +end + +local function joinArgs(start) + local parts = {}; + for i = start, args.n do + parts[#parts + 1] = args[i]; + end + return table.concat(parts, ' '); +end + +local function printSessions(ai) + local ok, result = ai.listSessions(); + if not ok then + print(result); + return; + end + if #result == 0 then + print('no sessions'); + else + for _, s in ipairs(result) do + print((s.id or '?') .. ' ' .. (s.title or '(untitled)')); + end + end +end + +local function askAndPrint(ai, prompt) + local ok, result = ai.ask(prompt); + if not ok then + print(result); + return; + end + print(result.reply); +end + +local command = args[1]; + +if command == '--version' or command == '-version' or command == 'version' then + print('ai v' .. _VERSION); + return; +end + +if command == '--help' or command == '-help' or command == 'help' then + printUsage(); + return; +end + +if args.n == 0 then + printUsage(); + return; +end + +local ai = createAi(); + +if (command == 'sessions' or command == '--sessions') and args.n == 1 then + printSessions(ai); + return; +end + +if command == 'ping' and args.n == 1 then + local ok, result = ai.ping(); + if not ok then + print(result); + return; + end + print(result.reply); + return; +end + +if command == 'new' or command == '--new' then + local prompt = joinArgs(2); + if prompt == '' then + printUsage(); + return; + end + ai.clearSession(); + askAndPrint(ai, prompt); + return; +end + +if string.sub(command, 1, 1) == '-' then + printUsage(); + return; +end + +askAndPrint(ai, joinArgs(1)); diff --git a/tests/ai-helloworld.lua b/tests/ai.lua similarity index 64% rename from tests/ai-helloworld.lua rename to tests/ai.lua index 39cf95f..e114aa0 100644 --- a/tests/ai-helloworld.lua +++ b/tests/ai.lua @@ -1,5 +1,5 @@ local createLibTest = require('/apis/libtest'); -local createAiHelloWorld = require('/apis/libaihelloworld'); +local createAi = require('/apis/libai'); local testlib = createLibTest({ ... }); @@ -60,7 +60,7 @@ local function httpError(code, body) end local function sessionResp(id) - return response(200, textutils.serializeJSON({ id = id, title = 'cc-helloworld' })); + return response(200, textutils.serializeJSON({ id = id, title = 'cc-ai' })); end local function messageResp(reply) @@ -75,13 +75,13 @@ end 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') }, + { sessionResp('ses_1'), messageResp('ok') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' }); settingsStub.values['opencc.password'] = 'pass'; settingsStub.values['opencc.username'] = 'user'; - createAiHelloWorld({ http = httpStub, settings = settingsStub }).askHello(); + createAi({ http = httpStub, settings = settingsStub }).ask('hello'); local auth = httpStub.postCalls[1].headers['Authorization']; -- base64('user:pass') = 'dXNlcjpwYXNz' testlib.assertEquals(auth, 'Basic dXNlcjpwYXNz'); @@ -89,13 +89,13 @@ end); testlib.test('base64encode handles padding with one remainder byte', function() local httpStub = fakeHttp( - { sessionResp('ses_1'), messageResp('pong') }, + { sessionResp('ses_1'), messageResp('ok') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' }); settingsStub.values['opencc.password'] = 'x'; settingsStub.values['opencc.username'] = 'a'; - createAiHelloWorld({ http = httpStub, settings = settingsStub }).askHello(); + createAi({ http = httpStub, settings = settingsStub }).ask('hello'); -- base64('a:x') = 'YTp4' testlib.assertEquals(httpStub.postCalls[1].headers['Authorization'], 'Basic YTp4'); end); @@ -106,7 +106,7 @@ 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 ai = createAi({ http = httpStub, settings = settingsStub }); local ok, result = ai.listSessions(); @@ -117,7 +117,7 @@ end); testlib.test('listSessions fails when server_url missing', function() local httpStub = fakeHttp({}, {}); - local ai = createAiHelloWorld({ http = httpStub, settings = fakeSettings() }); + local ai = createAi({ http = httpStub, settings = fakeSettings() }); local ok, err = ai.listSessions(); @@ -131,7 +131,7 @@ 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 ai = createAi({ http = httpStub, settings = settingsStub }); local ok, err = ai.listSessions(); @@ -142,7 +142,7 @@ 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 ai = createAi({ http = httpStub, settings = settingsStub }); local ok, err = ai.listSessions(); @@ -150,70 +150,84 @@ testlib.test('listSessions maps HTTP error response codes', function() testlib.assertTrue(string.find(err, 'HTTP 401', 1, true) ~= nil); end); --- askHello -- +-- ask -- -testlib.test('askHello creates session then sends message when no session_id', function() +testlib.test('ask creates session then sends message when no session_id', function() local httpStub = fakeHttp( - { sessionResp('ses_new'), messageResp('pong') }, + { sessionResp('ses_new'), messageResp('reply') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host:4096' }); - local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub }); + local ai = createAi({ http = httpStub, settings = settingsStub }); - local ok, result = ai.askHello(); + local ok, result = ai.ask('hello'); testlib.assertTrue(ok, tostring(result)); - testlib.assertEquals(result.reply, 'pong'); + 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('askHello saves new session_id to settings', function() +testlib.test('ask creates cc-ai titled sessions', function() local httpStub = fakeHttp( - { sessionResp('ses_abc'), messageResp('pong') }, + { sessionResp('ses_new'), messageResp('reply') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' }); - local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub }); + local ai = createAi({ http = httpStub, settings = settingsStub }); - ai.askHello(); + 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('askHello reuses existing session_id without creating a new session', function() +testlib.test('ask reuses existing session_id without creating a new session', function() local httpStub = fakeHttp( - { messageResp('pong') }, + { messageResp('reply') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host', ['opencc.session_id'] = 'ses_existing', }); - local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub }); + local ai = createAi({ http = httpStub, settings = settingsStub }); - local ok = ai.askHello(); + 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('askHello sends correct message body', function() +testlib.test('ask sends exact prompt text', function() local httpStub = fakeHttp( - { messageResp('pong') }, + { messageResp('reply') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host', ['opencc.session_id'] = 'ses_1', }); - local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub, prompt = 'my prompt' }); + local ai = createAi({ http = httpStub, settings = settingsStub }); - ai.askHello(); + ai.ask('my prompt'); local body = textutils.unserializeJSON(httpStub.postCalls[1].body); testlib.assertEquals(#body.parts, 1); @@ -221,7 +235,33 @@ testlib.test('askHello sends correct message body', function() testlib.assertEquals(body.parts[1].text, 'my prompt'); end); -testlib.test('askHello concatenates multiple text parts', function() +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 = {}, @@ -238,19 +278,19 @@ testlib.test('askHello concatenates multiple text parts', function() ['opencc.server_url'] = 'http://host', ['opencc.session_id'] = 'ses_1', }); - local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub }); + local ai = createAi({ http = httpStub, settings = settingsStub }); - local ok, result = ai.askHello(); + local ok, result = ai.ask('hello'); testlib.assertTrue(ok, tostring(result)); testlib.assertEquals(result.reply, 'hello world'); end); -testlib.test('askHello fails with missing server_url', function() +testlib.test('ask fails with missing server_url', function() local httpStub = fakeHttp({}, {}); - local ai = createAiHelloWorld({ http = httpStub, settings = fakeSettings() }); + local ai = createAi({ http = httpStub, settings = fakeSettings() }); - local ok, err = ai.askHello(); + local ok, err = ai.ask('hello'); testlib.assertTrue(not ok); testlib.assertTrue(string.find(err, 'opencc.server_url', 1, true) ~= nil); @@ -259,57 +299,57 @@ testlib.test('askHello fails with missing server_url', function() testlib.assertEquals(#httpStub.postCalls, 0); end); -testlib.test('askHello fails when server unreachable on session create', function() +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 = createAiHelloWorld({ http = httpStub, settings = settingsStub }); + local ai = createAi({ http = httpStub, settings = settingsStub }); - local ok, err = ai.askHello(); + local ok, err = ai.ask('hello'); 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() +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 = createAiHelloWorld({ http = httpStub, settings = settingsStub }); + local ai = createAi({ http = httpStub, settings = settingsStub }); - local ok, err = ai.askHello(); + local ok, err = ai.ask('hello'); testlib.assertTrue(not ok); testlib.assertTrue(string.find(err, 'injoignable', 1, true) ~= nil); end); -testlib.test('askHello maps 401 on message send', function() +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 = createAiHelloWorld({ http = httpStub, settings = settingsStub }); + local ai = createAi({ http = httpStub, settings = settingsStub }); - local ok, err = ai.askHello(); + local ok, err = ai.ask('hello'); 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() +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 = createAiHelloWorld({ http = httpStub, settings = settingsStub }); + local ai = createAi({ http = httpStub, settings = settingsStub }); - local ok, err = ai.askHello(); + local ok, err = ai.ask('hello'); 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() +testlib.test('ask on 404 clears session_id and suggests ai new', function() local httpStub = fakeHttp( { response(404, '{}') }, {} @@ -318,16 +358,16 @@ testlib.test('askHello on 404 clears session_id and suggests --new', function() ['opencc.server_url'] = 'http://host', ['opencc.session_id'] = 'ses_stale', }); - local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub }); + local ai = createAi({ http = httpStub, settings = settingsStub }); - local ok, err = ai.askHello(); + local ok, err = ai.ask('hello'); testlib.assertTrue(not ok); - testlib.assertTrue(string.find(err, '--new', 1, true) ~= nil); + testlib.assertTrue(string.find(err, 'ai new', 1, true) ~= nil); testlib.assertEquals(settingsStub.values['opencc.session_id'], nil); end); -testlib.test('askHello on HTTP error 404 clears session_id', function() +testlib.test('ask on HTTP error 404 clears session_id', function() local httpStub = fakeHttp( { httpError(404, '{}') }, {} @@ -336,33 +376,54 @@ testlib.test('askHello on HTTP error 404 clears session_id', function() ['opencc.server_url'] = 'http://host', ['opencc.session_id'] = 'ses_stale', }); - local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub }); + local ai = createAi({ http = httpStub, settings = settingsStub }); - local ok, err = ai.askHello(); + local ok, err = ai.ask('hello'); testlib.assertTrue(not ok); - testlib.assertTrue(string.find(err, '--new', 1, true) ~= nil); + testlib.assertTrue(string.find(err, 'ai new', 1, true) ~= nil); testlib.assertEquals(settingsStub.values['opencc.session_id'], nil); end); -testlib.test('askHello omits Authorization header when no password', function() +testlib.test('ask omits Authorization header when no password', function() local httpStub = fakeHttp( - { sessionResp('ses_1'), messageResp('pong') }, + { sessionResp('ses_1'), messageResp('reply') }, {} ); local settingsStub = fakeSettings({ ['opencc.server_url'] = 'http://host' }); - local ai = createAiHelloWorld({ http = httpStub, settings = settingsStub }); + local ai = createAi({ http = httpStub, settings = settingsStub }); - ai.askHello(); + 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 = createAiHelloWorld({ http = fakeHttp({}, {}), settings = settingsStub }); + local ai = createAi({ http = fakeHttp({}, {}), settings = settingsStub }); ai.clearSession();