script: route console output through an optional sink

Since the Zig 0.16 merge, console.log and completion echoes from the
agent runtime tests reach the real stdout (0.15 silently swallowed
them), polluting the bench.json that CI captures from 'zig build test'
and breaking the perf-fmt job's JSON decoding.

Add Runtime.console_sink: when set, console lines write there instead
of stdout/stderr. The three printing tests now capture their output
and assert it, covering console argument joining and the top-level
return echo.
This commit is contained in:
Adrià Arrufat
2026-07-22 17:00:40 +02:00
parent 12fa469214
commit e1202b7660

View File

@@ -42,6 +42,8 @@ console_data: [std.enums.values(ConsoleMethod).len]ConsoleData,
/// clear the live spinner so script output starts on a clean line instead
/// of colliding with the indicator; the line still goes to stdout/stderr.
console_observer: ?ConsoleObserver = null,
/// When set, every console line writes here — stderr-bound methods included.
console_sink: ?*std.Io.Writer = null,
/// In-flight async `goto`s; emptied before each `runSource` returns.
pending_gotos: std.ArrayList(PendingGoto),
/// Restarted per `runSource`; backs `PendingGoto.deadline_ms`.
@@ -685,6 +687,10 @@ fn invokeConsole(self: *Runtime, method: ConsoleMethod, info: *const v8.Function
fn writeConsoleLine(self: *Runtime, method: ConsoleMethod, line: []const u8) void {
if (self.console_observer) |obs| obs.notify(obs.context);
if (self.console_sink) |sink| {
sink.print("{s}\n", .{line}) catch {};
return;
}
var buf: [4096]u8 = undefined;
const file = if (method.writesStderr()) std.Io.File.stderr() else std.Io.File.stdout();
var writer = file.writerStreaming(lp.io, &buf);
@@ -1329,6 +1335,10 @@ test "agent script runtime: primitives re-entered from argument callbacks stay i
const runtime = try Runtime.init(testing.allocator, testing.test_app, testing.test_session, &registry);
defer runtime.deinit();
var sink: std.Io.Writer.Allocating = .init(testing.allocator);
defer sink.deinit();
runtime.console_sink = &sink.writer;
try runTestScript(runtime,
\\const page = new Page();
\\await page.goto("http://localhost:9582/src/browser/tests/mcp_actions.html");
@@ -1340,6 +1350,7 @@ test "agent script runtime: primitives re-entered from argument callbacks stay i
\\console.log("value", { toString() { probed += 1; return page.evaluate("'ok'"); } }, "tail");
\\if (probed !== 1) throw new Error("console toString re-entry not exercised");
);
try testing.expectString("value ok tail\n", sink.written());
}
test "agent script runtime: terminate interrupts local JavaScript" {
@@ -1414,11 +1425,16 @@ test "agent script runtime: console is available in agent context" {
const runtime = try Runtime.init(testing.allocator, testing.test_app, testing.test_session, &registry);
defer runtime.deinit();
var sink: std.Io.Writer.Allocating = .init(testing.allocator);
defer sink.deinit();
runtime.console_sink = &sink.writer;
try runTestScript(runtime,
\\if (typeof console !== "object") throw new Error("missing console");
\\if (typeof console.log !== "function") throw new Error("missing console.log");
\\console.log("agent console ready");
);
try testing.expectString("agent console ready\n", sink.written());
}
test "agent script runtime: tool errors throw and stop execution" {
@@ -1516,6 +1532,10 @@ test "agent script runtime: top-level await runs in an async wrapper" {
const runtime = try Runtime.init(testing.allocator, testing.test_app, testing.test_session, &registry);
defer runtime.deinit();
var sink: std.Io.Writer.Allocating = .init(testing.allocator);
defer sink.deinit();
runtime.console_sink = &sink.writer;
// `await` on a non-goto promise resolves without touching the browser, and
// a top-level `return` surfaces as the (otherwise un-echoed) result.
try runTestScript(runtime,
@@ -1523,4 +1543,5 @@ test "agent script runtime: top-level await runs in an async wrapper" {
\\if (x + 2 !== 42) throw new Error("top-level await did not resolve: " + x);
\\return x + 2;
);
try testing.expectString("42\n", sink.written());
}