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