From d77f89aeb1377a9338f4a411974562862f951f6b Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 3 May 2026 11:04:38 +0700 Subject: [PATCH] fix(verifier): tolerate out-of-range override indices A single stale index from V8 aborted the entire override map, so any valid entries alongside it were dropped and verification ran on stale extractor values. V8 and goja register from the same bundle so a mismatch is unusual but recoverable. Skip out-of-range entries instead of erroring, and return the skipped count so the runner logs the mismatch without losing valid overrides. --- internal/runner/runner.go | 9 +++++++-- internal/verifier/verifier_test.go | 26 ++++++++++++++++++-------- internal/verifier/worker.go | 21 ++++++++++++++------- 3 files changed, 39 insertions(+), 17 deletions(-) diff --git a/internal/runner/runner.go b/internal/runner/runner.go index a3e0619..0a761c2 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -138,8 +138,13 @@ func Run(ctx context.Context, options Options) (Summary, error) { }); err != nil { return summary, fmt.Errorf("step %d push: %w", stepIndex, err) } - if err := options.Verifier.OverrideExtractorValues(v8Overrides); err != nil { - logger.Warn("v8 override apply failed", "step", stepIndex, "err", err) + skipped, overrideErr := options.Verifier.OverrideExtractorValues(v8Overrides) + if overrideErr != nil { + logger.Warn("v8 override apply failed", "step", stepIndex, "err", overrideErr) + } + if skipped > 0 { + logger.Warn("v8 override skipped out-of-range entries", + "step", stepIndex, "skipped", skipped, "have", len(v8Overrides)) } screen := "" diff --git a/internal/verifier/verifier_test.go b/internal/verifier/verifier_test.go index e4a1c4a..ab5272a 100644 --- a/internal/verifier/verifier_test.go +++ b/internal/verifier/verifier_test.go @@ -540,7 +540,7 @@ func TestOverrideExtractorValues_PreservesPrevious(t *testing.T) { if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{"ledger.balance": json.RawMessage(`100`)}}); err != nil { t.Fatal(err) } - if err := verifier.OverrideExtractorValues(map[int]json.RawMessage{1: json.RawMessage(`777`)}); err != nil { + if _, err := verifier.OverrideExtractorValues(map[int]json.RawMessage{1: json.RawMessage(`777`)}); err != nil { t.Fatal(err) } balance := verifier.runtime.GlobalObject().Get("balance").ToObject(verifier.runtime) @@ -564,10 +564,10 @@ func TestOverrideExtractorValues_NilIsNoop(t *testing.T) { if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{"ledger.balance": json.RawMessage(`42`)}}); err != nil { t.Fatal(err) } - if err := verifier.OverrideExtractorValues(nil); err != nil { + if _, err := verifier.OverrideExtractorValues(nil); err != nil { t.Fatal(err) } - if err := verifier.OverrideExtractorValues(map[int]json.RawMessage{}); err != nil { + if _, err := verifier.OverrideExtractorValues(map[int]json.RawMessage{}); err != nil { t.Fatal(err) } balance := verifier.runtime.GlobalObject().Get("balance").ToObject(verifier.runtime) @@ -576,14 +576,24 @@ func TestOverrideExtractorValues_NilIsNoop(t *testing.T) { } } -func TestOverrideExtractorValues_UnknownIndexErrors(t *testing.T) { +func TestOverrideExtractorValues_UnknownIndexSkipped(t *testing.T) { verifier := newVerifier(t) mustLoad(t, verifier, helloSpec) - if err := verifier.PushSnapshot(SnapshotInput{}); err != nil { + if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{"ledger.balance": json.RawMessage(`42`)}}); err != nil { t.Fatal(err) } - err := verifier.OverrideExtractorValues(map[int]json.RawMessage{99: json.RawMessage(`1`)}) - if err == nil || !strings.Contains(err.Error(), "out of range") { - t.Errorf("expected out-of-range error, got %v", err) + skipped, err := verifier.OverrideExtractorValues(map[int]json.RawMessage{ + 1: json.RawMessage(`777`), + 99: json.RawMessage(`1`), + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if skipped != 1 { + t.Errorf("expected 1 skipped entry, got %d", skipped) + } + balance := verifier.runtime.GlobalObject().Get("balance").ToObject(verifier.runtime) + if balance.Get("current").ToInteger() != 777 { + t.Errorf("valid override should still apply alongside skipped one, got current=%v", balance.Get("current")) } } diff --git a/internal/verifier/worker.go b/internal/verifier/worker.go index 3c29291..d588339 100644 --- a/internal/verifier/worker.go +++ b/internal/verifier/worker.go @@ -246,21 +246,28 @@ func (v *Verifier) PushSnapshot(input SnapshotInput) error { // state.ax shim. Passing a nil/empty map is a no-op so the mobile path can // call this unconditionally. The override must run *after* PushSnapshot // (which advanced `previous`) and *before* EvaluateProperties. -func (v *Verifier) OverrideExtractorValues(overrides map[int]json.RawMessage) error { +// +// Out-of-range indices are tolerated (skipped) rather than fatal: V8 and goja +// register extractors from the same spec bundle so counts should always +// match, but a stale or partial override map should not block valid overrides +// from applying. The number of skipped entries is reported so the caller can +// surface a mismatch. +func (v *Verifier) OverrideExtractorValues(overrides map[int]json.RawMessage) (skipped int, err error) { if len(overrides) == 0 { - return nil + return 0, nil } for index, raw := range overrides { if index < 0 || index >= len(v.extractors) { - return fmt.Errorf("extractor override index %d out of range (have %d)", index, len(v.extractors)) + skipped++ + continue } - value, err := jsonToJSValue(v.runtime, raw) - if err != nil { - return fmt.Errorf("extractor override %d: %w", index, err) + value, conversionErr := jsonToJSValue(v.runtime, raw) + if conversionErr != nil { + return skipped, fmt.Errorf("extractor override %d: %w", index, conversionErr) } _ = v.extractors[index].handle.Set("current", value) } - return nil + return skipped, nil } // SnapshotInput bundles everything a step feeds into the verifier. Fields