Mutable state for one test run, kept separate from the resolved settings on Config.
Organized by sharing lifetime, and that organization is the invariant to preserve:
- Every field except
groupis shared by reference across all concurrent groups. Groups are spread off the parent with a shallow{...config}, which copiesstateby reference. So the counter, failure sets, coverage collector and reporters are one set of objects for the whole run — which is what makes TAP numbering globally sequential and the coverage report whole. groupis replaced per group. It is the only place a per-group slot may live; anything added elsewhere silently becomes shared.
The consequence for shared fields is that they must be mutated in place, never reassigned —
see RunState.reset. Assigning a fresh object on one config detaches it from the others and
splits the run's totals, which no type can catch. Fields of a shared object may be reassigned
freely (results.coverage = new Map()); it is the object itself that must survive.
import * as RunState from './setup/run-state.ts'; const state = RunState.create(); state.results.counter.total; // 0 — one shared accumulator for the whole run
Whole-run accumulators, shared by reference across every concurrent group.
Active reporter instances for this run, built by Reporters.create in Config.setup.
One set for the whole run, so a stateful reporter sees every group rather than one slice.
Where this run's text goes: the TAP document, every reporter line, every # diagnostic and
every forwarded page log. processConsole for the CLI, silentConsole for a programmatic run that
only wants the result value. Shared by reference across concurrent groups.
Named console, not output: config.output is the build DIRECTORY, and the two were one
word apart on the same object graph.
signal: AbortSignal
Cancels this run when it fires, or absent when nothing can. Carried here rather than threaded
alongside the config because it is per-run input like everything else in state — and it is
what lets a verb reach it from the resolved config instead of re-reading the raw arguments.
Only carried: Config.setup never subscribes. Whoever wires a listener has to unwire it, and
setup has no teardown, so a shared controller would accumulate one listener per run.
daemon: DaemonState | null
Non-null exactly when this run is executing inside the persistent daemon process — it is the daemon-mode flag as well as the handles. Daemon runs reuse the shared browser, suppress the per-connection TAP header, and leave that browser open at the end of the run.
groupCount: number
Number of concurrent groups in this run; 1 for watch and single-group runs.
How this run's files were split across those groups — the descriptive record RunResult
reports. Assigned once per run alongside groupCount and shared by reference, so a group
can read the whole split rather than only its own slice.
Empty until a split is computed, which is why groupCount stays the authority the runner
branches on: page reuse is decided during daemon setup, before any grouping exists. Watch
never splits at all, so it leaves this empty and the result names its one group from the
files that rerun ran.
aborters: Set<() => void>
One callback per live server that tells its connected pages to drop the rest of the QUnit
queue — what qq, session.abort() and an aborted signal all go through.
A set rather than a slot because a concurrent run may have one server per group, and aborting half a run is not aborting it. Shared by reference across groups, so a group registering its own server marks it for everyone. Entries for closed servers are harmless: publishing to a server with no clients reaches nobody.
File-watcher build bookkeeping. Only meaningful in watch mode, where there is one group.
HTML fixtures and their referenced assets, resolved once by buildCachedContent before any
group config is spread off. Frozen from that point on, so all groups share one copy.
RunState for this group only. The group spread replaces this object (everything else in
RunState is shared by reference), so it is the one place per-group slots may live.