fix(runner): install the step's logs before the page extracts

On web every extractor reading is replaced by the one the page computed,
and the page answered logs: [], so noLogcatErrors counted an empty array
however full of errors the console was.

Claude-Session: https://claude.ai/code/session_01ShuAy8q8ZfPi8KHxwc8JpQ
This commit is contained in:
pj committed 2026-08-16 01:41:10 +05:30
1 parent 3dfd5df511
commit 78d57bf94f
2 files changed
+35 -14

No files matched your search

+4 -3
View File
@@ -222,9 +222,10 @@ func Run(ctx context.Context, options Options) (Summary, error) {
// behind the hierarchy fetch; the fetch is what decides whether this // behind the hierarchy fetch; the fetch is what decides whether this
// step counts at all, so it has to go first. // step counts at all, so it has to go first.
// //
// lastAction is the same value PushSnapshot hands the goja state // lastAction and logs are the same values PushSnapshot hands the
// below: the two engines evaluate this step against one action. // goja state below: the two engines evaluate this step against one
v8Overrides, overridesErr := extractorSource.ExtractorOverrides(ctx, lastAction) // action and one set of log entries.
v8Overrides, overridesErr := extractorSource.ExtractorOverrides(ctx, lastAction, logs)
if overridesErr != nil { if overridesErr != nil {
// Not a warning. Without the page's values this step's // Not a warning. Without the page's values this step's
// extractors keep goja's dump-derived readings while the // extractors keep goja's dump-derived readings while the
+31 -11
View File
@@ -24,14 +24,15 @@ type ActionSource interface {
// PushSnapshot. The mobile path has none (returns nil); the web path returns the // PushSnapshot. The mobile path has none (returns nil); the web path returns the
// values its extractors computed in V8 against the real DOM. // values its extractors computed in V8 against the real DOM.
// //
// lastAction is the action the previous step actually applied, the same value // lastAction and logs are what PushSnapshot hands the goja state. The web path
// PushSnapshot hands the goja state. The web path has to install it in the page // has to install both in the page before its extractors run: a spec extractor
// before its extractors run: a spec extractor reading state.lastAction runs in // reading state.lastAction or state.logs runs in V8 there, and V8 knows neither
// V8 there, and V8 has no way to know what the runner dispatched. // what the runner dispatched nor what the driver's log fetch returned.
type ExtractorSource interface { type ExtractorSource interface {
ExtractorOverrides( ExtractorOverrides(
ctx context.Context, ctx context.Context,
lastAction *verifier.Action, lastAction *verifier.Action,
logs []verifier.LogEntry,
) (map[int]json.RawMessage, error) ) (map[int]json.RawMessage, error)
} }
@@ -42,6 +43,13 @@ type lastActionInstaller interface {
SetLastAction(ctx context.Context, encoded json.RawMessage) error SetLastAction(ctx context.Context, encoded json.RawMessage) error
} }
// logInstaller is the same channel for the entries this step's log fetch
// returned. Console output reaches the driver over CDP, so the page can only
// learn about it from the runner.
type logInstaller interface {
SetLogs(ctx context.Context, encoded json.RawMessage) error
}
// gojaSource drives both action selection and (trivially) extractor overrides // gojaSource drives both action selection and (trivially) extractor overrides
// for the mobile path, where the goja-bundled picker runs in-process and no V8 // for the mobile path, where the goja-bundled picker runs in-process and no V8
// extractor values exist. // extractor values exist.
@@ -56,6 +64,7 @@ func (s gojaSource) NextAction(context.Context) (verifier.Action, error) {
func (gojaSource) ExtractorOverrides( func (gojaSource) ExtractorOverrides(
context.Context, context.Context,
*verifier.Action, *verifier.Action,
[]verifier.LogEntry,
) (map[int]json.RawMessage, error) { ) (map[int]json.RawMessage, error) {
return nil, nil return nil, nil
} }
@@ -77,24 +86,35 @@ func (s webSource) NextAction(ctx context.Context) (verifier.Action, error) {
return verifier.DecodeAction(raw) return verifier.DecodeAction(raw)
} }
// ExtractorOverrides installs the previous step's action in the page, then // ExtractorOverrides installs the previous step's action and this step's log
// reads back what the spec's extractors computed against the live DOM. The // entries in the page, then reads back what the spec's extractors computed
// install is not best-effort: a web driver that cannot take it leaves // against the live DOM. Neither install is best-effort: a web driver that
// state.lastAction null in V8, which silently turns every action-gated // cannot take them leaves state.lastAction null and state.logs empty in V8,
// property vacuously true, so it is reported as an error instead. // which silently turns every action-gated property and every log property
// vacuously true, so both are reported as errors instead.
func (s webSource) ExtractorOverrides( func (s webSource) ExtractorOverrides(
ctx context.Context, ctx context.Context,
lastAction *verifier.Action, lastAction *verifier.Action,
logs []verifier.LogEntry,
) (map[int]json.RawMessage, error) { ) (map[int]json.RawMessage, error) {
installer, ok := s.web.(lastActionInstaller) actions, ok := s.web.(lastActionInstaller)
if !ok { if !ok {
return nil, fmt.Errorf( return nil, fmt.Errorf(
"web driver %T cannot install state.lastAction; every property gated "+ "web driver %T cannot install state.lastAction; every property gated "+
"on the last action would be vacuously true", s.web) "on the last action would be vacuously true", s.web)
} }
if err := installer.SetLastAction(ctx, verifier.EncodeLastAction(lastAction)); err != nil { if err := actions.SetLastAction(ctx, verifier.EncodeLastAction(lastAction)); err != nil {
return nil, fmt.Errorf("install last action: %w", err) return nil, fmt.Errorf("install last action: %w", err)
} }
entries, ok := s.web.(logInstaller)
if !ok {
return nil, fmt.Errorf(
"web driver %T cannot install state.logs; every property reading the "+
"log stream would be vacuously true", s.web)
}
if err := entries.SetLogs(ctx, verifier.EncodeLogs(logs)); err != nil {
return nil, fmt.Errorf("install logs: %w", err)
}
return s.web.EvaluateExtractors(ctx) return s.web.EvaluateExtractors(ctx)
} }