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.
This commit is contained in:
pj committed 2026-05-03 11:04:38 +07:00
1 parent 3b204b75dc
commit d77f89aeb1
3 files changed
+39 -17

No files matched your search

+7 -2
View File
@@ -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 := ""
+18 -8
View File
@@ -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"))
}
}
+14 -7
View File
@@ -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