mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-04 20:17:09 +00:00
test(spec): golden-fixture cross-runtime parity gate for the node picker
Replace the env-driven parity harness with a shared scenario module and a committed golden the node picker asserts independently. The goja side asserts the same golden, so neither runtime invokes the other at test time.
This commit is contained in:
1 parent
0fa6968a5a
commit
e3bb9aeeb2
3 files changed
+193
-27
No files matched your search
+127
@@ -0,0 +1,127 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 250,
|
||||||
|
"y": 260,
|
||||||
|
"selector": "id:gamma"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 150,
|
||||||
|
"y": 160,
|
||||||
|
"selector": "id:beta"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 50,
|
||||||
|
"y": 60,
|
||||||
|
"selector": "id:alpha"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "InputText",
|
||||||
|
"x": 150,
|
||||||
|
"y": 160,
|
||||||
|
"text": "a",
|
||||||
|
"selector": "id:beta"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "InputText",
|
||||||
|
"x": 50,
|
||||||
|
"y": 60,
|
||||||
|
"text": "\t\n",
|
||||||
|
"selector": "id:alpha"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 250,
|
||||||
|
"y": 260,
|
||||||
|
"selector": "id:gamma"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 150,
|
||||||
|
"y": 160,
|
||||||
|
"selector": "id:beta"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 250,
|
||||||
|
"y": 260,
|
||||||
|
"selector": "id:gamma"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 150,
|
||||||
|
"y": 160,
|
||||||
|
"selector": "id:beta"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 250,
|
||||||
|
"y": 260,
|
||||||
|
"selector": "id:gamma"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 250,
|
||||||
|
"y": 260,
|
||||||
|
"selector": "id:gamma"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 150,
|
||||||
|
"y": 160,
|
||||||
|
"selector": "id:beta"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 250,
|
||||||
|
"y": 260,
|
||||||
|
"selector": "id:gamma"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 50,
|
||||||
|
"y": 60,
|
||||||
|
"selector": "id:alpha"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "InputText",
|
||||||
|
"x": 150,
|
||||||
|
"y": 160,
|
||||||
|
"text": "999999999999999999999",
|
||||||
|
"selector": "id:beta"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "InputText",
|
||||||
|
"x": 150,
|
||||||
|
"y": 160,
|
||||||
|
"text": "-1",
|
||||||
|
"selector": "id:beta"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "InputText",
|
||||||
|
"x": 250,
|
||||||
|
"y": 260,
|
||||||
|
"text": "-1",
|
||||||
|
"selector": "id:gamma"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 50,
|
||||||
|
"y": 60,
|
||||||
|
"selector": "id:alpha"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 250,
|
||||||
|
"y": 260,
|
||||||
|
"selector": "id:gamma"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "Tap",
|
||||||
|
"x": 50,
|
||||||
|
"y": 60,
|
||||||
|
"selector": "id:alpha"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,37 +1,52 @@
|
|||||||
// Cross-runtime parity harness. Run under node (tsx) by the Go test
|
// Shared cross-runtime parity scenario: the FIXED seed, FIXED candidate list,
|
||||||
// internal/verifier/parity_test.go, which feeds the SAME seed and candidate
|
// and FIXED action root that both the node test (parity.test.ts) and the goja
|
||||||
// list into the goja verifier and asserts an identical action stream. Both
|
// test (internal/verifier/parity_test.go) drive. Each side runs the SAME
|
||||||
// engines run the SAME pick.ts over the SAME Pcg, so any divergence here is an
|
// pick.ts over the SAME Pcg and asserts the SAME committed golden
|
||||||
// engine-level (number/bigint) parity bug.
|
// (fixtures/parity-golden.json); matching one golden on both sides proves the
|
||||||
|
// two engines agree without either invoking the other.
|
||||||
//
|
//
|
||||||
// Inputs come from env so the Go side controls them:
|
// The candidate ORDER and the per-tick PCG draw order are the parity contract.
|
||||||
// SANDERLING_PARITY_SEED decimal uint64 seed
|
// The weighted root mixes a tap branch (1 candidate draw) with a typing branch
|
||||||
// SANDERLING_PARITY_STEPS action count to emit
|
// (1 candidate draw + 1 corpus draw), so a tick exercises weighted selection, a
|
||||||
// SANDERLING_PARITY_CANDIDATES JSON array of {x,y,selector,width,height}
|
// builtin, and the input corpus together; reordering candidates or adding or
|
||||||
//
|
// dropping a draw on either side shifts the stream and fails the golden.
|
||||||
// Output: one JSON array of serialized actions on stdout.
|
|
||||||
|
|
||||||
import { installRuntime } from "../src/runtime-entry.ts";
|
import { Pcg } from "../src/pcg.ts";
|
||||||
|
import { nextAction } from "../src/pick.ts";
|
||||||
|
import { serializeAction, type SerializedAction } from "../src/runtime-entry.ts";
|
||||||
|
import { taps, typing, weighted } from "../src/actions.ts";
|
||||||
import type { BuiltinVerb, Candidate, GeneratorNode, Host } from "../src/action-tree.ts";
|
import type { BuiltinVerb, Candidate, GeneratorNode, Host } from "../src/action-tree.ts";
|
||||||
|
|
||||||
const seed = BigInt(process.env.SANDERLING_PARITY_SEED ?? "0");
|
export const PARITY_SEED_HI = 0x9e3779b97f4a7c15n;
|
||||||
const steps = Number(process.env.SANDERLING_PARITY_STEPS ?? "0");
|
export const PARITY_STEPS = 20;
|
||||||
const candidates = JSON.parse(process.env.SANDERLING_PARITY_CANDIDATES ?? "[]") as Candidate[];
|
|
||||||
|
|
||||||
const host: Host = {
|
export const PARITY_CANDIDATES: Candidate[] = [
|
||||||
|
{ x: 50, y: 60, selector: "id:alpha", width: 100, height: 40 },
|
||||||
|
{ x: 150, y: 160, selector: "id:beta", width: 120, height: 48 },
|
||||||
|
{ x: 250, y: 260, selector: "id:gamma", width: 80, height: 32 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// A 3:1 weighted split over taps and typing. The stub host returns the same
|
||||||
|
// candidate list for every verb so the only variables are the draw order and
|
||||||
|
// the JS engine's number/bigint behavior.
|
||||||
|
export const PARITY_ROOT: GeneratorNode = weighted([3, taps], [1, typing]);
|
||||||
|
|
||||||
|
const HOST: Host = {
|
||||||
platform: () => "android",
|
platform: () => "android",
|
||||||
queryCandidates: (_verb: BuiltinVerb) => candidates,
|
queryCandidates: (_verb: BuiltinVerb) => PARITY_CANDIDATES,
|
||||||
reportUnsupported: () => {},
|
reportUnsupported: () => {},
|
||||||
seedHi: () => seed,
|
seedHi: () => PARITY_SEED_HI,
|
||||||
seedLo: () => 0n,
|
seedLo: () => 0n,
|
||||||
};
|
};
|
||||||
|
|
||||||
const root: GeneratorNode = { kind: "builtin", verb: "taps" };
|
// runParity emits the parity scenario's action stream from a fresh Pcg. It
|
||||||
|
// drives pick.ts directly (not installRuntime, whose globals are locked once)
|
||||||
installRuntime(host, root, () => ({}));
|
// so the caller can run it repeatedly to assert determinism.
|
||||||
|
export function runParity(): (SerializedAction | null)[] {
|
||||||
const nextAction = (globalThis as { __sanderlingNextAction__: () => unknown })
|
const rng = new Pcg(PARITY_SEED_HI, 0n);
|
||||||
.__sanderlingNextAction__;
|
const stream: (SerializedAction | null)[] = [];
|
||||||
const stream = [];
|
for (let i = 0; i < PARITY_STEPS; i++) {
|
||||||
for (let i = 0; i < steps; i++) stream.push(nextAction());
|
stream.push(serializeAction(nextAction(PARITY_ROOT, rng, HOST)));
|
||||||
process.stdout.write(JSON.stringify(stream));
|
}
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { test } from "node:test";
|
||||||
|
|
||||||
|
import { PARITY_STEPS, runParity } from "./parity-harness.ts";
|
||||||
|
|
||||||
|
const goldenPath = fileURLToPath(new URL("./fixtures/parity-golden.json", import.meta.url));
|
||||||
|
const golden = JSON.parse(readFileSync(goldenPath, "utf8"));
|
||||||
|
|
||||||
|
// The node side of the W2 acceptance gate: the shared picker's stream for the
|
||||||
|
// fixed parity scenario must match the committed golden byte-for-byte. The goja
|
||||||
|
// test asserts the SAME golden, so a match on both sides proves cross-runtime
|
||||||
|
// parity. A drift here means pick.ts, the corpus, or the Pcg changed.
|
||||||
|
test("node picker matches the cross-runtime golden", () => {
|
||||||
|
const stream = runParity();
|
||||||
|
assert.equal(stream.length, PARITY_STEPS);
|
||||||
|
assert.deepEqual(stream, golden);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Determinism: a second run from the same seed yields the identical stream.
|
||||||
|
test("node picker is deterministic across repeated runs", () => {
|
||||||
|
assert.deepEqual(runParity(), runParity());
|
||||||
|
});
|
||||||
Reference in new issue
Block a user