Table of Contents
- Testing
- Contents
- Test framework and setup
- What "framework" means here
- The harness (duplicated per workspace, two byte-identical pairs)
- The runner (47 lines, behaviour-identical across all four suites)
- TypeScript is loaded directly
- Committed fixtures
- Assertion style
- Running tests
- Writing new tests
- Test patterns
- 1. Injected fakes over mocks and over the real platform
- 2. Global stubs: install, then restore in finally
- 3. Source-introspection gates ("grep the shipped source")
- 4. Build-artifact gates (assert on the shipped bundle)
- Two more conventions worth copying
- Anti-patterns to avoid
- Coverage requirements
- Visual capture workflow
- Repo gate CLIs (privacy-audit / budget-audit)
- CI integration
- Quick reference
Testing
arah² has no third-party test framework. Every suite in this repo is a hand-rolled, zero-dependency Node script that registers cases into a small array and drains it. The runner shape was written first in theme/, then copied into bot/ and web/, and again into the newer search/ and routing/ workspaces — five workspaces, one idea, deliberately.
Beyond the suites, additional gates stand in for CI (this repo has none — see CI integration): tsc --noEmit typechecks, the theme's off-token audit is itself a tested CLI, and two repo-root gate CLIs (privacy-audit, budget-audit) each carry their own TDD suite.
Case totals (verified against the current tree):
| Workspace | Suites | Cases |
|---|---|---|
bot/ |
10 | 126 |
web/ |
14 | 165 |
search/ |
8 | 57 |
routing/ |
9 | 65 |
theme/ (single audit suite) |
1 | — |
Contents
- Test framework and setup
- Running tests
- Writing new tests
- Test patterns
- Coverage requirements
- Visual capture workflow
- Repo gate CLIs
- CI integration
Test framework and setup
What "framework" means here
| Workspace | Runner | Harness | Registered via | Command |
|---|---|---|---|---|
bot/ |
bot/test/runner.mjs |
bot/test/harness.mjs |
import { test } from "./harness.mjs" |
npm run bot:test |
web/ |
web/test/runner.mjs |
web/test/harness.mjs |
import { test } from "./harness.mjs" |
npm run web:test |
search/ |
search/test/runner.mjs |
search/test/harness.mjs |
import { test } from "./harness.mjs" |
npm run search:test |
routing/ |
routing/test/runner.mjs |
routing/test/harness.mjs |
import { test } from "./harness.mjs" |
npm run routing:test |
theme/ |
inline in theme/scripts/audit.test.mjs |
local check() helper |
check("name", cond, detail) |
npm test -w theme |
There is no jest, vitest, mocha, playwright-test, or any other runner in any package.json. The test-relevant dev dependencies are typescript (tsc --noEmit, a compile-time gate) and playwright (used by the capture scripts, not by any suite). routing/ additionally dev-depends on sharp, but only for its glyph/road-graph build tooling — never for tests.
The harness (duplicated per workspace, two byte-identical pairs)
bot/test/harness.mjs and web/test/harness.mjs are byte-identical; search/test/harness.mjs and routing/test/harness.mjs are a second byte-identical pair with the same five-line body:
const tests = [];
export function test(name, fn) {
tests.push({ name, fn });
}
export const registry = tests;
test(name, fn) pushes a case. fn may be sync or async — the runner awaits every one.
The runner (47 lines, behaviour-identical across all four suites)
bot/test/runner.mjs, web/test/runner.mjs, search/test/runner.mjs, and routing/test/runner.mjs are behaviour-identical (only the header comments differ):
readdirSync(import.meta.dirname), filter*.test.mjs, sort.- Dynamic-
import()each suite inside atry/catch. A suite that throws at load time is not fatal — it is pushed into the registry as a single failing case namedsuite load failed: <file>. The run always completes. - Drain
registryin registration order:PASS <name>on stdout, orFAIL <name>plus the error stack on stderr. - Print
[runner] <n> passed, <m> failedand setprocess.exitCode = 1if anything failed.
Registration order is therefore: suite files in alphabetical order, cases in file order. There is no shuffling, no parallelism, no filtering, no watch mode.
TypeScript is loaded directly
All four test/ directories await import("../src/....ts") directly against the shipped source — no build step, no transpile step, no fixtures directory (except the committed fixture files described below). This works because Node strips types natively (the code comments call this "Node 24 type stripping").
Committed fixtures
Unlike the rest of the suites, two test directories carry small committed fixtures (both gitignored-pattern-free, plain files):
search/test/fixtures/—golden-corpus.json(the golden search corpus the CO cases measure),overpass-response.json(a recorded Overpass response the topup cases replay).routing/test/fixtures/—mini-graph.bin(a tiny built graph binary),golden-pairs.json(pinned origin/destination pairs), andbuild-mini-graph.mjs(the generator that produced the binary).
Assertion style
Every suite in bot/, web/, search/, and routing/ uses node:assert's strict mode, imported as:
import { strict as assert } from "node:assert";
In practice: assert.ok(...) for existence checks, assert.equal / assert.strictEqual for scalar equality, assert.deepEqual / assert.deepStrictEqual for structural equality, assert.match / assert.doesNotMatch for regex, and assert.throws / assert.doesNotThrow when a throw is the expected result. Every assertion carries a message string explaining the invariant being pinned.
theme/scripts/audit.test.mjs does not use node:assert at all — it defines a local check(name, cond, detail = '') that accumulates into passes[] / failures[], reports them all at the end, and exits 1 if any failed.
Running tests
Everything, per workspace
# from the repo root
npm run bot:test # → node test/runner.mjs in bot/
npm run web:test # → node test/runner.mjs in web/
npm run search:test # → node test/runner.mjs in search/
npm run routing:test # → node test/runner.mjs in routing/
npm test -w theme # → node ./scripts/audit.test.mjs in theme/
There is no root-level npm test aggregate — run all five, or add one to package.json when you need it.
Running one suite
The runners auto-discover by filename and offer no filter flag. A single suite file invoked directly executes its module side effects but produces no pass/fail output — the runner is what drains the registry. Always go through the runner:
cd bot && node test/runner.mjs # all bot suites
cd web && node test/runner.mjs # all web suites
cd search && node test/runner.mjs # all search suites
cd routing && node test/runner.mjs # all routing suites
theme is the exception — its suite is a standalone script and does report standalone:
cd theme && node scripts/audit.test.mjs
Typecheck gates
None of the suites compile. Run these alongside:
npm run bot:typecheck # tsc --noEmit in bot/
npm run web:typecheck # tsc --noEmit in web/
npm run search:typecheck # tsc --noEmit in search/
npm run routing:typecheck # tsc --noEmit in routing/
The theme audit as a standalone tool
The audit is both the thing under test and a useful CLI in its own right. Run it on the real build:
npm run audit # → node ./scripts/audit.mjs ./dist/rams.css
Its exit codes are a contract: 0 clean, 1 violations (each printed as file:line: declaration), 2 token-sheet drift or error. See theme/scripts/audit.mjs for the full usage block and its scope adjudications.
graph TD
ROOT["bot:test / web:test / search:test / routing:test / theme test"] --> BR["bot/test/runner.mjs"]
ROOT --> WR["web/test/runner.mjs"]
ROOT --> SR["search/test/runner.mjs"]
ROOT --> RR["routing/test/runner.mjs"]
ROOT --> TR["theme/scripts/audit.test.mjs"]
BR --> BDISC["readdir *.test.mjs → dynamic import"]
BDISC --> BREG["bot/test/harness.mjs registry"]
BREG --> BCASES["cari · commands · guidance-cron · guidance-envelope · initdata · location-pin · privacy · route-privacy · sessions · webhook"]
WR --> WDISC["readdir *.test.mjs → dynamic import"]
WDISC --> WREG["web/test/harness.mjs registry"]
WREG --> WCASES["build-artifact · copy-gate · destination-preload · failure-state-matrix · fullscreen · guidance-eta · guidance-replay · guidance-surface · homescreen · location-chain · no-storage · route-surface · session · theme"]
SR --> SDISC["readdir *.test.mjs → dynamic import"]
SDISC --> SREG["search/test/harness.mjs registry"]
SREG --> SCASES["converter · corpus · distance · gazetteer · loader-env · normalizer · score · topup"]
RR --> RDISC["readdir *.test.mjs → dynamic import"]
RDISC --> RREG["routing/test/harness.mjs registry"]
RREG --> RCASES["astar · contract · corpus · fixture-source · graph · http-source · loader-env · polyline · snap"]
TR --> TFIX["fixtures under /tmp/opencode/audit-test/"]
TFIX --> TSPAWN["spawnSync scripts/audit.mjs"]
TSPAWN --> TCHECK["check() → passes[] / failures[]"]
TCHECK --> TREAL["also audits real dist/rams.css"]
classDef entry fill:#EB8008,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
classDef runner fill:#1A1A1A,stroke:#EB8008,stroke-width:2px,color:#F7F0E3
classDef harness fill:#D9D2C6,stroke:#1A1A1A,stroke-width:2px,color:#1A1A1A
classDef theme fill:#F5EDE0,stroke:#736B1E,stroke-width:2px,color:#1E1B14
class ROOT entry
class BR,WR,SR,RR,TR runner
class BDISC,BREG,WDISC,WREG,SDISC,SREG,RDISC,RREG harness
class TFIX,TSPAWN,TCHECK,TREAL theme
Writing new tests
Naming and location
| Rule | Value |
|---|---|
| File name | <subject>.test.mjs — the runner only discovers this suffix |
| Directory | bot/test/, web/test/, search/test/, or routing/test/ — flat, no subdirectories (fixtures live in a fixtures/ dir, not alongside suites) |
| Import the harness | import { test } from "./harness.mjs"; |
| Case name | Prefix with a stable case code: "WK1 POST valid initData (under cap) → 200 {token: uuid}…", "CR3 NO CRYPTO IN THE DEEP-LINK PATH…", "RT4 null contract — …". The code is the identity; the prose is the explanation. |
Case-code families currently in use (sampled from the suite headers):
bot/—WK(webhook),CS(commands),CR(cari venue card),GC(guidance cron,GC-Ccron-cases),GE(guidance envelope),T(initData),LP(location pin),PG(privacy),RP(route privacy),SM/SR/SG(sessions)web/—BA(build artifact),COPY(copy gate),DP(destination preload),FM(failure-state matrix),FS(fullscreen),E(guidance ETA),R(guidance replay),GS(guidance surface),HS(homescreen),GEO(location chain / storage),RS(route surface),SESS(session),PX02(theme)search/—CV(converter),CO(corpus quality),D(damerau),G(gazetteer),L(loader env),T(normalizer),S(score),TP(topup)routing/—A(astar),C(contract),RT(route corpus),F(fixture source),G(graph binary),H(http source),L(loader env),P(polyline)
A new file dropped into any test/ directory is picked up automatically on the next run — no manifest, no import list, no registration step anywhere.
The minimum shape
#!/usr/bin/env node
/*
* <subject>.test.mjs — what this suite pins, and under which requirement id.
* ------------------------------------------------------------------------
* <CODE> one line per case, so a reader can see the contract without
* reading the code.
*/
import { strict as assert } from "node:assert";
import { test } from "./harness.mjs";
const { someExport } = await import("../src/some/module.ts");
test("<CODE> <what is asserted> <and why>", () => {
assert.equal(someExport("input"), "expected", "why this matters");
});
test("<CODE> async path", async () => {
const res = await someAsync();
assert.ok(res.ok, `expected ok:true, got ${JSON.stringify(res)}`);
});
For theme/, add cases to theme/scripts/audit.test.mjs using the local helper instead:
{
const r = runAudit([dirtyFixture]);
check('T2 dirty stylesheet → exit 1', r.status === 1, `status=${r.status} error=${r.error}`);
}
Note the theme suite uses a bare block ({ … }) rather than a named function, and relies on passes[]/failures[] instead of exceptions.
Where the source of truth lives
Every suite header comment lists its cases up front. That header is the documentation — keep it in sync when adding a case. Most cases reference decision ids or requirement tags (D-02, PRIV-02, GEO-03, CS4, COPY5, WK3, RTNG-02) that the case is pinning; reuse that convention.
Test patterns
The repo has converged on four recognisable patterns. Follow the closest precedent rather than inventing a new one.
1. Injected fakes over mocks and over the real platform
bot/test/sessions.test.mjs, bot/test/webhook.test.mjs, and bot/test/location-pin.test.mjs all hand-roll a ~20-line KVNamespace fake rather than depend on miniflare or wrangler:
function makeKV() {
const map = new Map();
const ttlLog = [];
return {
async get(key) { return map.has(key) ? map.get(key) : null; },
async put(key, value, opts) {
ttlLog.push({ key, expirationTtl: opts?.expirationTtl ?? null });
map.set(key, value);
},
async delete(key) { map.delete(key); },
map,
ttlLog,
};
}
Two variants exist and the difference matters:
- Asserting fake (
sessions.test.mjs) —put()itself throws ifexpirationTtlis missing or non-finite, making the TTL law unbreakable from the inside. - Recording fake (
webhook.test.mjs,location-pin.test.mjs) —put()records everything intottlLogand the case asserts on the log afterwards.
2. Global stubs: install, then restore in finally
Node has no DOM, no navigator, no document. Suites stub them per case and always restore:
function stubNavigator(geolocationOrNull) {
const original = Object.getOwnPropertyDescriptor(globalThis, "navigator");
Object.defineProperty(globalThis, "navigator", { value: /* … */, configurable: true, writable: true });
return () => {
if (original) Object.defineProperty(globalThis, "navigator", original);
else delete globalThis.navigator;
};
}
// in the case
const restoreNav = stubNavigator({ geolocation: true });
try {
// …
} finally {
restoreNav();
}
web/test/theme.test.mjs does the same for globalThis.document (installDocumentStub() returns the dataset object, which is the assertion target). The headers are explicit about why: "a leaked stub would silently make a later pass/fail for the wrong reason."
web/test/location-chain.test.mjs also stubs globalThis.fetch. bot/test/location-pin.test.mjs patches a different layer entirely — grammY's node shim reads node-fetch's cached default export, so createRequire reaches node-fetch and swaps .default for the duration of the case.
3. Source-introspection gates ("grep the shipped source")
Structural claims — "no menu surface exists", "copy lives only in strings.ts", "nothing calls setStyle" — are pinned by reading the file, not by mocking behaviour. This is the most distinctive pattern in the repo and it appears in every workspace:
bot/test/commands.test.mjsCS4 — asserts/keyboard/idoes not appear in any command source.bot/test/privacy.test.mjsPG1 — every line containing.put(must also containexpirationTtlreferencing aSCREAMING_CASEconstant; PG2 — no persistence-shaped export names exist.bot/test/guidance-cron.test.mjsGC6 — realmakeStreetAtover the mini-graph; GC8 — structural: noconsolein the cron.web/test/copy-gate.test.mjsCOPY5 — no shell module inlines user-facing copy.web/test/no-storage.test.mjsGEO3 — no banned storage token appears inweb/srccode, plus no service-worker registration exists.web/test/session.test.mjsSESS4,web/test/theme.test.mjsPX02 case 3,web/test/guidance-surface.test.mjsGS,web/test/route-surface.test.mjsRS,web/test/fullscreen.test.mjsFS,web/test/homescreen.test.mjsHS — same shape at larger scale.
Comment-stripping is now the default in both workspaces. The two-replace prelude — strip block comments, then line comments without breaking https:// URLs — is repeated verbatim in web/test/ gates and in the newer bot/test/ gates (cari.test.mjs, route-privacy.test.mjs, guidance-cron.test.mjs). The reason is documented in no-storage.test.mjs: web/src/mock/telegram-mock.ts documents the storage ban in prose, and prose is not a call site. The original three bot gates (commands.test.mjs CS4, privacy.test.mjs PG1/PG2) still read raw readFileSync(file, "utf8") and match lines directly — their targets carry no self-describing prose.
const code = src
.replace(/\/\*[\s\S]*?\*\//g, " ") // block comments
.replace(/(^|[^:\\])\/\/[^\n]*/g, "$1"); // line comments (skips URLs)
4. Build-artifact gates (assert on the shipped bundle)
web/test/build-artifact.test.mjs reads web/dist — the production build only, never web/dist-harness/ — and proves what actually ships:
| Case | Assertion |
|---|---|
| BA1 | No emitted .js/.css references a third-party CDN host |
| BA2 | At least one self-hosted .woff2 was copied through |
| BA3 | A hashed MapLibre worker chunk exists under dist/assets |
| BA4 | The built JS carries the inlined Rams style JSON |
| BA5 | installMockWebApp / __ARAHARAH_MOCK__ appear in no emitted .js or .html |
| BA6 | No emitted HTML carries the runner page's fetch stubs |
| BA7 | The shipped bundle carries EVERY gazetteer id (the bot/web id-set parity gate) |
| BA8 | The shipped bundle carries NO deep-link crypto |
| BA9 | The route payload fits the 200 KiB gz bundle budget — measured and printed |
| BA10 | The shipped bundle carries EVERY committed pairId (BA7's fixture-parity analog) |
These cases require web/dist to exist. Each one starts with assert.ok(existsSync(DIST), claimBuildFirst().message), which yields:
run `npm run build -w theme && npm run build -w web` before this suite
The suite's header is emphatic about why BA5 scans HTML as well as JS: web/index.html is one entry shared by both builds, so a JS-only scan would miss an unguarded profile block entirely.
Two more conventions worth copying
Lazy module imports for RED phases. bot/test/initdata.test.mjs wraps its import in loadValidator() so a missing module produces one FAIL per case rather than a lost suite:
let validatorPromise = null;
function loadValidator() {
if (!validatorPromise) validatorPromise = import("../src/security/initdata.ts").then((m) => m.validateInitData);
return validatorPromise;
}
The search/ and routing/ suites use the same lazy-import shape (score.test.mjs, astar.test.mjs, both corpus.test.mjs files).
Self-checks that pin the test's own primitives. initdata.test.mjs T7 asserts node:crypto's HMAC output equals the RFC-4231 test-case-1 digest, so a broken signing path cannot produce false-green signing tests.
Anti-patterns to avoid
- Letting a case reach the live Bot API.
bot/test/webhook.test.mjsdocuments the constraint at length: grammY's cloudflare-mod adapter callsbot.init()(a livegetMe) before its own secret-token compare, so no committed case may let an update reach the framework with an armed secret. Armed-secret behaviour is verified by live smokes, not by the suite. - Writing fixtures into the repo.
theme/scripts/audit.test.mjscreates everything under/tmp/opencode/audit-test/; the bot/web suites keep fixtures in-process entirely (the two committed fixture files undersearch/test/fixtures/androuting/test/fixtures/are the deliberate exceptions — they hold pinned public data, never runtime state). - Adding a test framework. The zero-dependency property is load-bearing (decision
D-12) and repeated across all runners.
Coverage requirements
No coverage threshold is configured. There is no coverageThreshold, no nyc/c8 config, and no instrumentation anywhere — because there is no framework to instrument with.
What substitutes for coverage measurement:
| Mechanism | Where |
|---|---|
| Existence assertions on the scanned tree | no-storage.test.mjs asserts files.length >= 12 before scanning ("scanned a real tree"), so the gate cannot pass vacuously on an empty directory. copy-gate.test.mjs COPY5 asserts files.length >= 1. |
| Fixture self-checks | theme/scripts/audit.test.mjs aborts with fixture setup: … not found if theme/src/tokens.css loses a key the fixtures depend on. routing/test/fixtures/build-mini-graph.mjs regenerates mini-graph.bin so the graph cases test a real decode path. |
| Golden-corpus rate gates | search/test/corpus.test.mjs CO1–CO8 measures top-3 hit rate and refusal margin over the committed corpus; routing/test/corpus.test.mjs RT1–RT10 does the same for route fixtures. |
| Typecheck as a structural gate | bot:typecheck / web:typecheck / search:typecheck / routing:typecheck catch what the suites would not: a type change in a shape the runtime assertions never inspect. |
If you need coverage numbers, you would have to add tooling — which contradicts the current design. Prefer adding a case.
Visual capture workflow
Two Playwright scripts generate screenshots. They are evidence generators, not tests — they are not part of any npm test command and nothing in the suites depends on them. Both share the same shape: a docroot-guarded static server on 127.0.0.1:0, a Playwright chromium launch, one MIN_BYTES = 10240 blank bar per PNG, a FAILED/MISSING/UNDERSIZED report, and exit(1) if anything is unusable.
npm run capture # → theme/scripts/screenshot.mjs → theme/screenshots/
npm run web:capture # → web/scripts/capture.mjs → web/screenshots/
Both output directories are gitignored — they are regenerated artifacts, never committed.
theme/scripts/screenshot.mjs
Serves theme/ as docroot, then captures:
| Output | Notes |
|---|---|
sheet-full.png |
Component sheet at 390px, full page, after document.fonts.ready |
mockup-<screen>.png ×4 |
The four mockup screens over file:// — CDN Tailwind, so network required. On failure, the committed screen.png is copied in as fallback and logged as FALLBACK |
basemap-kl.png |
1280×800, waits on the page's own RAMS-BASEMAP-READY console signal + 2s tile settle |
The mockups are read-only reference (decision D-06) and are never edited.
web/scripts/capture.mjs — the 22-leg harness
Rebuilds all three bundles first, then serves web/dist-harness/, not web/dist:
npm run build -w theme && npm run build -w web && npm run build:harness -w web
It then asserts in both directions before launching the browser — the harness bundle must contain installMockWebApp, and web/dist must not — and fails the run if either is wrong, because a wrong bundle means the capture would race forever.
Every leg races console state tokens (RAMS-BASEMAP-READY, ARAHARAH-LIVE, ARAHARAH-DEST, ARAHARAH-ROUTE, ARAHARAH-GUIDE) or waits on a DOM state (denied lamp, expected body text). A timeout is always a hard failure with a diagnostic naming which signals fired — never a silent skip, never a blank-but-passing shot. Wait modes: immediate, settle400, live, denied, dest, route, guide.
The 22 legs (all under web/screenshots/):
| Output | What it pins |
|---|---|
shell-boot-1.png |
boot stage 1 — validating-session card over live map |
shell-boot-2.png |
boot stage 2 — acquiring-location card |
shell-live-light.png |
LIVE state, Telegram light theme |
shell-live-dark.png |
LIVE state, Telegram dark theme (PX-02) |
shell-denied.png |
denied card + ASK VIA CHAT (GEO-02 entry point) |
shell-chat-pin.png |
LIVE via the chat-pin relay (?pin=1, stubbed /api/location) |
shell-dest-preload.png |
Mini App booted WITH a preloaded destination (?dest=<id>) |
shell-route-fixture.png |
route drawn from the demo pair's pinned origin (?fixpair=) |
shell-route-alt.png |
alternative selected via chip tap (ROUTE {n}/{N} re-render) |
shell-route-pending.png |
the honest ROUTE // ROUTER pending card (D-03) |
shell-route-dark.png |
route drawn, Telegram dark theme (PX-02) |
gallery-compare.png |
DSGN-02 side-by-side vs navigate_map_overview/screen.png |
route-gallery-compare.png |
DSGN-02 route fidelity artifact |
shell-guidance-active.png |
the LIVE cockpit over a replayed drive (?drive=) |
shell-guidance-list.png |
the banner tap's expanded list + NEXT row lamp |
shell-guidance-rerouting.png |
captured INSIDE the REROUTING window, no red |
shell-guidance-confirm.png |
the abandon confirm card, bottom register |
shell-guidance-tripend.png |
the trip-ended card, chrome cleared |
shell-guidance-dark.png |
the cockpit, Telegram dark theme (PX-02) |
guidance-gallery-compare.png |
DSGN-02 guidance fidelity artifact vs active_route_guidance/screen.png |
shell-guidance-landscape.png |
the cockpit in landscape (driving posture) |
shell-route-pending-landscape.png |
the designed pending card in landscape |
Success prints CAPTURE OK — 22/22 non-blank captures under web/screenshots/.
Three capture legs double as runtime verification:
- The theme honesty check — the
live/route/guidethemed legs readdocument.documentElement.dataset.tgThemeafter the console signal and throw if it does not equal the requestedcolorScheme. A themed screenshot cannot silently show the wrong theme. - The GEO-03 runtime probe — after boot-to-live,
page.evaluatereadslocalStorage.length,sessionStoragekeys,document.cookie, andindexedDB.databases(), and fails the run on any non-empty app storage surface. One allowlisted exception: the officialtelegram-web-app.jsSDK writes__telegram__initParams, which is the SDK's own launch bookkeeping and is not app data. Any other key fails. - The D-15 coordinate-hygiene scan — the guidance legs scan the WHOLE leg's console output (an unbounded capture buffer) for leaked coordinates; a coordinate in a console line fails the run.
Repo gate CLIs (privacy-audit / budget-audit)
Two repo-root CLIs are maintainer-time audits, each with its own TDD suite:
| CLI | Suite | What it proves |
|---|---|---|
npm run privacy-audit |
scripts/test/privacy-audit.test.mjs |
storage-api, kv-ttl, log-vocabulary, runtime-hosts, bundle-telemetry gates — plus two --live probes |
npm run budget-audit |
scripts/test/budget-audit.test.mjs |
route-cpu, bundle-budget, kv-writes, subrequests, golden-corpus gates — plus one --live probe |
Both follow one exit-code contract — no silent passes:
| Exit | Meaning |
|---|---|
0 |
clean — every required gate PASSed |
1 |
violations — each printed as a per-gate evidence line |
2 |
NOT RUN — environment/credential drift (a --live probe could not run because credentials are absent, or the build/graph is absent). A NOT RUN is never a pass. |
The --live protocol matters: without --live, the live probes print as NOT RUN with a (pass --live) hint (flag-skip, excluded from exit-2 — the operator's explicit choice). With --live but missing credentials, the run exits 2 with PRIVACY AUDIT NOT RUN — a skipped probe is never mistaken for a pass. Credentials come from process.env first, then bot/.dev.vars — see CONFIGURATION.
The gate suites themselves run directly (no npm script — they are maintainer tools):
node scripts/test/privacy-audit.test.mjs # SA/BG/CP/AU vectors → …TEST CLEAN
node scripts/test/budget-audit.test.mjs # CPU/BA/KV/SQ/CH/GC vectors → …vectors ok
Both are self-contained runners: they collect failures, print FAIL per vector, tally at the end, and process.exit(1) if any failed. They use the PRIVACY_AUDIT_ROOT env var as a repo-root override for fork checkouts.
CI integration
No CI/CD pipeline is detected. There is no .github/workflows/, no .gitea/workflows/, and no other workflow definition in the repository.
The full green check a contributor must run by hand before pushing:
npm run bot:test
npm run web:test
npm run search:test
npm run routing:test
npm test -w theme
npm run bot:typecheck
npm run web:typecheck
npm run search:typecheck
npm run routing:typecheck
npm run audit # token-drift audit on the real dist/rams.css
npm run privacy-audit # the five static privacy gates
npm run budget-audit # the five measured budget gates
npm run build # required before web:test's BA cases can pass
Order matters in one place: web/test/build-artifact.test.mjs reads web/dist, so npm run build must run before npm run web:test or BA1–BA10 all fail with the build-first message. budget-audit's route-cpu gate also needs the committed graph binary (routing/data/kl-roadgraph.v1.bin) and the corpus gates re-run search:corpus + routing:corpus, which read the committed data files.
Quick reference
| I want to… | Command |
|---|---|
| Run every bot test | npm run bot:test |
| Run every Mini App test | npm run web:test |
| Run every search test | npm run search:test |
| Run every routing test | npm run routing:test |
| Run the theme audit tests | npm test -w theme |
| Typecheck the Worker | npm run bot:typecheck |
| Typecheck the Mini App | npm run web:typecheck |
| Typecheck search/routing | npm run search:typecheck / npm run routing:typecheck |
| Audit the real compiled CSS | npm run audit |
| Run the privacy gate CLI | npm run privacy-audit (add -- --live for the live probes) |
| Run the budget gate CLI | npm run budget-audit (add -- --live for the live cache-header probe) |
| Re-run the golden corpora | npm run search:corpus / npm run routing:corpus |
| Add a test | new <workspace>/test/<subject>.test.mjs — picked up automatically |
| Regenerate design screenshots | npm run capture |
| Regenerate Mini App screenshots | npm run web:capture |
Related docs: CONFIGURATION for the secrets the bot: tests substitute with 12345:TESTTOKEN · PRIVACY-AUDIT for what each privacy gate inspects · BUDGETS for the measured budget record the budget CLI prints · SEARCH-QUALITY for the corpus metrics the CO cases pin · ARCHITECTURE for why the Worker/Mini App split is what it is.
arah²
Packages
Repo
- Source tree
- API — API
- Deployment — Deployment
- Contributing — Contributing
- Core concepts — Core-Concepts