From 25e6725f9178e53ac9072154fa0ea946811bf777 Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 26 Apr 2026 23:57:43 +0700 Subject: [PATCH] fix(folio-spec): pick account card via seeded from(), not Math.random Math.random() breaks --seed reproducibility. The verifier's seeded RNG flows through from(), so re-running a seed now produces the same card pick sequence. --- examples/folio/sanderling/spec.ts | 2 +- internal/verifier/verifier_test.go | 43 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/examples/folio/sanderling/spec.ts b/examples/folio/sanderling/spec.ts index f112e87..7483770 100644 --- a/examples/folio/sanderling/spec.ts +++ b/examples/folio/sanderling/spec.ts @@ -162,7 +162,7 @@ const addTxn = actions(() => { if (route.current === "home") { const cards = accountCards.current; if (cards.length === 0) return []; - return [Tap({ on: cards[Math.floor(Math.random() * cards.length)] })]; + return [Tap({ on: from(cards).generate() })]; } if (route.current === "ledger") { const btn = addTxnButton.current; diff --git a/internal/verifier/verifier_test.go b/internal/verifier/verifier_test.go index c441375..2477266 100644 --- a/internal/verifier/verifier_test.go +++ b/internal/verifier/verifier_test.go @@ -259,6 +259,49 @@ func TestLoad_AcceptsSpecWithoutPropertiesOrActions(t *testing.T) { } } +// TestFrom_SeededReplayIsDeterministic guarantees `from()` over a per-step +// dynamic array picks the same element under the same seed across runs. The +// folio spec relies on this to replace Math.random() in account-card taps. +func TestFrom_SeededReplayIsDeterministic(t *testing.T) { + pickedSequence := func(seed uint64) []string { + verifier := newVerifier(t, WithRand(rand.New(rand.NewPCG(seed, 0)))) + mustLoad(t, verifier, ` + globalThis.actions = __sanderling__.actions(() => { + const cards = ["card_a", "card_b", "card_c", "card_d"]; + return [__sanderling__.tap({ on: __sanderling__.from(cards).generate() })]; + }); + `) + _ = verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{}}) + var picks []string + for range 20 { + action, err := verifier.NextAction() + if err != nil { + t.Fatal(err) + } + picks = append(picks, action.On) + } + return picks + } + first := pickedSequence(1234) + second := pickedSequence(1234) + for i := range first { + if first[i] != second[i] { + t.Fatalf("step %d: %q != %q (replay not deterministic)", i, first[i], second[i]) + } + } + other := pickedSequence(5678) + identical := true + for i := range first { + if first[i] != other[i] { + identical = false + break + } + } + if identical { + t.Fatal("expected different seeds to produce different pick sequences") + } +} + // PredicateError must reflect the most recent step's predicate result, not a // latched first-step error. The runner logs PredicateError once per step; if it // stays pinned to step 1 forever, downstream debugging looks frozen even though