Merge pull request #3024 from lightpanda-io/agent-console-sink

script: route console output through an optional sink
This commit is contained in:
Karl Seguin
2026-07-23 07:33:08 +08:00
committed by GitHub

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());
}