From 6d72410f559b128e67ba088142aa4dbd26403d3c Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 16 Aug 2026 01:39:17 +0530 Subject: [PATCH] feat(chrome): install the step's logs in the page Claude-Session: https://claude.ai/code/session_01ShuAy8q8ZfPi8KHxwc8JpQ --- internal/driver/chrome/driver.go | 24 ++++++++++++++ internal/driver/chrome/driver_test.go | 48 +++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/internal/driver/chrome/driver.go b/internal/driver/chrome/driver.go index db51c01..06914bd 100644 --- a/internal/driver/chrome/driver.go +++ b/internal/driver/chrome/driver.go @@ -887,6 +887,30 @@ func (d *Driver) SetLastAction(ctx context.Context, encoded json.RawMessage) err return nil } +// SetLogs installs the entries this step's log fetch returned as state.logs +// inside the page runtime. The page cannot derive them: console output reaches +// the driver over CDP and nothing in the page reads it back. Without this call +// every web state.logs is empty, and since the page's reading of an extractor +// replaces the host's, the default noLogcatErrors then reports green on a run +// whose console was full of errors. +// +// Unguarded for the same reason as SetLastAction: on a page with no setter, +// "the page cannot accept logs" has to fail the run rather than be reported as +// a successful install. +func (d *Driver) SetLogs(ctx context.Context, encoded json.RawMessage) error { + payload := strings.TrimSpace(string(encoded)) + if payload == "" { + payload = "[]" + } + script := fmt.Sprintf(`window.__sanderlingSetLogs__(%s)`, payload) + runCtx, cancel := d.runCtx(ctx) + defer cancel() + if err := chromedp.Run(runCtx, chromedp.Evaluate(script, nil)); err != nil { + return fmt.Errorf("set logs: %w", err) + } + return nil +} + // extractorScript resolves the extractor table once the page is not mid route // transition, giving up on that wait after %d ms. // diff --git a/internal/driver/chrome/driver_test.go b/internal/driver/chrome/driver_test.go index 36ef3fb..a0708a4 100644 --- a/internal/driver/chrome/driver_test.go +++ b/internal/driver/chrome/driver_test.go @@ -860,6 +860,54 @@ func TestSetLastAction_ReportsAPageThatCannotTakeIt(t *testing.T) { } } +// TestSetLogs_ReportsAPageThatCannotTakeThem is the same install on the channel +// the log properties hang off. The driver holding a console error changes +// nothing on web: the page's reading of every extractor replaces the host's, so +// unless the entries are put back into the page, noLogcatErrors counts an empty +// array and stays green through a run full of errors. +func TestSetLogs_ReportsAPageThatCannotTakeThem(t *testing.T) { + const withSetter = `` + const withoutSetter = `
no sanderling runtime here
` + pages := map[string]string{"/with": withSetter, "/without": withoutSetter} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + _, _ = w.Write([]byte(pages[r.URL.Path])) + })) + defer server.Close() + + d := New() + defer d.Terminate(context.Background()) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := d.Launch(ctx, server.URL+"/with", false, nil); err != nil { + t.Fatalf("Launch: %v", err) + } + logs := json.RawMessage(`[{"unixMillis":1,"level":"E","tag":"console","message":"boom"}]`) + if err := d.SetLogs(ctx, logs); err != nil { + t.Fatalf("SetLogs on a page that defines the setter: %v", err) + } + var seen []map[string]any + if err := chromedp.Run(d.tabCtx, + chromedp.Evaluate(`window.__logsSeen`, &seen)); err != nil { + t.Fatalf("read installed logs: %v", err) + } + if len(seen) != 1 || seen[0]["level"] != "E" || seen[0]["message"] != "boom" { + t.Errorf("the page received %v, want the error-level entry the driver captured", seen) + } + + if err := d.Launch(ctx, server.URL+"/without", false, nil); err != nil { + t.Fatalf("Launch: %v", err) + } + if err := d.SetLogs(ctx, logs); err == nil { + t.Error("SetLogs reported success on a page with no setter; " + + "a runtime that cannot take the step's logs is indistinguishable from one that did") + } +} + // TestEvaluateExtractors_ReportsAMissingTable is the same failure on the other // sampler. An empty override map is what a spec with no extractors returns, so // treating a missing table as {} makes "this page has no sanderling runtime"