From 226f5805aa4de5319b9a05d1a6bfff378c4d05cc Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 16 Aug 2026 01:37:52 +0530 Subject: [PATCH] feat(spec): let the runner install state.logs in the page Claude-Session: https://claude.ai/code/session_01ShuAy8q8ZfPi8KHxwc8JpQ --- pkg/spec/src/web-runtime.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pkg/spec/src/web-runtime.ts b/pkg/spec/src/web-runtime.ts index a607758..c28f4aa 100644 --- a/pkg/spec/src/web-runtime.ts +++ b/pkg/spec/src/web-runtime.ts @@ -445,6 +445,15 @@ if (typeof globalThis.addEventListener === "function") { // that reads state.lastAction vacuously true on web. let lastAction: unknown = null; +// logs is what the driver captured between the previous step and this one, +// pushed in by the Go runner (via __sanderlingSetLogs__) before each extractor +// evaluation, in the shape internal/verifier/marshal.go builds for goja. The +// page cannot derive it: console output reaches the runner over CDP and nothing +// in the page reads it back. Hardcoding [] here, as this file used to, makes +// every spec property that reads state.logs vacuously true on web, the default +// noLogcatErrors included, because the page's reading is the one that wins. +let logs: unknown[] = []; + function buildState(): unknown { return { snapshots: {}, @@ -453,7 +462,7 @@ function buildState(): unknown { window, lastAction, time: 0, - logs: [], + logs, exceptions: capturedExceptions.slice(), }; } @@ -502,6 +511,11 @@ defineLockedGlobal("__sanderlingSetLastAction__", (value: unknown) => { lastAction = value ?? null; }); +// The host calls this once per step too, alongside __sanderlingSetLastAction__. +defineLockedGlobal("__sanderlingSetLogs__", (value: unknown) => { + logs = Array.isArray(value) ? value : []; +}); + // writable:false stops a page script from shadowing the runtime via plain // assignment (the realistic in-page threat). configurable:true is required so // unit tests sharing one process can reinstall a fake via defineProperty; a