diff --git a/src/script/Runtime.zig b/src/script/Runtime.zig index 606da9862..79948ad83 100644 --- a/src/script/Runtime.zig +++ b/src/script/Runtime.zig @@ -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, ®istry); 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, ®istry); 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, ®istry); 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()); }