mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
Setup runs through the picker with the rng under both policies, so a generator there is legitimate and must keep working. Interleaving enumeration and setup catches the flag leaking out of the model's walk. Claude-Session: https://claude.ai/code/session_01A5KmftdEJ49A9z5mF5ESrX
126 lines
3.8 KiB
Go
126 lines
3.8 KiB
Go
package verifier
|
|
|
|
import (
|
|
"errors"
|
|
"maps"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"testing"
|
|
|
|
"github.com/priyanshujain/sanderling/internal/bundler"
|
|
"github.com/priyanshujain/sanderling/internal/hierarchy"
|
|
)
|
|
|
|
// bundleInlineSpec bundles an inline spec through the real @sanderling/spec API
|
|
// and goja runtime entry, so the bundle installs __sanderlingSetupAction__ the
|
|
// way the CLI does.
|
|
func bundleInlineSpec(t *testing.T, source string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
specPath := filepath.Join(dir, "spec.ts")
|
|
if err := os.WriteFile(specPath, []byte(source), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
abs := func(rel string) string {
|
|
path, err := filepath.Abs(rel)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
bundle, err := bundler.Bundle(bundler.Options{
|
|
EntryFile: specPath,
|
|
RuntimeFile: abs("../../pkg/spec/src/goja-runtime.ts"),
|
|
Aliases: map[string]string{
|
|
"@sanderling/spec": abs("../../pkg/spec/src/index.ts"),
|
|
"@sanderling/spec/defaults": abs("../../pkg/spec/src/defaults/index.ts"),
|
|
"@sanderling/spec/defaults/properties": abs("../../pkg/spec/src/defaults/properties.ts"),
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("bundle: %v", err)
|
|
}
|
|
return string(bundle.JavaScript)
|
|
}
|
|
|
|
func loadBundled(t *testing.T, source, treeJSON string) *Verifier {
|
|
t.Helper()
|
|
v, err := New()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := v.Load(bundleInlineSpec(t, source)); err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
tree, err := hierarchy.Parse(treeJSON)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := v.PushSnapshot(SnapshotInput{Tree: tree}); err != nil {
|
|
t.Fatalf("push snapshot: %v", err)
|
|
}
|
|
return v
|
|
}
|
|
|
|
func TestSetupActionWalksSetupOnly(t *testing.T) {
|
|
spec := `
|
|
import { Tap, actions, taps } from "@sanderling/spec";
|
|
export const setup = actions(() => [Tap({ on: "id:SignIn" })]);
|
|
export const actionsRoot = taps;
|
|
`
|
|
v := loadBundled(t, spec, enumTreeJSON)
|
|
action, err := v.SetupAction()
|
|
if err != nil {
|
|
t.Fatalf("SetupAction: %v", err)
|
|
}
|
|
if action.Kind != ActionKindTap || action.On != "id:SignIn" {
|
|
t.Errorf("SetupAction = %+v, want Tap on id:SignIn from setup", action)
|
|
}
|
|
}
|
|
|
|
func TestSetupActionIgnoresActionsRoot(t *testing.T) {
|
|
// No setup, but a live actionsRoot that NextAction would happily draw from.
|
|
spec := `
|
|
import { taps } from "@sanderling/spec";
|
|
export const actionsRoot = taps;
|
|
`
|
|
v := loadBundled(t, spec, enumTreeJSON)
|
|
if _, err := v.SetupAction(); !errors.Is(err, ErrNoAction) {
|
|
t.Fatalf("SetupAction err = %v, want ErrNoAction when no setup is declared", err)
|
|
}
|
|
// Sanity: the seeded root would have produced an action, proving SetupAction
|
|
// deliberately skips it.
|
|
if _, err := v.NextAction(); err != nil {
|
|
t.Fatalf("NextAction should draw from actionsRoot: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestSetupGeneratorDrawsUnderTheModelPolicy: setup walks through the picker
|
|
// with its rng under both policies, so a generator there is not the divergence
|
|
// the enumeration refuses and must keep drawing. Enumerating before every setup
|
|
// step is what a model-driven run does, and the refusal must not leak out of it.
|
|
func TestSetupGeneratorDrawsUnderTheModelPolicy(t *testing.T) {
|
|
spec := `
|
|
import { InputText, actions, integers, taps } from "@sanderling/spec";
|
|
const setupValues = integers().between(1, 500);
|
|
export const setup = actions(() => [InputText({ into: "id:Amount", text: String(setupValues.generate()) })]);
|
|
export const actionsRoot = taps;
|
|
`
|
|
v := loadBundled(t, spec, policyTreeJSON)
|
|
typed := map[string]bool{}
|
|
for range 16 {
|
|
if _, err := v.Candidates(LabelSourceVisibleText); err != nil {
|
|
t.Fatalf("Candidates: %v", err)
|
|
}
|
|
action, err := v.SetupAction()
|
|
if err != nil {
|
|
t.Fatalf("SetupAction: %v", err)
|
|
}
|
|
typed[action.Text] = true
|
|
}
|
|
if len(typed) < 2 {
|
|
t.Errorf("setup typed %v on every step; the picker's rng did not reach it", slices.Sorted(maps.Keys(typed)))
|
|
}
|
|
}
|