// Usage: node server/tests/parity.mjs [CONTRACT_BINARY] // Build tokenling-contract and core/wasm/build.sh first. No model or ONNX // library is loaded. Checks the native core against train/world.py, then // replays one engine scenario natively and in WebAssembly, in two time zones. import assert from "node:assert/strict"; import { execFileSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import * as C from "../../web/core.ts"; const root = fileURLToPath(new URL("../../", import.meta.url)); const binary = process.argv[2] || root + "server/target/debug/tokenling-contract"; if (!existsSync(binary)) throw new Error("Build tokenling-contract with cargo before running Rust parity."); const expected = JSON.parse( execFileSync("python3", [root + "test/parity_states.py", "1"], { maxBuffer: 1 << 26 }), ); const actual = JSON.parse( execFileSync(binary, { input: JSON.stringify(expected), maxBuffer: 1 << 26 }), ); assert.deepEqual(actual.lists, expected.lists); for (let i = 0; i < expected.cases.length; i++) { for (const key of ["chat", "diary", "look", "clock", "recall"]) assert.equal(actual.cases[i][key], expected.cases[i][key], `${i}: ${key}`); assert.ok( actual.cases[i].wake.every((p, j) => Math.abs(p - expected.cases[i].wake[j]) < 1e-12), `${i}: wake chances`, ); } console.log( `${expected.cases.length} Rust/Python states, ${Object.keys(expected.lists).length} lists, 0 mismatches`, ); C.initCoreSync(readFileSync(root + "core/wasm/pkg/tokenling_core_bg.wasm")); const MIN = 60e3, HOUR = 60 * MIN; // A day with chat, lookups, offers, a game, an absence, sleep, dreams, a // missed diary page and growth. The second zone crosses a DST change. function scenario(zone, t0) { const save = JSON.parse(readFileSync(root + "test/fixtures/save-v1.json", "utf8")); Object.assign(save, { created: t0 - 12 * HOUR, lastTick: t0, nextPoop: t0 + 2 * HOUR }); save.pet.hatched = t0 - 11 * HOUR; const care = (kind, item = null) => ({ type: "care", kind, item }); return { save: JSON.stringify(save), t0, zone, seed: "parity", rolls: [0.12, 0.83, 0.47, 0.05, 0.66, 0.91, 0.3, 0.58], steps: [ { op: "advance", dt: 1000, returning: false }, { op: "command", dt: 1000, command: { type: "message", text: "hello friend, frogs?" } }, { op: "task", dt: 0, background: false }, { op: "chat", dt: 500, kind: "react", replies: [ [" hi [recall:frogs]", "lookup"], [" i [look]", "lookup"], [" [clock]", "lookup"], [" yay [remember:Sam likes frogs] [gift:flower] [emote:love]", "line"], ], }, { op: "command", dt: 1000, command: care("feed", "cookie") }, { op: "command", dt: 10, command: care("bath") }, { op: "line", dt: 2000, text: "nom [yes] [walk:left]" }, { op: "command", dt: 1000, command: care("play") }, { op: "line", dt: 1000, text: "[yes] [emote:happy]" }, { op: "game", dt: 20000, score: 5 }, { op: "advance", dt: 3 * HOUR, returning: true }, { op: "command", dt: 1000, command: care("clean") }, { op: "command", dt: 1000, command: care("pat") }, { op: "command", dt: 1000, command: care("pat") }, { op: "command", dt: 1000, command: { type: "message", text: "goodnight little one" } }, { op: "chat", dt: 1000, kind: "idle", replies: [ [" fuck", "line"], [" sleepy [sleep] [diary:a big day]", "line"], ], }, { op: "line", dt: 1000, text: "[dream:moon] mm" }, { op: "task", dt: 2000, background: true }, { op: "diary", dt: 1000, reply: " it was fun [doodle:heart] [doodle:spaceship]\n\nwe played. [emote:happy]", }, { op: "command", dt: 1000, command: care("lights") }, { op: "advance", dt: 10 * HOUR, returning: false }, { op: "task", dt: 1000, background: true }, { op: "alerts", dt: 1000 }, { op: "command", dt: 1000, command: { type: "forgetMemory", index: 0, fact: "wrong" } }, { op: "command", dt: 1000, command: { type: "rename", name: "S[a]m:" } }, { op: "command", dt: 1000, command: { type: "chatter", value: false } }, { op: "line", dt: 1000, text: "[gift:star] [want:snack] [diary:new day]" }, { op: "task", dt: 31000, background: true }, { op: "advance", dt: 3 * 24 * HOUR, returning: true }, { op: "task", dt: 31000, background: true }, { op: "diary", dt: 1000, date: "missed", reply: " i missed sam.\nthe end [doodle:moon]" }, ], }; } function replay(sc) { const parsed = C.parseSave(sc.save, sc.t0, sc.zone); assert.ok("state" in parsed, parsed.error); const e = new C.Engine(parsed.state, sc.t0, sc.seed); let next = 0; e.setRolls(() => sc.rolls[next++ % sc.rolls.length]); let t = sc.t0; const attempt = (fn) => { try { fn(); return null; } catch (error) { return { error: error.message }; } }; const out = []; for (const step of sc.steps) { t += step.dt; let result = null; if (step.op === "command") result = attempt(() => e.command(step.command, t)); else if (step.op === "line") e.commitLine(step.text, t); else if (step.op === "game") result = attempt(() => e.command({ type: "gameComplete", gameId: e.game?.id || "", score: step.score }, t), ); else if (step.op === "advance") e.advance(t, step.returning); else if (step.op === "chat") { e.beginTurn(t); const turn = e.chatTurn(step.kind, t), requests = []; for (const [text, reason] of step.replies) { requests.push(turn.request()); if (!turn.feed(text, reason)) break; } const body = turn.body() ?? ""; e.commitLine(body, t); result = { requests, body }; } else if (step.op === "diary") { const state = e.state(); const date = step.date === "missed" ? Object.keys(state.history).sort()[0] || "" : state.today.date; const turn = e.diaryTurn(date, t); if (turn) { const request = turn.request(); result = { request, committed: e.commitDiary(turn, step.reply) }; } } else if (step.op === "task") result = e.chooseTask(t, step.background); else if (step.op === "alerts") result = e.alertPlan(t); out.push({ out: result, engine: JSON.parse(e.engineJson()) }); } return out; } for (const [zone, start] of [ ["UTC", "2026-10-01T18:00:00Z"], ["America/Los_Angeles", "2026-11-01T01:00:00Z"], ]) { process.env.TZ = zone; // the browser build reads the device's clock const sc = scenario(zone, Date.parse(start)); const native = JSON.parse( execFileSync(binary, { input: JSON.stringify({ scenario: sc }), maxBuffer: 1 << 26 }), ); const wasm = replay(sc); assert.equal(wasm.length, native.length); for (let i = 0; i < native.length; i++) assert.deepEqual(wasm[i], native[i], `${zone} step ${i} (${sc.steps[i].op})`); const last = native.at(-1).engine.state; assert.ok(last.diary.length === 2 && last.pet.stage === "teen", `${zone}: scenario ran`); console.log(`${zone}: ${native.length} engine steps identical in native and WebAssembly builds`); }