summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-03 09:11:36 -0600
committert <t@tjp.lol>2026-06-03 12:34:13 -0600
commitac5c4898dfa0a9e57424336774893dfc72b132e9 (patch)
tree03eb8e32a001845dbaec163dfd4905ffc23d9fa6 /src
parent2416a308c0e467f8dbdbe09942aa903bca751c0e (diff)
image uploads
Diffstat (limited to 'src')
-rw-r--r--src/extension_loader.zig49
-rw-r--r--src/lua_bridge.zig151
-rw-r--r--src/lua_runtime.zig134
3 files changed, 242 insertions, 92 deletions
diff --git a/src/extension_loader.zig b/src/extension_loader.zig
index b84eba5..6d470cc 100644
--- a/src/extension_loader.zig
+++ b/src/extension_loader.zig
@@ -491,6 +491,27 @@ fn applyShadowing(allocator: Allocator, list: *std.array_list.Managed(Found)) !v
const testing = std.testing;
+/// Test helper: first text part of a tool result.
+fn xokText(result: panto.ToolCallResult) []const u8 {
+ switch (result) {
+ .ok => |parts| {
+ for (parts) |p| {
+ if (p == .text) return p.text;
+ }
+ return "";
+ },
+ .err => return "",
+ }
+}
+
+/// Test helper: free a results slice (parts on `.ok`).
+fn xfreeResults(results: []panto.ToolCallResult) void {
+ for (results) |r| switch (r) {
+ .ok => |b| panto.freeResultParts(testing.allocator, b),
+ .err => {},
+ };
+}
+
fn writeFile(dir: Io.Dir, sub_path: []const u8, content: []const u8) !void {
try dir.writeFile(testing.io, .{ .sub_path = sub_path, .data = content });
}
@@ -643,8 +664,8 @@ test "loadFromDirs: project shadows user end-to-end (via long-lived runtime)" {
const calls = [_]panto.ToolCall{.{ .tool_name = "greet", .input = "{}" }};
var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer testing.allocator.free(results[0].ok);
- try testing.expectEqualStrings("PROJECT", results[0].ok);
+ defer xfreeResults(&results);
+ try testing.expectEqualStrings("PROJECT", xokText(results[0]));
}
test "loadFromDirs: tool-name collision between extensions errors" {
@@ -711,8 +732,8 @@ test "loadFromDirs: tools/ directory — single-file tool form" {
const calls = [_]panto.ToolCall{.{ .tool_name = "echo", .input = "{\"msg\":\"hi\"}" }};
var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer testing.allocator.free(results[0].ok);
- try testing.expectEqualStrings("echo: hi", results[0].ok);
+ defer xfreeResults(&results);
+ try testing.expectEqualStrings("echo: hi", xokText(results[0]));
}
test "loadFromDirs: tools/ directory — directory-style tool with sibling require" {
@@ -751,8 +772,8 @@ test "loadFromDirs: tools/ directory — directory-style tool with sibling requi
const calls = [_]panto.ToolCall{.{ .tool_name = "shout", .input = "{\"text\":\"hi\"}" }};
var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer testing.allocator.free(results[0].ok);
- try testing.expectEqualStrings("HI!", results[0].ok);
+ defer xfreeResults(&results);
+ try testing.expectEqualStrings("HI!", xokText(results[0]));
}
test "loadFromDirs: project tool shadows user tool of the same name" {
@@ -799,8 +820,8 @@ test "loadFromDirs: project tool shadows user tool of the same name" {
const calls = [_]panto.ToolCall{.{ .tool_name = "greet", .input = "{}" }};
var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer testing.allocator.free(results[0].ok);
- try testing.expectEqualStrings("PROJECT", results[0].ok);
+ defer xfreeResults(&results);
+ try testing.expectEqualStrings("PROJECT", xokText(results[0]));
}
test "loadFromDirs: project tool shadows user shadows base" {
@@ -858,8 +879,8 @@ test "loadFromDirs: project tool shadows user shadows base" {
const calls = [_]panto.ToolCall{.{ .tool_name = "greet", .input = "{}" }};
var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer testing.allocator.free(results[0].ok);
- try testing.expectEqualStrings("PROJECT", results[0].ok);
+ defer xfreeResults(&results);
+ try testing.expectEqualStrings("PROJECT", xokText(results[0]));
}
test "loadFromDirs: user tool shadows base tool when no project entry" {
@@ -905,8 +926,8 @@ test "loadFromDirs: user tool shadows base tool when no project entry" {
const calls = [_]panto.ToolCall{.{ .tool_name = "greet", .input = "{}" }};
var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer testing.allocator.free(results[0].ok);
- try testing.expectEqualStrings("USER", results[0].ok);
+ defer xfreeResults(&results);
+ try testing.expectEqualStrings("USER", xokText(results[0]));
}
test "loadFromDirs: extension and tool share a *file* name independently" {
@@ -996,8 +1017,8 @@ test "loadFromDirs: tools policy removes a denied registered tool name" {
const calls = [_]panto.ToolCall{.{ .tool_name = "std.read", .input = "{}" }};
var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer testing.allocator.free(results[0].ok);
- try testing.expectEqualStrings("READ", results[0].ok);
+ defer xfreeResults(&results);
+ try testing.expectEqualStrings("READ", xokText(results[0]));
}
test "loadFromDirs: extensions policy denies a whole entry before it loads" {
diff --git a/src/lua_bridge.zig b/src/lua_bridge.zig
index cb57ade..245b0ec 100644
--- a/src/lua_bridge.zig
+++ b/src/lua_bridge.zig
@@ -36,6 +36,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
+const panto = @import("panto");
pub const c = @cImport({
@cInclude("lua.h");
@@ -260,20 +261,118 @@ pub fn pushJsonAsLua(
pushJsonValue(L, parsed.value) catch return BridgeError.OutOfMemory;
}
-/// Read a Lua value at `idx` and serialize it to a JSON-compatible owned
-/// byte string. Used to convert handler return values into ToolResult
-/// content. For now we only accept string returns; extending to richer
-/// types is straightforward but unnecessary for slice 2.
+/// Read a tool handler's return value at `idx` and convert it into an
+/// owned `[]panto.ResultPart`. Two accepted shapes:
+///
+/// * a plain string -> one `.text` part;
+/// * a table `{ text = "...", attachments = { { media_type = "...",
+/// data = "..." }, ... } }` -> an optional `.text` part followed by
+/// one `.media` part per attachment (`data` is base64-encoded bytes).
+///
+/// Every returned slice/part owns its bytes (allocated with `allocator`);
+/// the caller frees via `panto.freeResultParts`.
pub fn readHandlerResult(
L: *c.lua_State,
idx: c_int,
allocator: Allocator,
-) BridgeError![]u8 {
- if (c.lua_type(L, idx) != T_STRING) return BridgeError.BadHandlerReturn;
+) BridgeError![]panto.ResultPart {
+ const ty = c.lua_type(L, idx);
+ if (ty == T_STRING) {
+ var len: usize = 0;
+ const ptr = c.lua_tolstring(L, idx, &len);
+ if (ptr == null) return BridgeError.BadHandlerReturn;
+ return (panto.textResult(allocator, ptr[0..len]) catch
+ BridgeError.OutOfMemory);
+ }
+ if (ty != T_TABLE) return BridgeError.BadHandlerReturn;
+ return readHandlerResultTable(L, idx, allocator);
+}
+
+/// Read a string field `name` from the table at `tbl_idx`. Returns null
+/// if absent/nil, an error if present-but-not-a-string. The returned
+/// slice borrows from Lua's internal buffer — copy before popping.
+fn optStringField(L: *c.lua_State, tbl_idx: c_int, name: [*:0]const u8) BridgeError!?[]const u8 {
+ const t = c.lua_getfield(L, tbl_idx, name);
+ if (t == T_NIL) {
+ c.lua_settop(L, c.lua_gettop(L) - 1);
+ return null;
+ }
+ if (t != T_STRING) {
+ c.lua_settop(L, c.lua_gettop(L) - 1);
+ return BridgeError.BadHandlerReturn;
+ }
var len: usize = 0;
- const ptr = c.lua_tolstring(L, idx, &len);
- if (ptr == null) return BridgeError.BadHandlerReturn;
- return allocator.dupe(u8, ptr[0..len]) catch BridgeError.OutOfMemory;
+ const ptr = c.lua_tolstring(L, -1, &len);
+ if (ptr == null) {
+ c.lua_settop(L, c.lua_gettop(L) - 1);
+ return BridgeError.BadHandlerReturn;
+ }
+ // Note: value still on stack; caller pops after copying.
+ return ptr[0..len];
+}
+
+fn readHandlerResultTable(
+ L: *c.lua_State,
+ idx: c_int,
+ allocator: Allocator,
+) BridgeError![]panto.ResultPart {
+ var parts: std.ArrayList(panto.ResultPart) = .empty;
+ errdefer {
+ for (parts.items) |p| p.deinit(allocator);
+ parts.deinit(allocator);
+ }
+
+ // Optional `text` field.
+ {
+ const maybe = try optStringField(L, idx, "text");
+ if (maybe) |s| {
+ const owned = allocator.dupe(u8, s) catch return BridgeError.OutOfMemory;
+ c.lua_settop(L, c.lua_gettop(L) - 1); // pop field value
+ parts.append(allocator, .{ .text = owned }) catch {
+ allocator.free(owned);
+ return BridgeError.OutOfMemory;
+ };
+ }
+ }
+
+ // Optional `attachments` array.
+ {
+ const t = c.lua_getfield(L, idx, "attachments");
+ defer c.lua_settop(L, c.lua_gettop(L) - 1); // pop attachments
+ if (t == T_TABLE) {
+ const n: usize = @intCast(c.lua_rawlen(L, -1));
+ var i: usize = 1;
+ while (i <= n) : (i += 1) {
+ _ = c.lua_rawgeti(L, -1, @intCast(i)); // push attachment table
+ defer c.lua_settop(L, c.lua_gettop(L) - 1); // pop attachment
+ if (c.lua_type(L, -1) != T_TABLE) return BridgeError.BadHandlerReturn;
+
+ // `media_type` is an optional hint; libpanto detects the
+ // type from the raw bytes when it's absent.
+ const media_type: ?[]const u8 = if (try optStringField(L, -1, "media_type")) |mt_slice| blk: {
+ const owned = allocator.dupe(u8, mt_slice) catch return BridgeError.OutOfMemory;
+ c.lua_settop(L, c.lua_gettop(L) - 1); // pop media_type value
+ break :blk owned;
+ } else null;
+ errdefer if (media_type) |mt| allocator.free(mt);
+
+ const data_slice = (try optStringField(L, -1, "data")) orelse
+ return BridgeError.BadHandlerReturn;
+ const data = allocator.dupe(u8, data_slice) catch return BridgeError.OutOfMemory;
+ c.lua_settop(L, c.lua_gettop(L) - 1); // pop data value
+ errdefer allocator.free(data);
+
+ parts.append(allocator, .{ .media = .{
+ .media_type = media_type,
+ .data = data,
+ } }) catch return BridgeError.OutOfMemory;
+ }
+ } else if (t != T_NIL) {
+ return BridgeError.BadHandlerReturn;
+ }
+ }
+
+ return parts.toOwnedSlice(allocator) catch BridgeError.OutOfMemory;
}
// ---------------------------------------------------------------------------
@@ -697,8 +796,38 @@ test "handler invocation: input parsed, result captured" {
}
const result = try readHandlerResult(L, -1, std.testing.allocator);
- defer std.testing.allocator.free(result);
- try std.testing.expectEqualStrings("got: hello", result);
+ defer panto.freeResultParts(std.testing.allocator, result);
+ try std.testing.expectEqual(@as(usize, 1), result.len);
+ try std.testing.expectEqualStrings("got: hello", result[0].text);
+}
+
+test "readHandlerResult: table with text and attachments" {
+ const L = c.luaL_newstate() orelse return error.LuaInitFailed;
+ defer c.lua_close(L);
+ c.luaL_openlibs(L);
+ install(L);
+
+ const script =
+ \\return {
+ \\ text = "see image",
+ \\ attachments = {
+ \\ { media_type = "image/png", data = "AAA=" },
+ \\ { media_type = "application/pdf", data = "BBB=" },
+ \\ },
+ \\}
+ ;
+ if (c.luaL_loadstring(L, script) != 0 or c.lua_pcallk(L, 0, 1, 0, 0, null) != 0) {
+ return error.LuaScriptFailed;
+ }
+
+ const result = try readHandlerResult(L, -1, std.testing.allocator);
+ defer panto.freeResultParts(std.testing.allocator, result);
+ try std.testing.expectEqual(@as(usize, 3), result.len);
+ try std.testing.expectEqualStrings("see image", result[0].text);
+ try std.testing.expectEqualStrings("image/png", result[1].media.media_type.?);
+ try std.testing.expectEqualStrings("AAA=", result[1].media.data);
+ try std.testing.expectEqualStrings("application/pdf", result[2].media.media_type.?);
+ try std.testing.expectEqualStrings("BBB=", result[2].media.data);
}
test "handler crash: error message surfaces via xpcall traceback hook" {
diff --git a/src/lua_runtime.zig b/src/lua_runtime.zig
index 7d18fe9..73078c0 100644
--- a/src/lua_runtime.zig
+++ b/src/lua_runtime.zig
@@ -548,9 +548,9 @@ const Slot = struct {
/// `true` if the handler returned cleanly, `false` if it raised
/// via the `pcall` wrapping.
ok: bool = false,
- /// Result payload as owned bytes. Allocated from `allocator`.
- /// Caller frees.
- value: ?[]u8 = null,
+ /// Result payload as owned parts. Allocated from `allocator`.
+ /// Caller frees via `panto.freeResultParts`.
+ value: ?[]panto.ResultPart = null,
/// On `ok = false`, an owned copy of the error message.
err_msg: ?[]u8 = null,
};
@@ -703,11 +703,11 @@ fn runBatch(
continue;
}
if (slot.ok) {
- results[i] = .{ .ok = slot.value orelse try allocator.dupe(u8, "") };
+ results[i] = .{ .ok = slot.value orelse try panto.textResult(allocator, "") };
// Free the err_msg if both ended up set somehow.
if (slot.err_msg) |m| allocator.free(m);
} else {
- if (slot.value) |v| allocator.free(v);
+ if (slot.value) |v| panto.freeResultParts(allocator, v);
std.log.warn(
"panto-lua: tool '{s}' failed: {s}",
.{
@@ -734,12 +734,13 @@ fn formatToolError(
allocator: Allocator,
tool_name: []const u8,
message: []const u8,
-) ![]u8 {
- return std.fmt.allocPrint(
+) ![]panto.ResultPart {
+ const text = try std.fmt.allocPrint(
allocator,
"panto-lua: tool '{s}' failed: {s}",
.{ tool_name, message },
);
+ return panto.ownedTextResult(allocator, text);
}
/// Start one coroutine: create a thread under the runtime's lua_State,
@@ -815,7 +816,7 @@ fn runLegacySync(
};
var err_msg: ?[]u8 = null;
defer if (err_msg) |m| allocator.free(m);
- const out_bytes = invokeCoroutineSync(
+ const out_parts = invokeCoroutineSync(
self.L,
handler_ref,
call.input,
@@ -834,7 +835,7 @@ fn runLegacySync(
) catch return .{ .err = error.OutOfMemory };
return .{ .ok = bytes };
};
- return .{ .ok = out_bytes };
+ return .{ .ok = out_parts };
}
/// Run a handler synchronously, with no event loop. On Lua-level
@@ -847,7 +848,7 @@ fn invokeCoroutineSync(
input: []const u8,
allocator: Allocator,
err_msg_out: *?[]u8,
-) ![]u8 {
+) ![]panto.ResultPart {
const co = c.lua_newthread(L) orelse return RuntimeError.LuaInitFailed;
defer c.lua_settop(L, c.lua_gettop(L) - 1);
@@ -1058,6 +1059,29 @@ fn logTopAsError(L: *c.lua_State, prefix: []const u8) void {
const testing = std.testing;
+/// Test helper: concatenated text across a result's text parts.
+/// Returns the first text part's items (sufficient for current tests,
+/// which return a single text part).
+fn okText(result: panto.ToolCallResult) []const u8 {
+ switch (result) {
+ .ok => |parts| {
+ for (parts) |p| {
+ if (p == .text) return p.text;
+ }
+ return "";
+ },
+ .err => return "",
+ }
+}
+
+/// Test helper: free a results slice (parts on `.ok`).
+fn freeResults(results: []panto.ToolCallResult) void {
+ for (results) |r| switch (r) {
+ .ok => |b| panto.freeResultParts(testing.allocator, b),
+ .err => {},
+ };
+}
+
fn writeTempScript(dir: Io.Dir, name: []const u8, source: []const u8) ![]const u8 {
try dir.writeFile(testing.io, .{ .sub_path = name, .data = source });
var buf: [std.fs.max_path_bytes]u8 = undefined;
@@ -1204,14 +1228,11 @@ test "invokeBatch runs each call through a coroutine and returns the result" {
};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer for (results) |r| switch (r) {
- .ok => |b| testing.allocator.free(b),
- .err => {},
- };
+ defer freeResults(&results);
- try testing.expectEqualStrings("got: hello", results[0].ok);
- try testing.expectEqualStrings("HI!", results[1].ok);
- try testing.expectEqualStrings("got: again", results[2].ok);
+ try testing.expectEqualStrings("got: hello", okText(results[0]));
+ try testing.expectEqualStrings("HI!", okText(results[1]));
+ try testing.expectEqualStrings("got: again", okText(results[2]));
}
test "module-global state survives across calls in the same runtime" {
@@ -1250,14 +1271,11 @@ test "module-global state survives across calls in the same runtime" {
};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer for (results) |r| switch (r) {
- .ok => |b| testing.allocator.free(b),
- .err => {},
- };
+ defer freeResults(&results);
- try testing.expectEqualStrings("1", results[0].ok);
- try testing.expectEqualStrings("2", results[1].ok);
- try testing.expectEqualStrings("3", results[2].ok);
+ try testing.expectEqualStrings("1", okText(results[0]));
+ try testing.expectEqualStrings("2", okText(results[1]));
+ try testing.expectEqualStrings("3", okText(results[2]));
// And a second batch keeps the counter going.
var more: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }};
@@ -1267,8 +1285,8 @@ test "module-global state survives across calls in the same runtime" {
&more,
testing.allocator,
);
- defer testing.allocator.free(more[0].ok);
- try testing.expectEqualStrings("4", more[0].ok);
+ defer freeResults(&more);
+ try testing.expectEqualStrings("4", okText(more[0]));
}
test "handler crash: per-call error surfaces, sibling calls succeed" {
@@ -1306,12 +1324,9 @@ test "handler crash: per-call error surfaces, sibling calls succeed" {
.{ .err = error.SourceDroppedCall },
};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer for (results) |r| switch (r) {
- .ok => |b| testing.allocator.free(b),
- .err => {},
- };
+ defer freeResults(&results);
- try testing.expectEqualStrings("fine", results[0].ok);
+ try testing.expectEqualStrings("fine", okText(results[0]));
// A handler crash is surfaced as an *ok* result whose payload is
// the formatted error message — not as `.err` — because libpanto
// would otherwise abort the entire turn on per-call `.err`. The
@@ -1319,11 +1334,11 @@ test "handler crash: per-call error surfaces, sibling calls succeed" {
// prefix and includes the Lua-side error message.
try testing.expect(std.mem.startsWith(
u8,
- results[1].ok,
+ okText(results[1]),
"panto-lua: tool 'boom' failed:",
));
- try testing.expect(std.mem.indexOf(u8, results[1].ok, "kaboom") != null);
- try testing.expectEqualStrings("fine", results[2].ok);
+ try testing.expect(std.mem.indexOf(u8, okText(results[1]), "kaboom") != null);
+ try testing.expectEqualStrings("fine", okText(results[2]));
}
test "directory-style extension can require sibling modules" {
@@ -1369,8 +1384,8 @@ test "directory-style extension can require sibling modules" {
const calls = [_]panto.ToolCall{.{ .tool_name = "shout", .input = "{\"text\":\"hi\"}" }};
var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer testing.allocator.free(results[0].ok);
- try testing.expectEqualStrings("HI!", results[0].ok);
+ defer freeResults(&results);
+ try testing.expectEqualStrings("HI!", okText(results[0]));
}
test "yielding handler with no event loop surfaces LuaHandlerYielded" {
@@ -1395,10 +1410,7 @@ test "yielding handler with no event loop surfaces LuaHandlerYielded" {
const calls = [_]panto.ToolCall{.{ .tool_name = "sleeper", .input = "{}" }};
var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer for (results) |r| switch (r) {
- .ok => |b| testing.allocator.free(b),
- .err => {},
- };
+ defer freeResults(&results);
// Same policy as the crash test: the failure is surfaced as `.ok`
// text so libpanto doesn't abort the turn. The error type name
@@ -1406,10 +1418,10 @@ test "yielding handler with no event loop surfaces LuaHandlerYielded" {
// sync path's formatToolError call.
try testing.expect(std.mem.startsWith(
u8,
- results[0].ok,
+ okText(results[0]),
"panto-lua: tool 'sleeper' failed:",
));
- try testing.expect(std.mem.indexOf(u8, results[0].ok, "LuaHandlerYielded") != null);
+ try testing.expect(std.mem.indexOf(u8, okText(results[0]), "LuaHandlerYielded") != null);
}
// Integration test: requires a `$PANTO_HOME` with luv already
@@ -1488,13 +1500,10 @@ test "scheduler: yielding handler is resumed by libuv" {
.{ .err = error.SourceDroppedCall },
};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer for (results) |r| switch (r) {
- .ok => |b| testing.allocator.free(b),
- .err => {},
- };
+ defer freeResults(&results);
- try testing.expectEqualStrings("awake", results[0].ok);
- try testing.expectEqualStrings("awake", results[1].ok);
+ try testing.expectEqualStrings("awake", okText(results[0]));
+ try testing.expectEqualStrings("awake", okText(results[1]));
}
// Contract-violation guard: a handler that yields WITHOUT arming any
@@ -1537,14 +1546,11 @@ test "scheduler: handler that yields without arming libuv work is surfaced as an
.{ .err = error.SourceDroppedCall },
};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer for (results) |r| switch (r) {
- .ok => |b| testing.allocator.free(b),
- .err => {},
- };
+ defer freeResults(&results);
- try testing.expect(std.mem.startsWith(u8, results[0].ok, "panto-lua: tool 'abandon' failed:"));
- try testing.expect(std.mem.indexOf(u8, results[0].ok, "still suspended") != null);
- try testing.expectEqualStrings("ok", results[1].ok);
+ try testing.expect(std.mem.startsWith(u8, okText(results[0]), "panto-lua: tool 'abandon' failed:"));
+ try testing.expect(std.mem.indexOf(u8, okText(results[0]), "still suspended") != null);
+ try testing.expectEqualStrings("ok", okText(results[1]));
}
/// Absolute path to the repo's `agent/tools` directory, derived from
@@ -1618,13 +1624,10 @@ test "scheduler: two real std.shell calls in one batch do not deadlock" {
.{ .err = error.SourceDroppedCall },
};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer for (results) |r| switch (r) {
- .ok => |b| testing.allocator.free(b),
- .err => {},
- };
+ defer freeResults(&results);
- try testing.expect(std.mem.indexOf(u8, results[0].ok, "first-done") != null);
- try testing.expect(std.mem.indexOf(u8, results[1].ok, "second-done") != null);
+ try testing.expect(std.mem.indexOf(u8, okText(results[0]), "first-done") != null);
+ try testing.expect(std.mem.indexOf(u8, okText(results[1]), "second-done") != null);
}
// Reproduction: two REAL `std.read` calls in one batch. read.lua uses
@@ -1658,13 +1661,10 @@ test "scheduler: two real std.read calls in one batch do not deadlock" {
};
try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator);
- defer for (results) |r| switch (r) {
- .ok => |b| testing.allocator.free(b),
- .err => {},
- };
+ defer freeResults(&results);
- try testing.expect(results[0].ok.len > 0);
- try testing.expect(results[1].ok.len > 0);
+ try testing.expect(okText(results[0]).len > 0);
+ try testing.expect(okText(results[1]).len > 0);
}
test "loadExtension: duplicate tool name from a second extension errors" {