From cbf2c313ec749ef51f7e2ce67adcef1c89db8fce Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 16 Aug 2026 01:42:16 +0530 Subject: [PATCH] test(runner): cover the logs reaching the page and failing to Claude-Session: https://claude.ai/code/session_01ShuAy8q8ZfPi8KHxwc8JpQ --- internal/runner/web_carrier_test.go | 46 +++++++++++++++++++++++++ internal/runner/web_last_action_test.go | 37 ++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/internal/runner/web_carrier_test.go b/internal/runner/web_carrier_test.go index 96b5f9d..e47a8fb 100644 --- a/internal/runner/web_carrier_test.go +++ b/internal/runner/web_carrier_test.go @@ -204,3 +204,49 @@ func TestRunner_LastActionInstallFailureFailsTheRun(t *testing.T) { t.Errorf("Run error = %v, want it to name the failed lastAction install", err) } } + +// logInstallFailsWebDriver takes lastAction and refuses the logs, the shape a +// page carrying an older published @sanderling/spec runtime has: it knows the +// action setter and not the log one. +type logInstallFailsWebDriver struct { + *installFailsWebDriver +} + +func (d *logInstallFailsWebDriver) SetLastAction(context.Context, json.RawMessage) error { + return nil +} + +func (d *logInstallFailsWebDriver) SetLogs(context.Context, json.RawMessage) error { + return errors.New("__sanderlingSetLogs__ is not a function") +} + +// TestRunner_LogInstallFailureFailsTheRun holds the log channel to the same +// standard as the action one. The driver having the console errors decides +// nothing on web: the page's reading of every extractor replaces the host's, so +// a run that cannot put the entries back into the page evaluates noLogcatErrors +// against an empty array and reports green on a console full of errors. +// Continuing past this is the vacuity the whole install exists to prevent. +func TestRunner_LogInstallFailureFailsTheRun(t *testing.T) { + state := newHarnessWithSpec(t, carrierSpec) + web := &logInstallFailsWebDriver{ + installFailsWebDriver: &installFailsWebDriver{Driver: state.mock}, + } + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _, err := Run(ctx, Options{ + Duration: 2 * time.Second, + IdleTimeout: 20 * time.Millisecond, + MaxSteps: 3, + Driver: web, + Verifier: state.verifier, + TraceWriter: state.writer, + }) + if err == nil { + t.Fatal("Run succeeded with a page that cannot take the step's logs; " + + "every property reading the log stream ran against an empty array") + } + if !bytes.Contains([]byte(err.Error()), []byte("install logs")) { + t.Errorf("Run error = %v, want it to name the failed log install", err) + } +} diff --git a/internal/runner/web_last_action_test.go b/internal/runner/web_last_action_test.go index a50c706..34a247c 100644 --- a/internal/runner/web_last_action_test.go +++ b/internal/runner/web_last_action_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/priyanshujain/sanderling/internal/driver" mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock" ) @@ -84,6 +85,42 @@ func TestRunner_WebInstallsLastActionInThePage(t *testing.T) { } } +// The same hole on the other channel: state.logs was hardcoded [] in +// pkg/spec/src/web-runtime.ts, and because the page's reading of an extractor +// replaces the host's on web, the driver's error-level entries never reached a +// property. The default noLogcatErrors counted an empty array on every run. +func TestRunner_WebInstallsTheStepsLogsInThePage(t *testing.T) { + state := newHarnessWithSpec(t, lastActionSpec) + state.mock.LogEntries = []driver.LogEntry{ + {UnixMillis: 1700000000123, Level: "E", Tag: "console", Message: "boom from the page"}, + } + web := &tappingWebDriver{Driver: state.mock} + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if _, err := Run(ctx, Options{ + Duration: time.Hour, + IdleTimeout: 20 * time.Millisecond, + MaxSteps: 2, + Driver: web, + Verifier: state.verifier, + TraceWriter: state.writer, + }); err != nil { + t.Fatalf("Run: %v", err) + } + + if len(web.installedLogs) == 0 { + t.Fatal("the page was never handed the step's logs; every property reading " + + "state.logs evaluated against the empty array the page starts with") + } + // The shape is the goja host's (internal/verifier/marshal.go logFields), + // pinned against it by TestLogs_WebJSONMatchesTheGojaObject. + const want = `[{"unixMillis":1700000000123,"level":"E","tag":"console","message":"boom from the page"}]` + if web.installedLogs[0] != want { + t.Errorf("step 1 installed %s, want %s", web.installedLogs[0], want) + } +} + // failingTapWebDriver dispatches the tap and then fails the call, the shape an // RPC deadline takes: the page has the click, the runner has an error. type failingTapWebDriver struct {