The Node SDK collects per-test code coverage during Tusk Drift replay using V8's built-in precise coverage. No external dependencies like NYC or c8 are needed.
When coverage is enabled (via --show-coverage, --coverage-output, or coverage.enabled: true in config), the CLI sets NODE_V8_COVERAGE=<temp-dir>. This tells V8 to enable precise coverage collection internally:
V8 internally calls: Profiler.startPreciseCoverage({ callCount: true, detailed: true })
This provides:
- Real execution counts (1, 2, 5...) not just binary covered/uncovered
- Block-level granularity: branches, loops, expressions
- Zero external dependencies — works with any Node.js version that supports
NODE_V8_COVERAGE - Works with CJS, ESM, TypeScript, bundled code — anything V8 executes
-
Baseline: After the service starts, the CLI sends a
CoverageSnapshotRequest(baseline=true). The SDK callsv8.takeCoverage(), which writes a JSON file to theNODE_V8_COVERAGEdirectory and resets all counters. The baseline captures all coverable lines (including uncovered at count=0) for the coverage denominator. -
Per-test: After each test, the CLI sends
CoverageSnapshotRequest(baseline=false). The SDK callsv8.takeCoverage()again. Because counters were reset, the result contains only lines executed by this specific test — no diffing needed. -
Processing: The SDK processes the V8 JSON using
ast-v8-to-istanbul, which converts V8 byte ranges into Istanbul-format line/branch coverage. The result is sent back to the CLI via protobuf.
After v8.takeCoverage() resets counters, V8 only reports functions that were called since the reset. Functions that were never called are absent from the V8 output.
- v8-to-istanbul assumes complete V8 data. Missing functions are treated as "covered by default." This produces 100% coverage for files where only
/healthwas hit. - ast-v8-to-istanbul parses the source file's AST independently. It knows about ALL functions from the AST and correctly marks missing ones as uncovered.
This is the key reason we use ast-v8-to-istanbul.
For TypeScript projects using tsc, the SDK automatically:
- Detects
//# sourceMappingURL=<file>.mapcomments in compiled JS - Loads the
.mapfile - Fixes
sourceRootif present (TypeScript setssourceRoot: "/"which breaks ast-v8-to-istanbul's internal path resolution — the SDK resolves sources relative to the actual project root) - Strips the
sourceMappingURLcomment from code passed to ast-v8-to-istanbul (prevents it from loading the unpatched.mapfile) - Passes the fixed source map to ast-v8-to-istanbul, which remaps coverage to original
.tsfiles
Requirements: sourceMap: true in tsconfig.json. Source map files (.js.map) must be present alongside compiled output.
Supported setups:
| Setup | Status |
|---|---|
tsc -> node dist/ |
Works (tested) |
swc/esbuild (compile, not bundle) -> node dist/ |
Should work (same .js + .js.map pattern) |
ts-node with TS_NODE_EMIT=true |
Works (CLI sets this automatically) |
ts-node-dev |
Limited — lazy compilation means only executed files have coverage |
| Bundled (webpack/esbuild/Rollup) | Untested — should work if source maps are produced |
The start command in .tusk/config.yaml often chains processes:
rm -rf dist && npm run build && node dist/server.js
This creates multiple Node processes (npm, tsc, the server), all inheriting NODE_V8_COVERAGE. Each writes its own V8 coverage file. The SDK handles this by quick-scanning each file to check for user scripts before running the expensive ast-v8-to-istanbul processing. Files from npm/tsc (which have 0 user scripts) are skipped.
The SDK tries parsing source code as CJS (sourceType: "script") first, falling back to ESM (sourceType: "module") if that fails. This handles both module formats without configuration.
These are set automatically by the CLI when coverage is enabled. You should not set them manually.
| Variable | Description |
|---|---|
NODE_V8_COVERAGE |
Directory for V8 to write coverage JSON files. Set by CLI. |
TUSK_COVERAGE |
Language-agnostic signal that coverage is enabled. Set by CLI. |
TS_NODE_EMIT |
Forces ts-node to write compiled JS to disk (needed for coverage processing). Set by CLI. |
- acorn parse failures:
ast-v8-to-istanbuluses acorn to parse JavaScript. Files using syntax acorn doesn't support (stage 3 proposals, certain decorator patterns) are silently skipped. - Stale
dist/artifacts:tscdoesn't clean old output files. If a source file was renamed or moved, the old compiled file remains indist/and may have broken imports. Userm -rf distbeforetscin your start command. - Multi-process apps: If your app uses Node's cluster module or PM2 to fork workers, each worker is a separate process. Only the worker connected to the CLI's protobuf channel handles coverage requests.
- Dynamic imports: Modules loaded via dynamic
import()after startup aren't in the baseline snapshot. Their uncovered functions won't be in the denominator. - Large codebases: Processing 500+ files from a 24MB V8 JSON takes several seconds. The CLI has a 30-second timeout for coverage snapshots.
- ts-node-dev: Lazy compilation means only files accessed during the test are compiled and covered. Pre-compiled
tscoutput gives much better coverage.