unit-test-runner
JS/TS unit-test framework detector + canonical-output parser + the
detect→init→exec→parse→finalize orchestrator helper. Phase 1
T-1.3 ships scripts/detect.sh (5-fixture heuristic). Phase 1
T-1.4 ships the parser layer: scripts/parse-jest.sh,
scripts/parse-vitest.sh, scripts/parse-bun.sh, and the
scripts/parse.sh dispatcher — they take a runner's stdout and emit the
canonical run.json shape consumed by auto-test (T-1.5) and the
claude-bridge loop. Phase 1 T-1.5 adds scripts/run.sh — chains
detect → init-run → exec runner → append-log → parse → finalize-run end
to end, returns the run-dir on stdout, and translates the framework's
exit code through to the {0, 1, 2} contract documented in
auto-test/references/exit-codes.md.
When to use
Activate when a caller (skill or user) needs only the unit-test layer of a JS/TS project. Concretely:
- The user said "run the unit tests" or a paraphrase ("execute the test suite", "run npm test", "what does my test runner say?").
- A meta-skill (
auto-test) is orchestrating a multi-layer run and asks this skill for the unit layer. - A debugging session needs a quick which framework is in this repo
answer —
scripts/detect.sh <root>returns it as JSON.
Do not activate when:
- The request is about end-to-end / browser tests, dev-server-required
scenarios, screenshot capture,
tests/browser/*.scenario.md, or any Playwright run that callspage.goto(...). Those route tobrowser-test(Phase 2). Theplaywright-runnerframework keyword in this skill refers ONLY to@playwright/testused as a unit-style runner with no browser navigation. - The project is Python (pytest), Rust (Cargo), Go (
go test), Ruby (rspec), or Elixir (mix). Those route to Phase 3 multi-runtime detectors (not yet built). - The user wants to read an already-finished run log — that is a plain
Readof<project>/.test-runs/latest/run.log, not this skill. - The user wants coverage thresholds, flakiness analysis, or lint / type-check output. Different skills (or none yet).
How to use
Detect (Phase 1.3)
# Find this skill's scripts dir relative to the caller — the orchestrator
# typically passes its own SKILL_DIR.
SKILL_DIR="<...>/skills/unit-test-runner"
# Emits one-line JSON on stdout, exit 0 on success (incl. unknown), 2 on
# bad input arg. Read-only; never writes to the project tree.
DETECT_JSON="$("$SKILL_DIR/scripts/detect.sh" "$PROJECT_ROOT")"
# Example output (jest fixture):
# {"command":"npm test","framework":"jest","markers":[
# "devDep:jest","jest.config.js","script:test","script:test:contains:jest"
# ],"package_manager":"npm","project_dir":"/abs/path/to/jest","runtime":"node",
# "schema_version":"1","test_script":"jest"}
The framework field is the routing key. Possible values:
framework |
Meaning | Phase 1.4 parser |
|---|---|---|
jest |
Jest in devDeps and/or jest.config.* present |
parse-jest.sh |
vitest |
Vitest in devDeps and/or vitest.config.* present |
parse-vitest.sh |
bun |
bun:test (built into Bun runtime) — bun.lockb/bun.lock + no other framework |
parse-bun.sh |
mocha |
Mocha in devDeps and/or .mocharc* / mocha.opts present |
(Phase 4 — not in T-1.4) |
playwright-runner |
@playwright/test in devDeps and/or playwright.config.* present |
(Phase 4 — not in T-1.4; Phase 2 browser-test is the navigation case) |
unknown |
No marker matched | caller falls back to ARCHITECTURE §(e) custom CLAUDE.md path or aborts with exit 2 |
Priority (when multiple markers are present, e.g. monorepo migration):
vitest > jest > playwright-runner > mocha > bun. Documented in
references/framework-detection.md and asserted by the test suite.
Parse (Phase 1.4 — landed)
The parser layer accepts the runner's structured / text stdout and emits
the canonical run.json shape (5 top-level keys: schema_version,
framework, summary, failures, suites). See
references/parser-output-schema.md for the full contract.
# Parse a captured stdout file (works with absolute or relative paths).
parsed_json="$("$SKILL_DIR/scripts/parse.sh" "$framework" "$RUN/streams/unit.log")"
# Or stream stdin → parser:
"$command" 2>&1 | "$SKILL_DIR/scripts/parse-${framework}.sh" -
Per-framework reporter input shape:
framework |
Input format | How to capture |
|---|---|---|
jest |
Jest's --json |
jest --json |
vitest |
Vitest --reporter=json |
vitest run --reporter=json |
bun |
Default text | bun test (verbose adds per-pass lines) |
Why Jest
--jsoninstead of "TAP" (as the plan listed): Jest ships no built-in TAP reporter;--jsonis built-in, documented, stable since Jest 22, and aligns with ARCHITECTURE §2's "JSON reporter where available" preference. Documented inreferences/parser-output-schema.md.
Run end-to-end (Phase 1.5 — scripts/run.sh)
scripts/run.sh chains the entire pipeline. Usage:
# Single arg: the project root. Stdout: one line — absolute path to the
# finalized run dir under <project>/.test-runs/<UTC-ts>/.
"$SKILL_DIR/scripts/run.sh" "$PROJECT_ROOT"
# Optional override when test-log-centralizer lives elsewhere:
"$SKILL_DIR/scripts/run.sh" "$PROJECT_ROOT" --centralizer-dir "$ALT_PATH"
What it does internally (single self-contained pipeline):
detect.sh— pick framework + command (exits 2 onunknown).test-log-centralizer/init-run.sh— scaffold run dir +meta.json+summary.jsonplaceholder.- spawn the framework command from the project root, with
NO_COLOR=1 FORCE_COLOR=0 CI=1so output is deterministic; merge stdout/stderr and pipe throughappend-log.sh <RUN> unit. Preserve the framework's exit code through${PIPESTATUS[0]}. - strip the timestamp prefix
append-log.shadds, thenparse.sh <framework>over the cleaned stream → canonical run.json content. finalize-run.shwrites summary/manifest + a placeholder run.json;run.shthen merges its parsed output (summary/failures/suites) into that placeholder so the finalrun.jsonis the rich shape, not the skeleton.- exit translation: framework 0 → 0, 1 → 1, ≥ 2 (or parse failure) → 2.
The auto-test meta-skill (skills/auto-test/scripts/orchestrate.sh)
wraps this — it adds the ASCII dashboard render on top.
Examples
User: "run the unit tests" → Claude routes to auto-test, which calls this
skill's scripts/detect.sh first (then the matching parser in Phase 1.4).
User: "what test framework does this project use?" → Claude calls
scripts/detect.sh directly and reports the framework + command
fields.
User: "run the playwright e2e against the dev server" → does not
activate — that is Phase 2 browser-test.
User: "tail .test-runs/latest/run.log" → does not activate — plain
Read is sufficient.
Files
scripts/
detect.sh — T-1.3: framework + runtime + PM heuristic; emits JSON
parse.sh — T-1.4: dispatcher (jest|vitest|bun → parse-<fw>.sh)
parse-jest.sh — T-1.4: jest --json → canonical run.json
parse-vitest.sh — T-1.4: vitest --reporter=json → canonical run.json
parse-bun.sh — T-1.4: bun text → canonical run.json
run.sh — T-1.5: detect → init-run → exec → append → parse → finalize
references/
framework-detection.md — T-1.3: priority order, marker table, edge cases
parser-output-schema.md — T-1.4: canonical TestRun/run.json shape, status mapping, deviation rationale
tests/
run-all.sh — T-1.4: convenience runner (alphabetical test-*.sh)
test-detect.sh — T-1.3: 6-fixture acceptance suite (~70 assertions)
test-parsers.sh — T-1.4: 79-assertion acceptance + branch coverage
fixtures/
jest/ — package.json + jest.config.js + package-lock.json
vitest/ — package.json + vitest.config.ts + pnpm-lock.yaml
bun/ — package.json + bun.lockb stub + script "bun test"
mocha/ — package.json + .mocharc.json + yarn.lock
playwright-runner/ — package.json + playwright.config.ts + package-lock.json
unknown/ — empty (no package.json)
goldens/ — T-1.4: per-framework {input, expected.json} pairs
See also
docs/ARCHITECTURE.md§2 —unit-test-runnerskill purpose + non-overlap.docs/ARCHITECTURE.md§(e) — Detection heuristic.docs/ARCHITECTURE.md§(f) — TestRun data model (consumed by Phase 1.4).skills/test-log-centralizer/— sibling skill;init-run.sh/append-log.sh/finalize-run.share the I/O contract.references/framework-detection.md— priority + marker table reference.