Testing
strikeabox is tested in layers. Each layer checks something the others cannot: a pass against the mock Strike server says nothing about the real Strike API, and a unit test says nothing about the app in a browser.
Test layers
| Layer | Runs against | Examples |
|---|---|---|
| Unit and property | In process | Pricing, outcome evaluator, grid geometry, request signing, configuration and the network allowlist |
| Database integration | A real PostgreSQL 16 | Atomic acceptance, settle-exactly-once, ledger sums to zero, background settlement |
| Mock Strike | packages/strike-mock on localhost | Builder Connect, live box lifecycle, timeouts, reconnects, close failures |
| Browser | The production build in Playwright, with a mock API | Mobile and desktop flows, rejection states, reconnects, accessibility, offline shell, update banner |
| Full-stack browser | The built server, PostgreSQL, the built web app and the mock Strike price stream | Tap to settlement, idempotent retries, a stale feed, receipts and replays, rooms, battles, referrals, leaderboards |
Checks against Strike testnet itself are separate. The full-stack suite can run on Strike's real price feed (see Full-stack browser tests). Builder Connect and a live box end to end on real Strike need a registered builder code and a funded testnet wallet, so they are not part of the automated tests.
What the tests cover
- Market data. The collector records
markprice@1sticks, ignores a duplicate(symbol, E), reconnects with backoff and flags implausible ticks. A gap longer than 2.5 s is detected, and a market with no tick for 3 s is stale: quotes and taps stop until it recovers. Only one collector records at a time. - Practice. Acceptance is atomic: retries are idempotent, concurrent taps cannot overspend or exceed the limits, and a bad tag, an old quote, the cutoff, a moved quote or a stale market are rejected. Each box settles exactly once under concurrent workers, restarts and replays; the ledger sums to zero; settlement finishes with no client connected, a lost wake-up is caught by polling and an expired job lease is picked up again.
- Live mode. Request signing (message format, empty-body hash, path and raw query). Builder Connect: challenge and verify, consent, key encryption, a disconnect that shreds the key, and a key revoked at Strike. The live box lifecycle: an entry that fills fully, partly or not at all, take-profit, stop, time exit, rejection, a dropped command, a timeout followed by a lookup before any re-send, user-stream reconnects, a restart mid-flight and a failed close. The network allowlist: a host of the other network is refused at start-up, with no fallback.
- Social. Rooms, presence from live connections only, the opt-in room feed, leaderboards from settled bets, battle scoring and finalisation, rematches, referral attribution rules and abuse limits.
- Web app. The chart draws only observed ticks and breaks across gaps; a stale market shows a banner and greys the board. The replay page is built from stored evidence, and its preview image is served. The service worker caches only the app shell; offline, taps are disabled and nothing is queued. Every test in the browser suite fails on an unexpected console error. A tap shows "Sending" within 100 ms, and the board draws within the 60 fps frame budget, also with the CPU slowed four times.
Unit and property tests
pnpm --filter @sbx/core testpackages/core holds the domain math. Property tests (fast-check) cover, among others:
- touch probabilities stay in [0, 1], are symmetric above and below the price, shrink as a box moves away from the price, and agree with a Monte-Carlo estimate within tolerance;
- the robust quote is never below the model, and the empirical estimate replays recorded paths through the same settlement rule;
- multipliers round down and respect the offer limits (probability 10–80 %, multiplier 1.10×–9.00×);
- the outcome evaluator: hits on boundary ticks, hits between ticks, early hits, gaps, misses and voids;
- grid geometry: epoch-aligned columns, aligned rows, stable cell ids, cutoff math.
Integration tests with a real database
The server tests use four disposable databases. Create them once:
for d in practice market social strike; do PGPASSWORD=sbx createdb -h 127.0.0.1 -U sbx sbx_test_$d; done
pnpm --filter @sbx/server test| Database | Used by | Override |
|---|---|---|
sbx_test_practice | practice, HTTP and process tests | SBX_TEST_DATABASE_URL |
sbx_test_market | market tests | MARKET_TEST_DATABASE_URL |
sbx_test_social | social tests | SBX_TEST_SOCIAL_DATABASE_URL |
sbx_test_strike | Strike and Live tests (created if missing) | SBX_TEST_PG |
Practice, social and Strike test files each get their own fresh schema with every migration applied, so they run in parallel without sharing state. Market test files take turns on their database, each starting from a freshly reset schema. Never point these variables at a database you want to keep. The tests cover concurrent taps that must not overspend, idempotent retries, every rejection, settlement exactly once under concurrent workers, restarts and replays, expired job leases, and ledger reconciliation.
The mock Strike
The mock in packages/strike-mock speaks Strike's price stream, REST API and user stream on localhost, and can inject fills, partial fills, no fills, rejections, dropped commands, timeouts and stream drops. Tests point the server at it with NODE_ENV=test and STRIKE_ALLOW_MOCK_HOSTS=true; that combination is refused in any other environment.
Browser tests
The web app's end-to-end tests run against the production build (served by vite preview, talking to a development mock of the API in apps/web/dev/mock-api.ts) with Playwright 1.56 and Chromium:
pnpm --filter @sbx/web build
pnpm --filter @sbx/web e2eThey run at 390 × 844 and 1440 × 900, and include an accessibility scan (axe: no serious or critical issues), reduced motion, touch target size, double taps, the cutoff race, reloads mid-tap, lost responses and an outage mode. The mock API's prices follow a synthetic path; they are test data, not market data.
Full-stack browser tests
pnpm --filter @sbx/web e2e:fullThis suite builds the server and the web app, creates a fresh Postgres database, and starts the server bundle in role all, serving the built web app as in production. Prices come from the mock Strike price stream, so outcomes are deterministic. pnpm --filter @sbx/web e2e:full:real runs the same stack on Strike testnet's real price feed instead: one practice tap is accepted and settled from whatever the market did.
The full-stack suite builds the server and the web app, starts the server (role all) on a fresh sbx_e2e_full database with prices from the mock Strike, and drives it from the browser:
pnpm --filter @sbx/web e2e:fullpnpm --filter @sbx/web e2e:full:real runs one practice tap against Strike's real price feed instead; its outcome is whatever the market does.
Calibration on recorded prices
Two scripts measure the pricing, and neither is a pass/fail test.
packages/core/scripts/model-bias.tssimulates the idealised random-walk model to measure its discrete-monitoring bias.apps/server/scripts/calibrate.tsreplays recorded Strike mark prices from atickstable through the production pricing and settlement code, and reports predicted against realised touch rates and the return to player. It is what the Fairness and calibration page reports.
STRIKE_DATA_NETWORK=mainnet DATABASE_URL=postgres://sbx:sbx@127.0.0.1:5432/<db> npx tsx apps/server/scripts/calibrate.ts 5 robust 1800The arguments are the quote interval in seconds, the mode (robust is the production quote; model is the random-walk model alone) and the history length in seconds. STRIKE_DATA_NETWORK only labels the output with the network the ticks were recorded on (default testnet; the published figures are on mainnet). Treat the output as a description of the data it ran on, not as a guarantee.
Mutation checks
scripts/mutation-check.mjs applies one deliberate mutation at a time from a JSON list, runs the named test command and expects it to fail, so a safeguard can't be removed unnoticed:
node scripts/mutation-check.mjs <mutations.json>Each entry is { name, file, find, replace, test }. The script restores the file after each run and exits non-zero if any mutation survives. The safeguards covered include idempotency, the settlement status guard, the cutoff and the quote-tag check. CI runs every mutations.json in the repository.