feat(ai): add lua exec command
This commit is contained in:
parent
58af683ca8
commit
f6d6c9b5af
271
apis/libai.lua
271
apis/libai.lua
@ -1,6 +1,8 @@
|
||||
local PING_PROMPT = 'reply with exactly: pong';
|
||||
|
||||
local DEFAULT_TIMEOUT_SECONDS = 1200;
|
||||
local DEFAULT_LUA_EXEC_MAX_RETRIES = 2;
|
||||
local DEFAULT_LUA_EXEC_TIMEOUT_SECONDS = 5;
|
||||
|
||||
local B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
|
||||
@ -54,6 +56,83 @@ local function extractTextParts(parts)
|
||||
return table.concat(texts, '');
|
||||
end
|
||||
|
||||
local function tablePack(...)
|
||||
return { n = select('#', ...), ... };
|
||||
end
|
||||
|
||||
local function endsWithNewline(s)
|
||||
return type(s) == 'string' and string.sub(s, -1) == '\n';
|
||||
end
|
||||
|
||||
local function valuesToLine(values, first, last)
|
||||
local parts = {};
|
||||
for i = first, last do
|
||||
parts[#parts + 1] = tostring(values[i]);
|
||||
end
|
||||
return table.concat(parts, '\t');
|
||||
end
|
||||
|
||||
local function classifyLuaRuntimeError(err)
|
||||
local text = tostring(err or '');
|
||||
if string.find(text, 'attempt to', 1, true) and string.find(text, 'nil value', 1, true) then
|
||||
return 'identifier';
|
||||
end
|
||||
if string.find(text, 'global', 1, true) and string.find(text, 'nil', 1, true) then
|
||||
return 'identifier';
|
||||
end
|
||||
return 'other';
|
||||
end
|
||||
|
||||
local function renderOutput(output)
|
||||
if output == nil or output == '' then
|
||||
return '(no output)';
|
||||
end
|
||||
return output;
|
||||
end
|
||||
|
||||
local function buildLuaExecPrompt(userPrompt)
|
||||
return table.concat({
|
||||
'Write ComputerCraft Lua code to answer this user request.',
|
||||
'Reply with raw Lua code only. Do not use markdown fences or explanations.',
|
||||
'The code runs locally with normal ComputerCraft globals available.',
|
||||
'Use print() or write() for values that should be sent back. Returned values are captured too.',
|
||||
'',
|
||||
'User request:',
|
||||
userPrompt,
|
||||
}, '\n');
|
||||
end
|
||||
|
||||
local function buildLuaCorrectionPrompt(userPrompt, code, err, errorKind)
|
||||
return table.concat({
|
||||
'The previous ComputerCraft Lua failed.',
|
||||
'Reply with corrected raw Lua code only. Do not use markdown fences or explanations.',
|
||||
'',
|
||||
'Original user request:',
|
||||
userPrompt,
|
||||
'',
|
||||
'Error kind: ' .. tostring(errorKind),
|
||||
'Error:',
|
||||
tostring(err),
|
||||
'',
|
||||
'Previous code:',
|
||||
code,
|
||||
}, '\n');
|
||||
end
|
||||
|
||||
local function buildLuaOutputPrompt(userPrompt, output)
|
||||
return table.concat({
|
||||
'The Lua executed successfully.',
|
||||
'Answer the original user request in natural language using the output below.',
|
||||
'Do not write more Lua unless the user explicitly asked for code.',
|
||||
'',
|
||||
'Original user request:',
|
||||
userPrompt,
|
||||
'',
|
||||
'Lua output:',
|
||||
renderOutput(output),
|
||||
}, '\n');
|
||||
end
|
||||
|
||||
local function sessionTime(session)
|
||||
if type(session) ~= 'table' or type(session.time) ~= 'table' then
|
||||
return 0;
|
||||
@ -77,6 +156,19 @@ local function createAi(opts)
|
||||
return DEFAULT_TIMEOUT_SECONDS;
|
||||
end
|
||||
|
||||
local function resolveLuaExecMaxRetries(options)
|
||||
local n = tonumber(options.maxRetries);
|
||||
if n and n >= 0 then return math.floor(n); end
|
||||
return DEFAULT_LUA_EXEC_MAX_RETRIES;
|
||||
end
|
||||
|
||||
local function resolveLuaExecTimeout(options)
|
||||
if options.luaTimeoutSeconds == false then return nil; end
|
||||
local n = tonumber(options.luaTimeoutSeconds);
|
||||
if n and n > 0 then return n; end
|
||||
return DEFAULT_LUA_EXEC_TIMEOUT_SECONDS;
|
||||
end
|
||||
|
||||
local function resolveConfig(options)
|
||||
local url = options.serverUrl or settingsLib.get('opencc.server_url');
|
||||
if not url or url == '' then
|
||||
@ -169,13 +261,14 @@ local function createAi(opts)
|
||||
local cfg, err = resolveConfig(options);
|
||||
if not cfg then return false, err; end
|
||||
|
||||
local persist = options.persist ~= false;
|
||||
local sessionId = options.sessionId;
|
||||
if sessionId == nil then
|
||||
if persist and sessionId == nil then
|
||||
sessionId = settingsLib.get('opencc.session_id');
|
||||
end
|
||||
|
||||
if not sessionId or sessionId == '' then
|
||||
local body, code = doPost(cfg, '/session', { title = 'cc-ai' });
|
||||
local body, code = doPost(cfg, '/session', { title = options.sessionTitle or '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);
|
||||
@ -185,8 +278,10 @@ local function createAi(opts)
|
||||
return false, 'reponse session invalide';
|
||||
end
|
||||
sessionId = decoded.id;
|
||||
settingsLib.set('opencc.session_id', sessionId);
|
||||
if settingsLib.save then settingsLib.save(); end
|
||||
if persist then
|
||||
settingsLib.set('opencc.session_id', sessionId);
|
||||
if settingsLib.save then settingsLib.save(); end
|
||||
end
|
||||
end
|
||||
|
||||
local body, code = doPost(cfg, '/session/' .. sessionId .. '/message', {
|
||||
@ -194,8 +289,10 @@ local function createAi(opts)
|
||||
});
|
||||
if not body then return false, code; end
|
||||
if code == 404 then
|
||||
settingsLib.unset('opencc.session_id');
|
||||
if settingsLib.save then settingsLib.save(); end
|
||||
if persist then
|
||||
settingsLib.unset('opencc.session_id');
|
||||
if settingsLib.save then settingsLib.save(); end
|
||||
end
|
||||
return false, 'session introuvable; lance: ai new <prompt>';
|
||||
end
|
||||
if code and code ~= 200 then
|
||||
@ -215,6 +312,168 @@ local function createAi(opts)
|
||||
return true, { reply = reply, sessionId = sessionId };
|
||||
end
|
||||
|
||||
function api.createLuaExecutor(options)
|
||||
options = options or {};
|
||||
local baseEnv = options.env or _G;
|
||||
local live = options.live ~= false;
|
||||
local livePrint = options.print or print;
|
||||
local liveWrite = options.write or write;
|
||||
local timeoutSeconds = resolveLuaExecTimeout(options);
|
||||
|
||||
return function(code)
|
||||
local buffer = {};
|
||||
|
||||
local function append(text)
|
||||
buffer[#buffer + 1] = text;
|
||||
end
|
||||
|
||||
local function capturedPrint(...)
|
||||
local values = tablePack(...);
|
||||
local line = valuesToLine(values, 1, values.n);
|
||||
append(line .. '\n');
|
||||
if live then livePrint(...); end
|
||||
end
|
||||
|
||||
local function capturedWrite(text)
|
||||
text = tostring(text or '');
|
||||
append(text);
|
||||
if live then liveWrite(text); end
|
||||
end
|
||||
|
||||
local env = setmetatable({
|
||||
print = capturedPrint,
|
||||
write = capturedWrite,
|
||||
}, { __index = baseEnv });
|
||||
local chunk, loadErr = load(code, 'ai-lua-exec', 't', env);
|
||||
if not chunk then
|
||||
return false, tostring(loadErr), 'syntax';
|
||||
end
|
||||
|
||||
local result;
|
||||
local finished = false;
|
||||
local function runner()
|
||||
result = tablePack(pcall(chunk));
|
||||
finished = true;
|
||||
end
|
||||
|
||||
if timeoutSeconds then
|
||||
parallel.waitForAny(runner, function() sleep(timeoutSeconds); end);
|
||||
else
|
||||
runner();
|
||||
end
|
||||
|
||||
if not finished then
|
||||
return false, 'lua execution timed out after ' .. tostring(timeoutSeconds) .. 's', 'other';
|
||||
end
|
||||
if not result[1] then
|
||||
return false, tostring(result[2]), classifyLuaRuntimeError(result[2]);
|
||||
end
|
||||
if result.n > 1 then
|
||||
if #buffer > 0 and not endsWithNewline(buffer[#buffer]) then
|
||||
append('\n');
|
||||
end
|
||||
append(valuesToLine(result, 2, result.n) .. '\n');
|
||||
end
|
||||
return true, table.concat(buffer), nil;
|
||||
end;
|
||||
end
|
||||
|
||||
function api.luaExec(userPrompt, options)
|
||||
options = options or {};
|
||||
if isBlank(userPrompt) then
|
||||
return false, { error = 'missing prompt; usage: ai lua-exec <prompt>', attempts = 0 };
|
||||
end
|
||||
|
||||
local log = options.log or function() end;
|
||||
local executor = options.executor or api.createLuaExecutor(options);
|
||||
local maxRetries = resolveLuaExecMaxRetries(options);
|
||||
local maxAttempts = maxRetries + 1;
|
||||
local sessionId;
|
||||
|
||||
local function askOptions()
|
||||
return {
|
||||
persist = false,
|
||||
sessionId = sessionId,
|
||||
sessionTitle = 'cc-ai lua-exec',
|
||||
serverUrl = options.serverUrl,
|
||||
username = options.username,
|
||||
password = options.password,
|
||||
timeoutSeconds = options.timeoutSeconds,
|
||||
};
|
||||
end
|
||||
|
||||
log('requesting Lua from AI');
|
||||
local ok, result = api.ask(buildLuaExecPrompt(userPrompt), askOptions());
|
||||
if not ok then
|
||||
return false, { error = result, attempts = 0, errorKind = 'ai' };
|
||||
end
|
||||
sessionId = result.sessionId;
|
||||
log('session: ' .. sessionId);
|
||||
|
||||
local code = result.reply;
|
||||
for attempt = 1, maxAttempts do
|
||||
log('attempt ' .. tostring(attempt) .. '/' .. tostring(maxAttempts));
|
||||
log('code:\n' .. code);
|
||||
|
||||
local execOk, outputOrErr, errorKind = executor(code);
|
||||
if execOk then
|
||||
local output = outputOrErr or '';
|
||||
log('output:\n' .. renderOutput(output));
|
||||
log('requesting final reply');
|
||||
local finalOk, finalResult = api.ask(buildLuaOutputPrompt(userPrompt, output), askOptions());
|
||||
if not finalOk then
|
||||
return false, {
|
||||
error = finalResult,
|
||||
attempts = attempt,
|
||||
errorKind = 'ai',
|
||||
code = code,
|
||||
output = output,
|
||||
sessionId = sessionId,
|
||||
};
|
||||
end
|
||||
log('final reply received');
|
||||
return true, {
|
||||
reply = finalResult.reply,
|
||||
output = output,
|
||||
code = code,
|
||||
attempts = attempt,
|
||||
sessionId = sessionId,
|
||||
};
|
||||
end
|
||||
|
||||
errorKind = errorKind or 'other';
|
||||
log('error (' .. tostring(errorKind) .. '):\n' .. tostring(outputOrErr));
|
||||
if (errorKind ~= 'syntax' and errorKind ~= 'identifier') or attempt >= maxAttempts then
|
||||
return false, {
|
||||
error = outputOrErr,
|
||||
attempts = attempt,
|
||||
errorKind = errorKind,
|
||||
code = code,
|
||||
sessionId = sessionId,
|
||||
retryExhausted = attempt >= maxAttempts,
|
||||
};
|
||||
end
|
||||
|
||||
log('requesting corrected Lua');
|
||||
local correctionOk, correctionResult = api.ask(
|
||||
buildLuaCorrectionPrompt(userPrompt, code, outputOrErr, errorKind),
|
||||
askOptions()
|
||||
);
|
||||
if not correctionOk then
|
||||
return false, {
|
||||
error = correctionResult,
|
||||
attempts = attempt,
|
||||
errorKind = 'ai',
|
||||
code = code,
|
||||
sessionId = sessionId,
|
||||
};
|
||||
end
|
||||
code = correctionResult.reply;
|
||||
end
|
||||
|
||||
return false, { error = 'lua-exec failed unexpectedly', attempts = maxAttempts };
|
||||
end
|
||||
|
||||
function api.ping(options)
|
||||
return api.ask(PING_PROMPT, options);
|
||||
end
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "TrapOS",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"branch": "next",
|
||||
"packages": [
|
||||
"trapos"
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
"trapos-boot": "0.2.2",
|
||||
"trapos-net": "0.2.1",
|
||||
"trapos-ui": "0.2.2",
|
||||
"trapos-ai": "0.4.2",
|
||||
"trapos": "0.6.0"
|
||||
"trapos-ai": "0.5.0",
|
||||
"trapos": "0.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trapos-ai",
|
||||
"version": "0.4.2",
|
||||
"version": "0.5.0",
|
||||
"description": "TrapOS AI client for opencode serve",
|
||||
"dependencies": ["trapos-core"],
|
||||
"files": [
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trapos",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.1",
|
||||
"description": "TrapOS full install meta-package",
|
||||
"dependencies": ["trapos-boot", "trapos-net", "trapos-ui", "trapos-test", "trapos-ai"],
|
||||
"files": [],
|
||||
|
||||
@ -10,6 +10,8 @@ local function printUsage()
|
||||
print(' ai ping');
|
||||
print(' ai new <prompt>');
|
||||
print(' ai --new <prompt>');
|
||||
print(' ai lua-exec <prompt>');
|
||||
print(' ai --lua-exec <prompt>');
|
||||
print(' ai sessions');
|
||||
print(' ai --sessions');
|
||||
print(' ai --version');
|
||||
@ -56,6 +58,43 @@ local function askAndPrint(ai, prompt)
|
||||
print(result.reply);
|
||||
end
|
||||
|
||||
local function printLuaExecLog(message)
|
||||
local text = tostring(message or '');
|
||||
if text == '' then
|
||||
print('[lua-exec]');
|
||||
return;
|
||||
end
|
||||
|
||||
local start = 1;
|
||||
while start <= #text do
|
||||
local newline = string.find(text, '\n', start, true);
|
||||
if not newline then
|
||||
print('[lua-exec] ' .. string.sub(text, start));
|
||||
return;
|
||||
end
|
||||
print('[lua-exec] ' .. string.sub(text, start, newline - 1));
|
||||
start = newline + 1;
|
||||
end
|
||||
end
|
||||
|
||||
local function luaExec(ai, prompt)
|
||||
local ok, result = ai.luaExec(prompt, {
|
||||
executor = ai.createLuaExecutor({ live = true }),
|
||||
log = printLuaExecLog,
|
||||
});
|
||||
if not ok then
|
||||
printLuaExecLog('failed after ' .. tostring(result.attempts or 0) .. ' attempt(s)');
|
||||
if result.errorKind then
|
||||
printLuaExecLog('error kind: ' .. tostring(result.errorKind));
|
||||
end
|
||||
printLuaExecLog(tostring(result.error));
|
||||
return;
|
||||
end
|
||||
|
||||
printLuaExecLog('final reply:');
|
||||
print(result.reply);
|
||||
end
|
||||
|
||||
local command = args[1];
|
||||
|
||||
if command == '--version' or command == '-version' or command == 'version' then
|
||||
@ -101,6 +140,16 @@ if command == 'new' or command == '--new' then
|
||||
return;
|
||||
end
|
||||
|
||||
if command == 'lua-exec' or command == '--lua-exec' then
|
||||
local prompt = joinArgs(2);
|
||||
if prompt == '' then
|
||||
printUsage();
|
||||
return;
|
||||
end
|
||||
luaExec(ai, prompt);
|
||||
return;
|
||||
end
|
||||
|
||||
if string.sub(command, 1, 1) == '-' then
|
||||
printUsage();
|
||||
return;
|
||||
|
||||
249
tests/ai.lua
249
tests/ai.lua
@ -5,13 +5,18 @@ local testlib = createLibTest({ ... });
|
||||
|
||||
local function fakeSettings(initial)
|
||||
local values = initial or {};
|
||||
local getCounts = {};
|
||||
local saveCount = 0;
|
||||
return {
|
||||
get = function(key) return values[key]; end,
|
||||
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
|
||||
@ -72,6 +77,11 @@ local function messageResp(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()
|
||||
@ -395,6 +405,48 @@ testlib.test('ask on HTTP error 404 clears session_id', function()
|
||||
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') },
|
||||
@ -408,6 +460,201 @@ testlib.test('ask omits Authorization header when no password', function()
|
||||
testlib.assertEquals(httpStub.postCalls[1].headers['Authorization'], nil);
|
||||
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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user