Testing #
| Check | Command | Catches |
|---|---|---|
| Type checking | npm run typecheck |
strict checking of every TS/TSX component, controller, canvas renderer and worker |
| Shared rules build | npm run core (core/wasm/build.sh) |
Rust/wasm-bindgen toolchain mismatch; produces core/wasm/pkg/ for every check below |
| Shared rules (native) | cargo test --locked --manifest-path core/Cargo.toml |
save reading/refusal, stop and fit rules, the lookup line loop and filtered retries, offers, scheduling (react, bedtime and missed diary, idle), catch-up |
| Engine, body and saves | node --test test/*.test.mjs |
the WebAssembly build's body rules, offers, sleeping answers and wake rolls, turns and lookups, silent lines, the owner's return, v1-to-v2 local migration, bad eggs and ownership/reset write guards |
| Prompt parity | node test/parity.mjs |
drift between the core's WebAssembly build and Python in headers, stat words, word lists, look/clock/recall, event lines and offers |
| Rust host | cargo test --locked --manifest-path server/Cargo.toml |
private access, background turns, both transfers, stale replies, deduplication, persistence, body and sleeping offers |
| Native serving (opt-in) | release --test native -- --ignored --test-threads=1, with explicit runtime/model/profile/output env |
real CPU/CUDA parity, near-limit prompts, lookup continuation, concurrent private HTTP and in-flight transfer fencing |
| Actual startup fallback (opt-in) | python3 test/host-startup.py --help for required inputs |
missing CUDA provider keeps real CPU serving, authentication, CORS, no-store and transfers |
| Native parity and replay | build tokenling-contract and the WebAssembly rules, then node server/tests/parity.mjs |
native/Python headers, lookups, 15 lists and wake probabilities across 300 states; a 31-step engine scenario replayed natively and in WebAssembly with identical JSON after every step, in UTC and in a daylight-saving zone |
| Model behavior | train/eval_pet.py CKPT 40 |
invalid tags, unresolved lookups, unanswered offers (including after an opener that skipped the answer), decisions that ignore the tummy, chatty idle turns, diaries that do not end |
| Quantization | train/quant_eval.py CKPT EXPORT data.jsonl |
4-bit quality loss |
| Data rules | python3 test/data_rules.py |
sleep/offer rules, corrupt or shifted supervision, equivalent span coverage, diary masks, no-wake/mixed migration and a stand-in sleep teacher |
| Page contract | node test/contract.mjs |
host mount/config, component ids, documented config keys and accidental imperative rendering |
| Site build | site/build.sh /tmp/site-out |
unfilled placeholders, a config block that is not valid JSON |
| Script lifecycle | python3 test/site_tools.py |
failed-build preservation, publication retry/immutability and preview runtime refresh (builds the shared rules once first; a timed-out script is killed with its children) |
| Brain speed and memory | <base>/bench.html in every site build |
load time, decode tokens/s, prefill time, page memory |
| Browser flow | playwright-cli against test/dev-site.sh or the live URL |
everything the visitor sees |
Run npm ci first (Node.js 24+). npm run format formats the frontend and
new build/test scripts; npm run format:check checks without writing.
npm test builds the shared rules to WebAssembly, then runs strict type
checking, prompt parity, the component/host contract, Node's built-in
engine/body/save tests, Python data rules and script lifecycle checks. It needs
the Rust toolchain from core. Node executes the erasable
TypeScript in the pure modules directly and loads the module with
initCoreSync(); no separate test transpiler or browser framework is required.
Real browser checks use the playwright-cli skill. A repeatable regression
flow, including a real model download, is available for a fresh, disposable
persistent profile:
playwright-cli -s=pet open http://127.0.0.1:8765/pet/?debug=1 --profile=/tmp/pet-test-profile
playwright-cli -s=pet run-code --filename=test/browser-flow.js
It checks edited/cleared hatch names, chat, care offers, healthy medicine, shelf/memory rendering, real diary generation, catch pointer/keyboard controls, tab keys, all panels at 1280/768/360 px, cached reload and Web Lock takeover. The shelf/gift and catch setup use explicit debug tags to make UI assertions independent of model sampling. Use only a test profile: it creates a pet and changes its state. Error recovery, imports, offline use and upgrades have additional recipes below.
For the optional Rust host, start it with the configuration in
its guide, allow the preview's browser origin and set
REMOTE_ENDPOINT when building the preview. Use another fresh disposable profile:
playwright-cli -s=host open http://127.0.0.1:8765/pet/?debug=1 --profile=/tmp/host-test-profile
playwright-cli -s=host run-code --filename=test/hosting-flow.js
This flow deliberately loses an activation acknowledgement, checks remote creation without a local model download, closes the app page for 20 seconds, transfers in both directions with the real brains, and recovers during a simulated outage. On loopback it uses a second hostname as a stand-in for another host, verifying that the old pending stop/key survives the move. It checks 1280/768/360 px and preserves newer recovery-branch state when the stop receipt arrives. Injected network failures produce expected browser resource errors. Test profiles contain private keys: keep them outside Git.
test/fixtures/save-v1.json is an actual browser export from the previous
vanilla app at commit 6c99c23, captured with generic Sam/Mochi names on
2026-10-07. The save tests
require the current v2 migration to preserve every original pet value while
adding local hosting metadata. The earlier Preact rewrite kept v1.
test/site_tools.py uses Python's standard library and the actual shell
scripts with disposable model/runtime stand-ins. It needs no downloaded
weights, teacher, GPU or browser. It exercises failed release/config/render
builds without destroying old output, runtime-publication failure followed by
same-id retry, immutable artifacts and permission overrides, and repeat
previews over HTTP, including obsolete runtime removal and --rebuild.
Preact migration check receipt (2026-10-07) #
Measured in headless desktop Chromium against a local build, with the real v6 model and vendored runtime; these are functional checks, not new speed results.
npm test: strict types, 300 parity states/15 lists with zero mismatches, component/host contract, eight body/save tests and ten data-rule tests passed.npm run format:check, cleannpm ci, root and subpath builds also passed.test/browser-flow.jspassed: fresh download/hatch, chat, food and healthy medicine offers, memory/shelf, real diary generation, pointer/keyboard catch, all panels at 1280/768/360 px, cached reload and tab takeover. The regular flow had no browser console errors or warnings. LCD screenshot inspected.- Loaded a save from the previous vanilla app; name, owner and transcript survived. A v99 save stayed untouched after rendering/ticking. Valid save import, export and hard reset worked in a disposable profile.
- A deliberately missing worker script showed the retry control at 360 px; retry loaded the real brain and preserved the egg and edited name.
- Upgrade dialog/download/swap/cache cleanup passed using two local aliases of
the same v6 export (a stand-in for two compatible releases). The pet survived
and only the selected model's files remained. This does not establish v5/v6
prompt compatibility; the release's
olderlist remains empty. - With the preview server stopped, reload loaded the cached page, both app bundles, runtime and brain; isolation stayed enabled and a real chat reply completed. Static explanations/build details were visible with JavaScript off.
- Not re-tested for this rewrite: physical phones, Firefox's throttled download, actual install prompts, background alert delivery, or the 90-second one-thread fallback. Their existing logic remains in the typed controllers.
PR #1 integration check receipt (2026-10-07) #
The fixes were integrated into the Preact/TypeScript app and checked in headless desktop Chromium against a local build with the real v6 brain.
- Formatting, strict types, 300 parity states/15 lists with zero mismatches, the page contract, eight body/save tests, 20 data-rule tests and four script lifecycle tests passed. Root and subpath builds passed.
test/browser-flow.jspassed, including real download/generation, diary, care, catch controls, cached reload, tab takeover and all panels at 1280/768/360 px.- Controlled-clock checks applied a three-hour awake gap, a one-minute gap, a half-hour sleeping gap and a ten-day absence exactly once. The ten-day absence matched the seven-day simulation cap.
- Blocked session storage still booted at 360 px. Injected worker-constructor and initial-message failures released the worker and showed retry; restoring the native API loaded v6 while preserving the egg and edited name. No uncaught errors occurred in these recovery checks.
- These checks did not retrain a model or remeasure speed. Physical phones, Firefox, offline reload, upgrades, alerts and the watchdog stall fallback were not re-tested for this merge.
Optional Rust host check receipt (2026-10-09) #
Measured in headless desktop Chromium against a local preview with the real v6 q4 export. Native CPU used eight threads; the browser used its normal WASM loader. These are functional checks, not new speed or capacity measurements.
npm testand formatting passed: strict types, 300 JS/Python states and 15 lists with zero mismatches, page contract, eight body/save tests, 20 data-rule tests and four script lifecycle tests. Root and subpath builds passed. The real v1 fixture migrated to v2 without losing original values.- Seven Rust tests passed: private access, restart persistence, deduplicated care, complete-save handoffs, old generation/prepared activation fencing, sleeping offers/body rules, memory eviction and scripted GPU failure fallback. Rust/Python parity also had 300 states, 15 lists and zero mismatches.
test/browser-flow.jspassed with real local generation, care, diary, catch controls, cached reload and tab takeover. No unexpected browser console errors or warnings appeared in the regular flow.test/hosting-flow.jspassed: remote creation without a local model download; a committed activation whose response was deliberately lost; body progression with the app page closed; both transfers retaining name, owner, traits and birth time; offline checkpoint recovery; moving to another host while the previous host was unreachable; eventual stop without overwriting newer memories. The alternate host used a loopback hostname alias and a separate hosted record, rather than another physical machine.- A separate real native-brain check produced autonomous turns with the page closed, including walking, emotes, sleep and a dream. API tests use scripted generators to establish timing and fencing deterministically.
- All panels and hosting controls fit 1280/768/360 px without horizontal overflow. Setup and LCD screenshots were inspected.
- Not measured: physical phones, native CUDA performance/integration, simultaneous CPU/GPU capacity, public-host deployment, ATProto sign-in or multiplayer. No production model/runtime artifact was changed.
Native CUDA and mixed-serving check receipt (2026-10-09) #
Measured with the same immutable v6 q4 file, native ONNX Runtime 1.30.0 (CUDA 13 build) and an RTX 3090, CPU intra-op threads 8, GPU launch threads 1, inter-op 1 and TF32 disabled. These are brief warm qualification runs with explicit greedy sampling, not production stochastic or sustained capacity.
- Eight ordinary Rust tests passed, including CPU/background and GPU/reactive preference with serialized lanes and GPU-failure fallback. The opt-in native HTTP test passed in CPU, GPU-only and mixed profiles.
- Three real prompt cases had 64, 66 and 976 input tokens and 15, 3 and 15 output tokens. Five repetitions per case/profile matched CPU/CUDA text, stop reason and token counts. The last case has 991 total tokens; this is near-limit validation, not a full 1024-token acceptance measurement.
- The private HTTP test ran simultaneous bursts of 1/4/8/16/32 pets, three rounds each. All 183 turns in each profile completed and parked their complete saves without failures. Mixed runs recorded overlapping CPU/GPU calls for every multi-pet burst. Requests queue behind one session per worker; this does not mean 32 generations run in parallel.
- Actual model-emitted clock lookup/continuation and a handoff during native inference passed on both devices. Cancellation discarded the stale turn and kept the GPU worker available. Ordinary tests cover GPU error requeue; a hardware failure was not induced on the shared GPU.
- CUDA KV values now stay on-device through I/O binding. Only logits return for sampling. The earlier host-returned-KV candidate was slower, especially near the context limit; raw timings belong in deployment qualification receipts, with the machine and thread count recorded.
test/host-startup.pypassed first with a stand-in and then with the actual server, CPU-only ORT and requested CUDA. CPU serving remained ready, produced a real reply and passed authentication, CORS/no-store, local transfer and deletion. The private test database stayed outside Git.- Formatting, types, both prompt parity suites, page contract, eight Node tests, 20 Python data-rule tests and four script lifecycle tests passed. Frontend browser flows were not repeated for this native-only change; their preceding receipt remains above.
- This was one GPU-only/mixed run order, with short identical seven-token HTTP replies. It does not establish long-response, mixed-length fairness, batching, many-minute steady load, public-host operation or physical-phone behavior. Small bursts can finish sooner on GPU alone because CPU work adds a slower tail; cost-aware routing remains follow-up work.
See the native-test recipe and variables.
Queue-cost routing: CPU check receipt (2026-10-09) #
The routing candidate keeps nine whole-turn timing estimates per worker and plans completion over queued work. Unit cases cover avoiding a late CPU tail, larger queues using both lanes, unknown/busy shapes, unavailable GPU, and background aging across context edits. Four pure routing tests and ten host tests pass, including filtered diaries keeping a healthy GPU available.
The real CPU test passed 1/4/8/16/32-pet short and varied-length bursts, three rounds each, followed by a 120-second owner-traffic window with eight pets at production temperature 0.85. All pets progressed and final handoffs completed. Controlled bursts disable chatter so extra idle turns do not enter the latency comparison. The steady phase permits ordinary diary/cancellation behavior. This receipt validates CPU and rules; the GPU and mixed receipt follows. It is not a serving-capacity claim.
Queue-cost routing: GPU and mixed receipt (2026-10-10) #
Measured with the same immutable v6 q4 file and native ONNX Runtime 1.30.0 (CUDA 13 build) on an RTX 3090: CPU intra-op threads 8, TF32 disabled, KV on the device. Each window ran two profiles back to back, with greedy bursts of 1/4/8/16/32 pets (short and varied lengths, three rounds) and then 120 seconds of eight-pet owner traffic at temperature 0.85. Every profile passed its transfer, deletion and failure assertions, with no failed or cancelled steady calls; within each run, steady turns per pet differed by at most two. Medians over the three rounds:
| Profile, position in window | Short 4 | Varied 4 | Short 32 | Varied 32 | Steady turns | Steady p50/p95 |
|---|---|---|---|---|---|---|
| GPU-only, first | 117 ms | 207 ms | 932 ms | 1,641 ms | 1,459 | 604/724 ms |
| Mixed, previous router, second | 146 ms | 1,081 ms | 926 ms | 1,586 ms | 1,309 | 672/905 ms |
| Mixed, current router, first | 118 ms | 221 ms | 866 ms | 1,663 ms | 1,447 | 613/726 ms |
| GPU-only, second | 118 ms | 217 ms | 981 ms | 1,684 ms | 1,306 | 667/921 ms |
The previous router sent a long prompt to the idle CPU in every varied 4-pet round (about 1.07 s against about 0.1 s on the GPU), because that shape had only ever run on the CPU. One unusual early sample (a two-call clock turn) also kept a GPU estimate high long enough to send a short 4-pet turn to the CPU. The current router averages the first five samples and estimates a shape seen on only one worker from shapes both have run. Two unit tests reproduce these measured cases. After a restart, each new shape still costs one CPU sample: the first varied 4-pet round took 1,211 ms; the next two took 221 and 204 ms. In steady traffic, the CPU ran 23 of 1,929 calls, all in the first 7.3 s as conversations grew into new shapes. From 8 pets up, mixed bursts matched GPU-only or finished up to 8% sooner. At 32 varied pets, the GPU always finished last, and the difference was within the round-to-round spread (1,617–1,768 ms).
Position in the window mattered more than the profile. The second profile ran with this card at 75–78 °C under software thermal slowdown. Median GPU time per generated token on long steady prompts rose from 5.84–5.85 to 6.08–6.18 ms, and it completed about 10% fewer steady turns, whether GPU-only or mixed. Compare profiles only at the same position. A first-position GPU-only run on a second RTX 3090 without thermal slowdown completed 1,478 steady turns (5.85 ms per token), with bursts within 4%. During the current-router window, an unrelated CPU-heavy process shared the host. These are two short windows on one host. They are not sustained or production capacity and do not include phones.
Private-network HTTPS deployment receipt (2026-10-10) #
The CPU-only host ran as a hardened system service behind an HTTPS reverse
proxy on a private network, with the app served from a different origin on
the same network. Four intra-op threads, no GPU, the immutable v6 graph and
native ONNX Runtime 1.30.0 (CPU build). Health, no-store, exact-origin CORS
(a foreign origin's preflight received no allow-origin header) and 401 for
an unauthenticated pet read were checked through the proxy.
test/hosting-flow.js passed against the deployed page and host in 31 s.
The loopback-only alternate-host step does not apply to a real hostname and
was skipped. A separate remote pet produced four host-written lines (walks,
emotes, a want) in 63 s with no app page open, and the reopened page synced
them. Panels fit at 1280, 768 and 360 px. Both test pets were deleted through
the API, leaving no pet or operation rows. The service used about 0.6 GiB of
memory. One deployment; no load, multi-day or phone checks.
Shared rules migration check receipt (2026-10-10) #
The rules the browser and the host both apply moved into core/ (one Rust
crate, native in the host and WebAssembly in the page and worker). It replaced
the browser's TypeScript prompt and body modules, the host's own save/prompt
modules and both runtimes' copies of care, tags, turns, diary and save rules.
The earlier isolated WebAssembly proof (2026-10-09) is superseded and was
removed. Measured on one Linux desktop: headless Chromium, the real v6 q4
export, native CPU ORT with four threads. These are functional checks, not
speed measurements.
npm test: core build, strict types, 300 parity states/15 lists with zero mismatches through the WebAssembly build, the page contract, 13 Node engine/body/save tests (the real v1 fixture migrates losslessly), 20 data-rule tests and 4 script lifecycle tests. Formatting checks passed.cargo testforcore/(6 tests) andserver/(17 tests, host tests unchanged in substance) passed; the releasenativetest compiled.node server/tests/parity.mjs: 300 native/Python states and 15 lists with zero mismatches, and the 31-step engine replay identical in both builds in UTC and America/Los_Angeles (across the November DST change).test/browser-flow.jspassed against a local preview: fresh download, hatch and real generation, chat, food and healthy-medicine offers, memory/shelf, a real diary page (paragraphs kept), catch controls, keyboard tabs, all panels at 1280/768/360 px, cached reload and Web Lock takeover. No browser console errors or warnings.test/hosting-flow.jspassed in 32 s against that preview and a local release host with the real native CPU brain: remote creation with a lost activation acknowledgement and no local model download, closed-page body progress, both transfer directions preserving identity, offline recovery, a second (loopback alias) host and eventual stop, and 1280/768/360 px. The only console errors were the flow's injected network failures.- Builds of the same commit from two checkout paths (one through a symlink) first produced different modules: Rust embedded absolute source paths. After remapping paths and stripping symbols, both produced identical files, and the checks above passed again on that build.
- Not re-tested for this change: physical phones, Firefox, install prompts, background alert delivery, upgrades between two releases, the 90-second one-thread fallback and CUDA serving.
Preview server #
export MODEL=$DATA/release/tokenling-v6 RUNTIME=$DATA/release/runtime # from export.sh and vendor.sh
test/dev-site.sh /tmp/dev-site & # stages and serves http://127.0.0.1:8765/pet/
test/dev-site.sh /tmp/dev-site --rebuild # after editing web/ TypeScript/components or the page
The script writes a release description for the preview and runs
site/build.sh (the same build as a real deploy, including test/bench.html),
symlinks the exported model, copies the runtime, and serves everything with
the COOP/COEP headers, so crossOriginIsolated is true and WASM threads work.
A full run refreshes the local runtime from RUNTIME, removing obsolete files
only after the new copy succeeds. --rebuild changes page/app files only and
leaves the staged runtime and models alone.
Options are environment variables, documented in the script header: PORT,
BASE (the URL base path, default pet), MODEL_ID, OLDER (export
directories staged as older brains), NEWS, FEATURES, and TRAINED and
SOURCE (text for the page's details). REMOTE_ENDPOINT supplies the optional
default host address. ISOLATE=0 serves without the headers for A/B tests.
ONNX_BYTES_PER_SECOND (default 0, unlimited)
throttles only .onnx responses, including URLs with a query string, for
slow-download checks; other files retain normal speed.
Speed and memory: open http://127.0.0.1:8765/pet/bench.html (query
threads=N, qdq=1, prepack=1, dtype=…, model=ID) and read
window.res; await window.mem() returns page memory in MB.
Stop it with pkill -f '[s]erve.py /tmp/dev-site'.
Debug panel #
Open the page with ?debug=1 (or #debug) to get a debug tab after the
others. It is not saved or shown otherwise, and it sends nothing anywhere.
- Pet: the running brain, stage, day and exact age, and whether an offer is open; buttons that set the age to the start of each stage; an age in days to set directly (the stage follows from the age and no "grew" line is written, so going back to baby works); sliders for the four stats; a sick switch; current wake probabilities and persistence count.
- Write today's page: debug-only write/rewrite, disabled while the brain is busy or unavailable or a game runs. It uses today's log so far even with fewer than two entries. An early page suppresses automatic bedtime writing; rewrite later to include later events. A failed rewrite retains the old page.
- Context: the last 20 prompts sent to the brain (chat lines, lookups and diaries), newest first, each exactly as the worker built it after trimming to the context window, with the model's output highlighted and the stop reason. "copy latest" copies the newest prompt and output; "copy full chat" copies the current header and the whole stored chat (more than the model sees once the chat is long). Prompts are kept in memory until the page reloads.
Use it to see why the pet did something: copy the prompt and replay it against a checkpoint, or look at which lines the context window dropped.
Browser recipes #
Slow Firefox download and recovery #
Start a fresh preview/profile with ONNX_BYTES_PER_SECOND=2000000 in the
preview server's environment and open it with
playwright-cli -s=slow open URL --browser=firefox --profile=/tmp/slow-profile.
Wait for navigator.serviceWorker.controller before getting an egg. The
97 MB model transfer must run past 30 seconds and reach a ready brain; before
the fix it ended with Error in input stream at roughly 30 seconds on Linux
Firefox. Leave the storage-persistence prompt unanswered: it must not delay
the download. Check cached reload, hatch/chat and offline reload afterward.
For recovery, interrupt a model response before completion. The egg must
remain unhatched with the error and setup-retry visible. Restore the network,
click retry, and check that the same egg and entered pet name survive and
the download reaches ready. Check worker script failure too: its error must
release the worker so retry can start. Test the retry row at 360 px.
An interrupted response may instead report protobuf parsing failed; check
that the bad ONNX entry is evicted and retry downloads a fresh copy.
Catch-up and restricted browser APIs #
Use a disposable profile for controlled-clock checks. Freeze Date.now(),
let an animation frame establish the live tick baseline, then advance it and
dispatch visibilitychange while the document is visible. Set an awake pet's
energy to 80 with other needs comfortable and no poop due: after a three-hour
return energy should be 65 and stay there on the next frame, not decay twice.
A one-minute gap should still be applied once by the live tick. With lights
off, a sleeping pet at energy 10 reaches 21 after half an hour, without a
second recovery on the next frame. Also check that a ten-day absence consumes
only the capped seven-day replay and is not reapplied by the live tick. Reload
and discard the test save after replacing the clock.
Before submitting "get an egg", inject a throwing Worker constructor, then
repeat with a real worker whose initial postMessage throws. Both should
reach brain.state === "error" immediately, release the worker and expose
#setup-retry. Restore the native API and retry: a new worker must start with
the same egg and edited pet name. This exercises startup recovery without
requiring model weights; successful download/inference needs the regular
model preview below.
For blocked storage, use page.addInitScript to make the sessionStorage
getter throw SecurityError before loading the page. The app must still boot
without an uncaught error or horizontal overflow at 360 px. Check normal
two-tab ownership and "use it here" separately with session storage available.
Regular flows #
Use a named session with a persistent profile whenever the brain is
involved. An in-memory profile cannot store the model: Cache Storage put
fails with "Unexpected internal error" and the brain reloads every time.
playwright-cli -s=pet open http://127.0.0.1:8765/pet/ --profile=/tmp/pet-profile
playwright-cli -s=pet fill "#owner-name" "Sam" --submit
The owner step ("get an egg") starts the brain download. Wait for it, then hatch:
playwright-cli -s=pet run-code "async page => {
await page.waitForFunction(() => ['ready','error'].includes(window.tokenling.brain.state), null, {timeout: 300000});
await page.fill('#pet-name', 'Mochi'); await page.click('#hatch-btn'); }"
playwright-cli -s=pet --raw eval "document.querySelector('#brain-status').textContent"
Offers and pats: window.tokenling.act('feed', 'cookie') (or bath,
medicine, play, pat), wait for !window.tokenling.brain.busy, then read
the chat: the offer line should be followed by a pet line with [yes] or
[no], and state().today.log shows "ate a cookie" or "said no to a
cookie". Idle turns start a few seconds after the brain is ready; pause them
by keeping the tab hidden or reading state().chat right after a turn.
Talk to it and read the transcript:
playwright-cli -s=pet run-code "async page => {
const say = async (m) => { await page.fill('#chat-input', m); await page.press('#chat-input', 'Enter');
await page.waitForTimeout(500);
await page.waitForFunction(() => !window.tokenling.brain.busy, null, {timeout: 180000}); };
await say('what time is it?'); await say('my favorite animal is the frog');
return await page.evaluate(() => window.tokenling.state().chat.join('\n')); }"
window.tokenling exposes state() (a read-only snapshot of the engine's
save; with ?debug=1, change it with debug.edit((s) => { s.stats.tummy = 15 })), brain (including id, the running model, and tps and
prefillMs of the last turn), upgrade, view, act(kind, arg),
petTurn(kind?), wantTurn(kind?), renderAll() and
writeDiary(dateKey?, rewrite?) (rewrite=true is honored only for today in
debug mode; tests can call this or seed history with a past day through
debug.edit and let the engine schedule the missed page).
Sleep: in debug mode, tokenling.debug.setWakeRoll(() => 0.5) fixes
only disturbance randomness; setWakeRoll(null) restores it. Reset the counter
with resetDisturbances(). At energy 10 a first message stays asleep with
that roll; at energy 70 it wakes. A roll of 0.99 leaves pats and lights asleep.
Offer a cookie while hungry and while stuffed: the offer itself never wakes;
only an accepted reply writes woke up and eats. Debug also exposes
applyTags (the engine applies the tags as if the pet wrote them), simulate
and edit for body checks. Confirm the sleep grime timer pauses and
resumes, and no more than two dream entries crowd the day log.
Optional eval_pet.py CKPT 80 REPORT.json saves metrics and all new probe
transcripts. The v6 eval adds idle/lights-off/message/pat dream validity, mumble length,
dream spread, awake replies after waking, sleeping offer answer/acceptance
rates by energy, tummy and cookie versus carrot, and ugh/literally and
positive hunger claims while stuffed by stage. It also probes medicine offers
while healthy/sick and awake/asleep. They run after the earlier
metrics. Use the same eval version for both checkpoints; results are samples,
not guarantees for every pet.
Animations: each CLI call takes about a second, longer than most
animations. Trigger and capture inside one run-code:
playwright-cli -s=pet run-code "async page => {
await page.evaluate(() => { window.tokenling.state().stats.tummy = 20; window.tokenling.act('feed','rice ball'); });
await page.waitForTimeout(300);
await page.locator('#pet-lcd').screenshot({path: 'eat.png'}); }"
Mobile: open with --mobile (360 px wide) and check that
document.documentElement.scrollWidth === innerWidth. Take a full-page
screenshot and look at the chat input, the action grid and the notes.
Catch controls: use a real pointer or touch gesture to hold each pad button, slide off it, and release; movement should continue while held and stop on release, cancellation, lost capture, blur or a hidden page. Try overlapping directions, rapid taps, arrow/A/D keys, and Space/Enter on a focused pad button. The pad should neither select text nor zoom. Check page scrolling and pinch zoom outside it too. For the brain reminder, check download, upgrade, progress and error states above the device at 360 px and desktop widths, including after switching tabs.
Offline: after the brain has loaded once, stop the preview server and
reload: the page, app and runtime come from the service worker, the brain
from Cache Storage, and crossOriginIsolated stays true.
playwright-cli network-state-set offline is a weaker test (it left
navigator.onLine true here).
Upgrade: start the preview with only the older model
(MODEL=$DATA/release/tokenling-v2), hatch a pet, then restart it with
MODEL=…/tokenling-v3 OLDER=…/tokenling-v2 (same port, same profile) and
reload. The pet should wake on v2 with the upgrade dialog open; "upgrade"
downloads v3 while v2 runs (window.tokenling.upgrade), swaps, and leaves only
v3's files in transformers-cache. playwright-cli requests should show one
request for the new .onnx.
For the v5 → v6 format change, stage v6 with no OLDER instead: the saved
pet must doze until v6 is downloaded. Check that its name, memories, chat and
diary survive the download, and that cached v5 is never loaded by the v6 app.
Feature gating: give a component a brainHas("x") disabled/class/title
gate and data-needs="x" for inspection, set
window.tokenling.brain.id to an older id that lacks x, call
window.tokenling.renderAll(), and the control is disabled with the class
needs-brain; set it back and it is enabled again.
Two tabs: open the page in a second tab: it shows "open in another tab" and does not run the pet. "use it here" moves the pet there, and the first tab shows the notice.
Bad egg: write a broken or newer save from a same-origin page that does
not run the app (the running app saves every second and would overwrite it),
for example goto <base>/assets/OFL-nunito.txt, then
eval "() => { localStorage.setItem('tokenling.v1', JSON.stringify({...JSON.parse(localStorage.getItem('tokenling.v1')), v: 99})); return 1 }",
and go back to the page. Check that the save is untouched, then try "start
anew" (it moves the save to tokenling.bad) and importing the old save.
Alerts: grant permission with
run-code "async page => page.context().grantPermissions(['notifications'], {origin: ORIGIN})",
tick #alerts-toggle, and read the plan from the tokenling-alerts cache.
Headless Chromium always reports the page as visible, so the worker never
alerts while it is open; to test delivery, write a plan with a past item,
navigate the tab to another page in scope, post {type: "alerts"} to the
worker and read registration.getNotifications().
Install: the console shows "Banner not shown: beforeinstallpromptevent.preventDefault() called" when the page is installable; after hatching, the install suggestion shows in the chat.
Console: playwright-cli console and console warning. Anything there,
especially ERR_BLOCKED_BY_RESPONSE, is worth chasing.
Close up: playwright-cli close-all. Do not leave sessions or profile
directories running.
What to check after a change #
- Art or animation: screenshot
#pet-lcdat each stage; check eyes, mouth, decorations, the shadow and nothing clipped at the LCD edges. - Simulation: set stats through
window.tokenling.state()and callact; check meters, event lines and the day log. - Prompt or tools: parity test, then
eval_pet.py, then a live chat that triggers each lookup and checks the app wrote the right{result}. - Page layout: desktop (≥1280), tablet and 360 px; keyboard-only through the tabs and action buttons; the explanation section reads without JavaScript.
- Saves, tabs, PWA: the upgrade, two tabs, bad egg, offline and alerts
recipes above after touching
load(), the boot sequence,sw.jsor the brain loader. - Brain loading: fresh profile (the egg's download), cached reload, and
bench.htmlfor speed and memory after any change toquantize.pyor the session options inbrain.ts. - After deploy: the same flow against the live URL with a fresh persistent
profile, plus the live checks in
deployment.md.
Phones #
Automated checks use desktop Chromium with a 360 px viewport. Speed and memory
on real phones have not been measured; open bench.html on one to do so.