diff --git a/internal/verifier/bindings.go b/internal/verifier/bindings.go index 49cf8fd..56961b0 100644 --- a/internal/verifier/bindings.go +++ b/internal/verifier/bindings.go @@ -10,6 +10,12 @@ import ( type extractorState struct { getter goja.Callable handle *goja.Object + name string + // prev/curr cache the JSON-encoded extractor values from the prior and + // current PushSnapshot, used by ChangedExtractors to surface per-step + // diffs in the trace. + prev []byte + curr []byte } type formulaState struct { @@ -139,19 +145,29 @@ func (v *Verifier) installRuntimeBindings() error { } func (v *Verifier) bindExtract(call goja.FunctionCall) goja.Value { - if len(call.Arguments) != 1 { - panic(v.runtime.NewTypeError("extract requires exactly one argument")) + if len(call.Arguments) < 1 || len(call.Arguments) > 2 { + panic(v.runtime.NewTypeError("extract requires (getter) or (getter, name)")) } getter, ok := goja.AssertFunction(call.Arguments[0]) if !ok { panic(v.runtime.NewTypeError("extract argument must be a function")) } + name := "" + if len(call.Arguments) == 2 { + arg := call.Arguments[1] + if !goja.IsUndefined(arg) && !goja.IsNull(arg) { + name = arg.String() + } + } + if name == "" { + name = fmt.Sprintf("extractor_%d", len(v.extractors)) + } handle := v.runtime.NewObject() _ = handle.Set("current", goja.Undefined()) _ = handle.Set("previous", goja.Undefined()) - v.extractors = append(v.extractors, &extractorState{getter: getter, handle: handle}) + v.extractors = append(v.extractors, &extractorState{getter: getter, handle: handle, name: name}) return handle } diff --git a/internal/verifier/verifier_test.go b/internal/verifier/verifier_test.go index 5e275ab..dcdd7e4 100644 --- a/internal/verifier/verifier_test.go +++ b/internal/verifier/verifier_test.go @@ -894,6 +894,26 @@ func TestSelectorStringFromJS_CanonicalGrammar(t *testing.T) { } } +// TestExtract_DefaultsAndNamedNames verifies bindExtract assigns a fallback +// `extractor_N` name when no name is supplied and respects an explicit one. +func TestExtract_DefaultsAndNamedNames(t *testing.T) { + verifier := newVerifier(t) + mustLoad(t, verifier, ` + __sanderling__.extract(state => 1); + __sanderling__.extract(state => 2, "ledgerRows"); + __sanderling__.extract(state => 3); + `) + if len(verifier.extractors) != 3 { + t.Fatalf("extractors registered: got %d, want 3", len(verifier.extractors)) + } + want := []string{"extractor_0", "ledgerRows", "extractor_2"} + for i, name := range want { + if got := verifier.extractors[i].name; got != name { + t.Errorf("extractor %d name: got %q, want %q", i, got, name) + } + } +} + // TestSelectorStringFromJS_NullEmpty verifies that nil/undefined args produce // an empty string instead of "null"/"undefined" garbage. func TestSelectorStringFromJS_NullEmpty(t *testing.T) {