# Testing `avril test` discovers test files, runs each in a fresh isolate, and reports one human-readable summary. There is no bench mode, no JSON reporter, and no `Deno` global: tests import their API from `avril:test`. ```ts import { test } from "avril:test"; test("adds", () => { if (1 + 1 !== 2) throw new Error("math broke"); }); test("steps nest", async (t) => { await t.step("one", () => {}); await t.step("two", async (t) => { await t.step("deeper", () => {}); }); }); ``` ```sh avril test avril test tests/ ./extra_test.ts avril test --filter=adds avril test --no-run avril test -0 --allow-net=example.test --allow-read=./fixtures/ ``` Permission, package (`--offline`, `--frozen`, `--jsr-registry`, `--cache-dir`, `--lock`, `--vendor`), `--include`, and `--env-file` options behave exactly as for `avril run`. Each file loads with a child of the CLI capabilities plus its own packages, store, and graph, so a `pledge()` in one file never narrows another; each discovered file gets an implicit js-exec grant like an entry module, so `avril test -0 ` works without extra flags. ## Discovery With no arguments, the working directory is walked. Explicit directories are walked; explicit files run even when their names match nothing; explicit `http(s)` URLs run as remote modules. Walked files must look like tests: `test.`, `*_test.`, or `*.test.`, where `` is one of `js`, `mjs`, `cjs`, `cts`, `jsx`, `ts`, `mts`, `tsx`. (`d.ts` files are excluded because they have no runtime code.) Dot-directories, `node_modules`, `vendor`, `target`, and `__pycache__` are skipped, as are dot-files; symlinks are never followed while walking (an explicit symlinked directory walks its target), and results are sorted and deduplicated. An empty result is an error (`No test files found`). ## API ```ts test(name, fn); test({ name, fn?, ignore?, only?, timeout? }); test.only(name, fn); test.only(options); test.ignore(name, fn); test.ignore(options); ``` - `fn` receives a `TestContext` and may be sync or async. It is required unless `ignore` is true (`test.ignore('name')` needs no function); unknown option keys throw at registration, which fails the file, as does calling `test()` after collection (for example inside a test body). - `timeout` is milliseconds before the test fails (default `5000`). `timeout: 0` means the body must settle synchronously; values past `2147483647` are rejected at registration (timers truncate to 32 bits); `Infinity` disables the per-test timeout, but the file-level 60s progress budget below still applies. The timeout covers the body plus all of its steps, including steps the body never awaited. - When any test in a file sets `only`, every other test in that file is filtered out. Filtered-out tests never fail the run: a file whose focused tests pass exits 0. `--filter=PATTERN` keeps only tests whose top-level name contains the substring; steps inherit their test's selection, and filtered tests are counted, not run. - `t.step(name, fn)` / `t.step({ name, fn?, ignore? })` runs a substep sequentially in call order and reports it indented. It resolves `true` when the step passed and `false` when it was ignored or failed — a failing step never throws. Any failed step still fails its ancestors up to the test, which reports `due to N failed step(s)` when its own body passed. Steps share their test's timeout; step-level `only` and `timeout` are rejected. Starting a step while another runs on the same context fails the newcomer (`Started step ... while another step was running`) without failing its victim; steps must be awaited one at a time. The parent waits out steps its body never awaited, and steps completing after their test settled (a timed-out body waking up) are dropped, never reported under a later test. Only top-level tests count toward file totals; steps are detail. - There is deliberately no `(name, options, fn)` third form and no compat shim beyond this surface. ## Isolation and authority Each file runs in its own fresh isolate, sequentially, in registration order. Global state is shared between tests in one file but never leaks across files. Each file also gets a child of the CLI capabilities with its own packages, store, and graph: a `pledge()` call inside a test narrows its whole file isolate irreversibly — including that file's own loading, exactly as under `avril run` — while later files still load and run under the un-narrowed parent, so tests after it in the same file inherit the restriction; prefer CLI flags for authority. There are no resource, timer, or op sanitizers: a test that leaves an interval running still passes, and the harness does not wait for idle handles before moving to the next file. Rejections nobody handles during the run are collected under the running test and fail the file with the full list instead of aborting silently; strays surfacing after the run resolves are not observed. ## Reporter and exit codes ``` Check ./math_test.ts ok - adds (0ms) FAILED - subtracts (1ms) Error: boom at Object.fn (/tmp/proj/math_test.ts:18:29) ignored - skipped ok - ./math_test.ts (1 passed, 1 failed, 1 ignored, 0 filtered) test result: FAILED. 1 passed; 1 failed; 1 ignored; 0 filtered; 1 file. ``` Failure stacks use source-mapped locations with the runner's own frames trimmed. Files that fail to prepare or evaluate (syntax errors, import failures, registration `TypeError`s) print their detail in the file footer. Guest `console` output passes through without per-test capture. A test that spins synchronously past its timeout never yields to the runner's own timers, so a watchdog thread terminates the isolate (own timeout plus a small slack, or the file budget for `Infinity`): the spinning test reports a timeout and the rest of its file fails with it. A file that makes no progress at all for 60s — top-level evaluation or a stall between tests — is terminated with that message in its footer. The exit code is nonzero when any test fails or any file errors. `--no-run` still evaluates each file's top level to collect registrations, then lists `test - name` entries instead of running them.