Configuration
SpecTracer follows the ESLint convention: the CLI takes no flags, and every knob lives in one JSON file you commit to the repo. This page documents every key.
CLI surface
# auto-discover spectracer.config.json in the current directory
spec-tracer
# or point at a differently named / located config
spec-tracer path/to/other-config.json
One command, one optional positional argument. No sub-commands, no flags, no environment variables.
./spectracer.config.json) or a bare filename. A leading
.\ in PowerShell style is mangled by POSIX-style shells such as Git Bash and WSL,
where backslash is the escape character.
Exit codes
| Code | Meaning | When |
|---|---|---|
0 | Success | Report generated, nothing gating. |
1 | Failure | Config error, parse error, error_on_failure: true with at least one failing test, or a fail_on health check in a red state. |
Config discovery
- With no argument, SpecTracer looks for
spectracer.config.jsonin the current working directory. - If it isn't found and none was specified, the tool errors out. A config file is mandatory.
- All relative paths inside the config resolve relative to that config file's own directory — so running from a subdirectory with an explicit path still works.
Key reference
{
"features": ["./features"],
"unit": {
"": ["./reports/unit.xml"],
"billing": ["./reports/billing-unit.xml"]
},
"integration": {
"": ["./reports/integration.xml"]
},
"e2e": {
"": ["./reports/e2e.json"],
"checkout": ["./reports/checkout-e2e.json"]
},
"output": "./report.html",
"output_json": "./report.json",
"error_on_failure": false,
"fail_on": ["pyramid", "e2e_runtime"],
"health_checks": {
"progress_threshold_green": 80,
"progress_threshold_amber": 50,
"e2e_duration_amber_seconds": 600,
"e2e_duration_red_seconds": 1800
}
}
| Key | Required | Description |
|---|---|---|
features | Yes | Array of Gherkin .feature file or directory paths. Directories are searched recursively. |
unit | No | Object keyed by module name; each value an array of JUnit XML and/or Cucumber JSON file or directory paths — format is auto-detected per file. Use "" for results not tied to a module. Matched against @require-unit / @require-unit:<module>. |
integration | No | Same shape as unit (JUnit XML and/or Cucumber JSON, auto-detected), matched against @require-integration. |
e2e | No | Same shape again, but the paths are Cucumber JSON. Matched against @require-e2e. |
output | Yes | Path for the generated HTML report. Parent directories are created if missing; an existing file is overwritten. |
output_json | No | Path for the machine-readable JSON report. Omit to skip JSON output entirely (the default). Same create/overwrite semantics as output. |
error_on_failure | No | If true, exit 1 when any collected test result is a failure. Default false. |
fail_on | No | Array of health checks that gate CI. Accepted values: "progress", "pyramid", "e2e_runtime". Any other value is a config error. Default: none. |
health_checks | No | Threshold overrides. See Health checks. |
Layer objects, in detail
The three layer keys share one shape, and it's the part of the config worth understanding properly:
"<layer>": {
"<module name>": [ "<file or directory>", ... ],
...
}
- The key is a module name. It's stamped onto every result parsed out of the
files listed under it, and it's what
@require-unit:billingmatches against. ""means unscoped. Results under it satisfy bare@require-unitbut never@require-unit:anything.- Values may be files or directories. E2E directories are scanned recursively for
.json. Unit/integration directories are scanned recursively for both.xmland.json— so a module can be JUnit-based, Cucumber-based (BDD-style scenarios tagged@scenario:xxx), or a mix of both. Individual files are read regardless of extension, with format sniffed from content when the extension is ambiguous. - Empty or missing paths are silently ignored — that layer simply contributes zero tests. This keeps a partially-wired pipeline from blowing up, but it also means a typo'd path looks exactly like "no tests yet". Check the pyramid counts if a layer seems empty.
- Omit a layer entirely if you don't use it. An absent
integrationkey is fine.
A JUnit XML file that fails to parse aborts the run with an error naming the offending file.
A Cucumber JSON file that fails to parse aborts on the JSON decode error — note that this message is not contextualised with the file path, so if you have several Cucumber JSON files (E2E, or BDD-style unit/integration) you may have to bisect.
Health checks
Four checks run on every report. By default they're purely visual — they colour the dashboard and
nothing else. Listing a check under fail_on promotes it to a CI gate.
| Check | Measures | Pass | Warn | Fail |
|---|---|---|---|---|
Progressprogress |
% of declared @require-* layer/module pairs that have a linked result |
≥ green threshold | ≥ amber threshold | < amber threshold |
Pyramid ratiopyramid |
Unit test count vs. integration + E2E count | Unit > I+E | Unit = I+E | Unit < I+E (inverted) |
E2E runtimee2e_runtime |
Total E2E duration in seconds | ≤ amber seconds | ≤ red seconds | > red seconds |
| Unlinked tests visual only |
Number of orphaned test results | 0 | 1–3 | 4+ |
Default thresholds
| Setting | Default | Meaning |
|---|---|---|
progress_threshold_green | 80 | At or above this percentage, Progress is green. |
progress_threshold_amber | 50 | At or above this, amber; below it, red. |
e2e_duration_amber_seconds | 600 | Ten minutes. Past this, E2E runtime goes amber. |
e2e_duration_red_seconds | 1800 | Thirty minutes. Past this, red. |
Override only what you need — unspecified keys keep their defaults.
CI gating
Two independent mechanisms, and either one exiting non-zero fails the build.
error_on_failure
Answers one question: did any test fail? Set it to true and a single
failing result exits 1. Nothing to do with coverage.
fail_on
Answers a different question: are we living up to our own standards? List
progress, pyramid, and/or e2e_runtime; a red status on any
listed check exits 1.
Need a threshold the built-in checks don't cover — say, "completion must not drop below last
week's" — script it against output_json rather than parsing HTML. See
CI/CD recipes.
The JSON report
Setting output_json writes a second file alongside the HTML, built from the exact
same internal data model — so the two outputs cannot drift apart. It conforms to
spectracer-report.schema.json (JSON Schema Draft 7), which is the authoritative
contract.
| Path | Contents |
|---|---|
summary.completion |
The headline stats. Note percent is the declared tests matched percentage (satisfied / required) — tested and total are scenario counts, so percent is not tested / total. |
summary.pyramid | Per-layer test counts, durations, and pass rates. |
summary.health | A single worst-of rollup: green / amber / red, plus a reasons[] array carrying each non-passing check's message. |
features[].scenarios[].results[] | Every linked result, with module, and duration (milliseconds) and failureMessage omitted rather than null when unavailable. Requirement satisfaction sits under requirements[]. |
unlinkedTests[] | The same orphaned results the HTML report lists. |
config | A verbatim echo of the resolved config, for provenance if the JSON is archived away from the repo. |
Worked examples
Minimal — E2E only
The smallest thing that produces a useful report. Every scenario without an explicit
@require-* defaults to @require-e2e, so this needs no tagging beyond
@id: / @scenario:.
{
"features": ["./features"],
"e2e": { "": ["./reports/e2e.json"] },
"output": "./reports/spectracer-report.html"
}
Full stack — three layers, JSON twin, gated
{
"features": ["./features"],
"unit": { "": ["./reports/unit"] },
"integration": { "": ["./reports/integration"] },
"e2e": { "": ["./reports/e2e"] },
"output": "./reports/spectracer-report.html",
"output_json": "./reports/spectracer-report.json",
"error_on_failure": true,
"fail_on": ["pyramid", "e2e_runtime"]
}
The layer paths here are directories — every XML under ./reports/unit is
picked up, so CI jobs can drop files in without the config changing.
Multi-module monorepo with custom thresholds
{
"features": ["./services/auth/features", "./services/billing/features"],
"unit": {
"auth": ["./services/auth/reports/unit.xml"],
"billing": ["./services/billing/reports/unit.xml"]
},
"integration": {
"auth": ["./services/auth/reports/int.xml"],
"billing": ["./services/billing/reports/int.xml"]
},
"e2e": {
"checkout": ["./reports/e2e-checkout.json"],
"signup": ["./reports/e2e-signup.json"]
},
"output": "./reports/spectracer-report.html",
"output_json": "./reports/spectracer-report.json",
"fail_on": ["progress", "pyramid"],
"health_checks": {
"progress_threshold_green": 90,
"progress_threshold_amber": 70,
"e2e_duration_amber_seconds": 300,
"e2e_duration_red_seconds": 900
}
}
Scenarios in this repo would tag @require-unit:auth,
@require-e2e:checkout, and so on. Note there is no "" key on any layer
here — every result is module-scoped, so a bare @require-unit would be satisfied by any
of them, while @require-unit:auth is satisfied only by the auth file.