---
title: "Cover"
description: "Generate tests that do not exist yet. Analyzes coverage gaps, then writes and runs new test files across three tiers (unit, integration via testcontainers, Playwright E2E), one test-generator agent per tier, healing failures for up to 3 iterations. Use when code has no tests or when raising coverage after implementation. Do NOT use to grade tests that already exist (use /ork:verify) or to run a suite without writing anything new."
canonical: "https://orchestkit.yonyon.ai/docs/reference/skills/cover"
---

# Cover

Generate tests that do not exist yet. Analyzes coverage gaps, then writes and runs new test files across three tiers (unit, integration via testcontainers, Playwright E2E), one test-generator agent per tier, healing failures for up to 3 iterations. Use when code has no tests or when raising coverage after implementation. Do NOT use to grade tests that already exist (use /ork:verify) or to run a suite without writing anything new.

<span className="badge badge-blue">Command</span> <span className="badge badge-orange">high</span>

```bash title="Invoke"
/ork:cover
```

<ContextualSkillSidebar slug="cover" />

> **Cover** Generate tests that do not exist yet. Analyzes coverage gaps, then writes and runs new test files across three tiers (unit, integration via testcontainers, Playwright E2E), one test-generator agent per tier, healing failures for up to 3 iterations. Use when code has no tests or when raising coverage after implementation.


# Cover: Test Suite Generator

Host-neutral workflow. Invoke by skill name (`cover`). Claude Code slash routing, YAML hook loaders, and `.claude/chain` live in `references/claude-code.md`.

Generate comprehensive test suites for existing code with real-service integration testing and automated failure healing.

> **Note:** If `disableSkillShellExecution` is enabled (CC 2.1.91), the precondition check for vitest/jest won't run. Verify a test runner is installed before proceeding: `npx vitest --version` or `npx jest --version`.

## Quick Start

```bash
cover authentication flow
cover --model=opus payment processing
cover --tier=unit,integration user service
cover --real-services checkout pipeline
```

## Argument Resolution

```python
SCOPE = "$ARGUMENTS"  # e.g., "authentication flow"

# Flag parsing
MODEL_OVERRIDE = None
TIERS = ["unit", "integration", "e2e"]  # default: all three
REAL_SERVICES = False

for token in "$ARGUMENTS".split():
    if token.startswith("--model="):
        MODEL_OVERRIDE = token.split("=", 1)[1]
        SCOPE = SCOPE.replace(token, "").strip()
    elif token.startswith("--tier="):
        TIERS = token.split("=", 1)[1].split(",")
        SCOPE = SCOPE.replace(token, "").strip()
    elif token == "--real-services":
        REAL_SERVICES = True
        SCOPE = SCOPE.replace(token, "").strip()
```

---

## Step -0.5: Effort-Aware Coverage Scaling (CC 2.1.76, env var since 2.1.120)

Read `$CLAUDE_EFFORT` (CC 2.1.120+) first; explicit `--effort=` token wins as override. Default `high` when CC &lt; 2.1.120 and no flag. Pattern matches assess + explore (#1540). Scale test generation depth:

| Effort Level | Tiers Generated | Agents | `maxIterations` to pass Phase 5 |
|-------------|----------------|--------|-----------------|
| **low** | Unit only | 1 agent | 2 (1 repair + 1 verify) |
| **medium** | Unit + Integration | 2 agents | 2 (1 repair + 1 verify) |
| **high** (default) | Unit + Integration + E2E | 3 agents | 3 (2 repairs + 1 verify) |
| **xhigh** (CC 2.1.111+) | Unit + Integration + E2E | 3 agents | 3 (the ceiling; xhigh adds agents, not heal passes) |

Values are what you pass as `maxIterations` in Phase 5. The script clamps to `[2, 3]`
(`heal-loop.js`), so anything outside that range is coerced, and the final iteration always
verifies rather than repairing. There is no 4-iteration mode.

> **Override:** Explicit `--tier=` flag or user selection overrides `/effort` downscaling.

## Step -1: MCP Probe + Resume Check

```python
# Probe MCPs (parallel):
# memory is alwaysLoad in .mcp.json (CC 2.1.121+, #1541). Probe below kept as fallback for older CC:
ToolSearch(query="select:mcp__memory__search_nodes")
ToolSearch(query="select:mcp__context7__resolve-library-id")

Write(".claude/chain/capabilities.json", {
  "memory": <true if found>,
  "context7": <true if found>,
  "skill": "cover",
  "timestamp": now()
})

# Resume check:
Read(".claude/chain/state.json")
# If exists and skill == "cover": resume from current_phase
# Otherwise: initialize state
```

---

## Step 0: Scope & Tier Selection

```python
AskUserQuestion(
  questions=[
    {
      "question": "What test tiers should I generate?",
      "header": "Test Tiers",
      "options": [
        {"label": "Full coverage (Recommended)", "description": "Unit + Integration (real services) + E2E"},
        {"label": "Unit + Integration", "description": "Skip E2E, focus on logic and service boundaries"},
        {"label": "Unit only", "description": "Fast isolated tests for business logic"},
        {"label": "E2E only", "description": "Playwright browser tests"}
      ],
      "multiSelect": false
    },
    {
      "question": "Healing strategy for failing tests?",
      "header": "Failure Handling",
      "options": [
        {"label": "Auto-heal (Recommended)", "description": "Fix failing tests up to 3 iterations"},
        {"label": "Generate only", "description": "Write tests, report failures, don't fix"},
        {"label": "Strict", "description": "All tests must pass or abort"}
      ],
      "multiSelect": false
    }
  ]
)
```

Override TIERS based on selection. Skip this step if `--tier=` flag was provided.

---

**Finish line.** Done means: every generated test runs, the suite is green or each remaining failure is reported with its heal-loop classification, and the coverage report is written. Follow `Read("../../shared/rules/long-run-protocol.md")`: keep going when a step needs no input from the user, stop and ask only when you can't continue without them or before anything destructive, check each subagent's evidence before accepting it, and mark anything you couldn't confirm with where you looked.

## Task Management (MANDATORY)

```python
# 1. Create main task IMMEDIATELY
TaskCreate(subject=f"Cover: {SCOPE}", description="Generate comprehensive test suite with real-service testing", activeForm=f"Generating tests for {SCOPE}")

# 2. Create subtasks for each phase
TaskCreate(subject="Discover scope and detect frameworks", activeForm="Discovering test scope")    # id=2
TaskCreate(subject="Analyze coverage gaps", activeForm="Analyzing coverage gaps")                  # id=3
TaskCreate(subject="Generate tests (parallel per tier)", activeForm="Generating tests")            # id=4
TaskCreate(subject="Execute generated tests", activeForm="Running tests")                          # id=5
TaskCreate(subject="Heal failing tests", activeForm="Healing test failures")                       # id=6
TaskCreate(subject="Generate coverage report", activeForm="Generating report")                     # id=7

# 3. Set dependencies for sequential phases
TaskUpdate(taskId="3", addBlockedBy=["2"])  # Analysis needs discovery first
TaskUpdate(taskId="4", addBlockedBy=["3"])  # Generation needs gap map
TaskUpdate(taskId="5", addBlockedBy=["4"])  # Execution needs generated tests
TaskUpdate(taskId="6", addBlockedBy=["5"])  # Healing needs test results
TaskUpdate(taskId="7", addBlockedBy=["6"])  # Report needs healed suite

# 4. Update status as you progress
TaskUpdate(taskId="2", status="in_progress")  # When starting
TaskUpdate(taskId="2", status="completed")    # When done, repeat for each subtask
```

---

## 6-Phase Workflow

| Phase | Activities | Output |
|-------|------------|--------|
| **1. Discovery** | Detect frameworks, scan scope, find untested code | Framework map, file list |
| **2. Coverage Analysis** | Run existing tests, rank risk targets per tier | Coverage baseline, risk targets with why |
| **3. Generation** | Parallel test-generator agents per tier, then behaviour gate | Test files created, gate verdicts |
| **4. Execution** | Run all generated tests | Pass/fail results |
| **5. Heal** | Fix failures, re-run (max 3 iterations) | Green test suite |
| **6. Report** | Coverage delta, test count, summary | Coverage report |

### Phase Handoffs

| After Phase | Handoff File | Key Outputs |
|-------------|-------------|-------------|
| 1. Discovery | `01-cover-discovery.json` | Frameworks, scope files, tier plan |
| 2. Analysis | `02-cover-analysis.json` | Baseline coverage, risk targets with why |
| 3. Generation | `03-cover-generation.json` | Files created, test count per tier, behaviour gate verdicts |
| 5. Heal | `05-cover-healed.json` | Final pass/fail, iterations used |

---

### Phase 1: Discovery

Detect the project's test infrastructure and scope the work.

```python
# PARALLEL, all in ONE message:
# 1. Framework detection (hook handles this, but also scan manually)
Grep(pattern="vitest|jest|mocha|playwright|cypress", glob="package.json", output_mode="content")
Grep(pattern="pytest|unittest|hypothesis", glob="pyproject.toml", output_mode="content")
Grep(pattern="pytest|unittest|hypothesis", glob="requirements*.txt", output_mode="content")

# 2. Real-service infrastructure
Glob(pattern="**/docker-compose*.yml")
Glob(pattern="**/testcontainers*")
Grep(pattern="testcontainers", glob="**/package.json", output_mode="content")
Grep(pattern="testcontainers", glob="**/requirements*.txt", output_mode="content")

# 3. Existing test structure
Glob(pattern="**/tests/**/*.test.*")
Glob(pattern="**/tests/**/*.spec.*")
Glob(pattern="**/__tests__/**/*")
Glob(pattern="**/test_*.py")

# 4. Scope files (what to test)
# If SCOPE specified, find matching source files
Grep(pattern=SCOPE, output_mode="files_with_matches")
```

**Real-service decision:**
- `docker-compose*.yml` found → integration tests use real services
- `testcontainers` in deps → use testcontainers for isolated service instances
- Neither found + `--real-services` flag → error: "No docker-compose or testcontainers found. Install testcontainers or remove --real-services flag."
- Neither found, no flag → integration tests use mocks (MSW/VCR)

Load real-service detection details: `Read("references/real-service-detection.md")`

### Phase 2: Coverage Analysis and Risk Targets

Run existing tests for a baseline, then pick targets by RISK. Coverage is information for the report, never a target.

```python
# Baseline: npx vitest run --coverage --reporter=json | pytest --cov=<scope> --cov-report=json | go test -coverprofile=coverage.out ./...
# Rank uncovered code by risk and record WHY each target was chosen:
# changed code, complex branches, error paths, security and money paths, past bugs
# gap_map[tier] = [{"target": "src/billing/refund.ts:roundRefund", "why": "money path, fixed in #812"}]
```

Output the baseline and the risk-ranked targets, each with its why, immediately (progressive output). Signals and how to find them: `Read("references/behaviour-gate.md")`.

### Phase 3: Generation (Parallel Agents)

Spawn test-generator agents per tier. Launch ALL in ONE message with `run_in_background=true`.

> **Isolation:** spawn each tier agent with `Agent(isolation="worktree")`, one
> worktree per tier (unit / integration / e2e) so they don't conflict. The
> subagent bypass of the worktree-isolation guard was fixed in CC 2.1.154 and
> completed in 2.1.203; ork's floor is >= 2.1.220, so every supported session
> gets real isolation. Do not create worktrees by hand before spawning.
>
> The new branch's base comes from the `worktree.baseRef` setting, never from a
> hardcoded branch name. **ork does not set it**: a plugin cannot, and no ork
> settings file carries it. Unless the operator put `"baseRef": "head"` in
> `.claude/settings.json` or `~/.claude/settings.json`, CC's default `"fresh"`
> applies: every tier agent branches from `origin/<default>`, unpushed local
> commits are invisible to it, and `tsc` fails with "cannot find module" for
> code you just wrote. Verify the setting before spawning. Full pattern:
> `Read("../chain-patterns/references/worktree-agent-pattern.md")`

```python
# Unit tests agent (worktree-isolated)
if "unit" in TIERS:
    Agent(
        subagent_type="ork:test-generator",
        isolation="worktree",
        prompt=f"""Generate unit tests for: {SCOPE}
        Risk targets, each with why: {gap_map["unit"]}
        Framework: {detected_framework}
        Existing tests: {existing_test_files}

        Focus on:
        - AAA pattern (Arrange-Act-Assert)
        - Parametrized tests for multiple inputs
        - MSW/VCR for HTTP mocking (never mock fetch directly)
        - Factory-based test data (FactoryBoy/faker-js)
        - Edge cases: empty input, errors, timeouts, boundary values
        - BEHAVIOUR GATE: write a test ONLY if it asserts an observable result. Never write a
          mock-call-only, assertion-free, tautological, or source-reading test (references/behaviour-gate.md)""",
        run_in_background=True,
        max_turns=50,
        model=MODEL_OVERRIDE
    )

# Integration + E2E agents follow the same pattern:
# - subagent_type="ork:test-generator", isolation="worktree", run_in_background=True, same targets + BEHAVIOUR GATE lines
# - Integration focus: API endpoints (Supertest/httpx), real DB, contract tests (Pact), Zod schema validation
# - E2E focus: Playwright, semantic locators, Page Object Model, axe-core a11y, visual regression
#
# Special case: emulate (Vercel Labs stateful API emulation):
# When integration tests need GitHub/Stripe/Resend/Okta/etc. emulated
# (HMAC webhooks, parallel port isolation, full config from scratch),
# spawn emulate-engineer instead of test-generator for that tier:
# - subagent_type="ork:emulate-engineer" (same isolation="worktree" form)
# - Pairs with emulate-seed skill for seed YAML patterns
```

Output each agent's results **as soon as it returns**. Don't wait for all agents.

### Phase 3b: Behaviour Gate

Before Phase 4, run `node "${CLAUDE_SKILL_DIR}/scripts/check-behaviour-tests.mjs" --json <new test files>` over the NEW test files only. Exit 1 lists each rejected test with its rule: (a) only mock-call assertions, (b) no assertion, only tautologies (`expect(true).toBe(true)`), or only not-to-throw when not throwing is not the stated contract, (c) reads or greps source instead of executing it. Delete each rejected test (the whole file on `drop`, which means every recognised test was rejected) or rewrite it to assert an observable result, and re-run until exit 0. Never delete an `unchecked` file or test (no test recognised, or its body is declared elsewhere): review it by hand. Run it again after Phase 5. Record the verdicts in `03-cover-generation.json`. Rules and limits: `Read("references/behaviour-gate.md")`.

> **Focus mode (CC 2.1.101):** In focus mode, include the full coverage report (before/after delta, test count per tier, files created) in your final message.

### Phase 4: Execution

Run all generated tests and collect results.

```python
# Run test commands per tier (PARALLEL if independent):
# Unit: npx vitest run tests/unit/ OR pytest tests/unit/
# Integration: npx vitest run tests/integration/ OR pytest tests/integration/
# E2E: npx playwright test

# Collect: pass count, fail count, error details, coverage delta
```

### Phase 5: Heal Loop

Do NOT hand-roll the loop. Run the real executor:

```python
Workflow(
  scriptPath="${CLAUDE_SKILL_DIR}/workflows/heal-loop.js",
  args={"testCommand": "<tier test command>", "tier": "unit", "testGlob": "tests/unit/",
        "maxIterations": 3}   # from the effort table above; omitted defaults to 3
)
# One invocation per tier that has failures.
```

**The iteration bound is enforced by the script, not by instruction.** `heal-loop.js`
runs a real counted loop clamped to `[2, 3]`: each iteration spawns a diagnose agent that
actually executes the test command and returns structured pass/fail plus the verbatim
failure output, then a repair agent that receives *that failure text* and edits test files
only. It exits early the moment the suite is green.

The **final iteration diagnoses but does not repair** - there would be no run left to verify
that repair. So `maxIterations: N` means N diagnose runs and N-1 repair passes, and the
default 3 means 2 repairs. A ceiling of 1 is coerced to 2, since 1 would mean zero repairs.

Every failure is classified into the taxonomy (`assertion`, `import`, `setup`, `timeout`,
`stale-selector`, `type`, `flaky`, plus `source-bug`), which selects the fix strategy.

The workflow returns `status: "healed"` or a **structured failure** (`status: "failed"`,
`healed: false`) carrying `remaining_failures`, `failure_categories`, and the per-iteration
ledger. Never report a `"failed"` result as a success: surface the still-failing tests in
the Phase 6 report.

Strategy detail (taxonomy table, fix rules, flaky prevention): `Read("references/heal-loop-strategy.md")`

**Boundary: heal fixes TESTS, not source code.** If a test fails because the source code has a bug, report it. Don't silently fix production code.

### Phase 6: Report

Generate coverage report with before/after comparison.

Full report layout (baseline→after table, tests-generated counts, heal iterations, files created, remaining gaps, next-steps commands): `Read("references/coverage-report-template.md")`.

### PushNotification on Completion (CC 2.1.110+)

Full `cover` runs (unit + integration + E2E with heal loop) take 15-45 min. After the Phase 6 report is assembled, call `PushNotification(message=f"ork:cover complete, {SCOPE}: {tests_kept} tests kept · {tests_dropped} dropped by behaviour gate · {heal_loops} heal iters", status="proactive")`. Full rule: `Read("../chain-patterns/rules/push-notification-on-completion.md")`.

### Coverage Drift Monitor (CC 2.1.71)

Optionally schedule a weekly check that the risk targets keep their behaviour tests:

```python
# Guard: Skip cron in headless/CI (CLAUDE_CODE_DISABLE_CRON)
# if env CLAUDE_CODE_DISABLE_CRON is set, run a single check instead
CronCreate(
  schedule="0 2 * * 0",
  prompt="Weekly drift check for {SCOPE}: run the tests covering the risk targets in 02-cover-analysis.json.
    Alert if one was deleted or fails, or if newly changed code has no behaviour test. Coverage is reported as information only."
)
```

---

## Key Principles

- **Output limits (CC 2.1.77+):** Opus 4.8 defaults to 64k output tokens (128k upper bound). For large test suites, chunk generation across multiple agent turns if output approaches the limit.
- **Partial reads (CC 2.1.144+):** Read returns a `[PARTIAL view]` first page (not an error) on oversized files. Re-read with explicit `offset`/`limit` so coverage analysis sees the whole file.
- **Tests only**: never modify production source code, only generate test files
- **Real services when available**: prefer testcontainers/docker-compose over mocks for integration tests because mock/prod divergence causes silent failures in production
- **Parallel generation**: spawn one test-generator agent per tier in ONE message
- **Heal, don't loop forever**: max 3 iterations, then report remaining failures
- **Progressive output**: show results as each agent completes
- **Factory over fixtures**: use FactoryBoy/faker-js for test data, not hardcoded values
- **Mock at network level**: MSW/VCR, never mock fetch/axios directly

---

## Agent Coordination

### Context Passing

Each test-generator agent receives: risk targets (each with why) for its tier, the behaviour gate rules, test framework config, real-service infrastructure (testcontainers, docker-compose), and fixture patterns from the project.

### Monitor + Partial Results (CC 2.1.98)

Use `Monitor` for streaming test execution output from background agents:

```python
# Stream test suite output in real-time
Bash(command="npm test -- --coverage 2>&1", run_in_background=true)
Monitor(pid=test_task_id)  # Each line → notification
```

Full pattern reference (until-condition gates, partial-result salvage, `TaskOutput` vs `Monitor` decision): `Read("../chain-patterns/references/monitor-patterns.md")`.

**Partial results (CC 2.1.98):** If a test-generator crashes mid-generation, synthesize what it produced:

```python
for agent_result in test_gen_results:
    if "[PARTIAL RESULT]" in agent_result.output:
        # Agent crashed: check if it wrote any test files before dying
        partial_tests = Glob(pattern="**/tests/**/*.test.*", path=agent_result.worktree)
        if partial_tests:
            # 6 passing tests from a crashed agent > 0 tests
            # Copy partial tests to main worktree, run them in Phase 4
            for test_file in partial_tests:
                Bash(command=f"cp {test_file} {main_worktree}/{test_file}")
            # Flag as partial in report
```

A `maxTurns` stop is also partial since CC 2.1.246 (summary: "stopped at its N-turn limit (partial result; continue it with SendMessage to the task-id)"); continue that agent with `SendMessage` instead of re-spawning it.

### SendMessage (Test Healing)

> **Cross-session replies land in the parent (CC 2.1.248):** when a subagent sends `SendMessage` to another session, the reply is delivered to the parent session's conversation, never to the subagent; a subagent sends and moves on, the parent reads the answer. Cross-session `SendMessage` / `ListAgents` also work on Bedrock, Vertex and Foundry and with telemetry disabled (CC 2.1.248).

When a generated test fails, the healer agent can request context from the generator:

```python
SendMessage(to="test-generator-unit", message="Test user_service_test.py:42 fails: TypeError on mock return. What's the expected shape?")
```

### Skill Chain

Standard chain: `implement → cover → verify → commit`. Use `addBlockedBy` between each.

### Verification Gate

Before claiming coverage is complete, apply: `Read("../../shared/rules/verification-gate.md")`. Run the coverage report fresh. "Should pass" is not evidence.

### Agent Status Protocol

All test-generator agents report using: `Read("../../shared/status-protocol.md")`. BLOCKED if tests can't be written due to missing interfaces. NEEDS_CONTEXT if test expectations are unclear.

## Quality Bar

Done means all of these hold:
- coverage report shows the before→after delta from the actual coverage command output, not an estimate
- every generated test file was executed; final pass/fail counts pasted from the runner
- the behaviour gate exited 0 on the new test files after Phase 5, and every target states why it was chosen
- no production source modified (only test files created); a source bug is reported, never silently patched
- failures healed within the iteration budget (≤3, effort-scaled) or reported explicitly with the remaining-failure count
- each requested tier (unit/integration/e2e) either produced tests or has a stated reason it was skipped

## Related Skills

- `ork:implement`: generates tests during implementation (Phase 5); use `cover` after for deeper coverage
- `ork:verify`: grades existing tests 0-10; chain: `implement → cover → verify`
- `testing-unit` / `testing-integration` / `testing-e2e`: knowledge skills loaded by test-generator agents
- `ork:commit`: commit generated test files

> **Session recovery (CC 2.1.108+):** After idle periods or interruptions, use `/recap` to restore conversational context alongside checkpoint-resume state. Enabled by default since CC 2.1.110 (even with telemetry disabled).

## References

Load on demand with `Read("references/<file>")`:

| File | Content |
|------|---------|
| `real-service-detection.md` | Docker-compose/testcontainers detection, service startup, teardown |
| `heal-loop-strategy.md` | Failure classification, fix patterns, iteration budget |
| `coverage-report-template.md` | Report format, delta calculation, gap analysis |
| `behaviour-gate.md` | Risk target signals, the three reject rules, checker usage and limits |
| `scripts/check-behaviour-tests.mjs` | Phase 3b checker: per-test keep or reject verdicts for new test files |
| `workflows/heal-loop.js` | Phase 5 executor: script-enforced 3-iteration repair loop (run via the Workflow tool) |

---

**Version:** 1.3.0 (September 2026): behaviour gate (Phase 3b checker) and risk-based targets replace percentage coverage targets


---

## References (5)

### Behaviour Gate

# Behaviour Gate and Risk Targets

`cover` writes a test only when it asserts behaviour, and it picks what to test by risk,
never by a coverage percentage. Coverage numbers appear in the report as information only.

## Pick targets by risk (Phase 2)

Rank candidate code by these signals, highest risk first. Every target carries a one-line
`why` naming the signal that selected it; a target with no signal is not a target.

| Signal | How to find it | Example `why` |
|--------|----------------|---------------|
| Changed code | `git diff --name-only <base>...HEAD` in scope | "changed in this branch, no test touches it" |
| Complex branches | nested conditionals, state machines, many early returns | "5 branches on order state, none exercised" |
| Error paths | `catch`, `except`, `throw`/`raise`, retries, timeouts | "retry-exhausted branch never runs in a test" |
| Security paths | auth, permission checks, input validation, secrets, crypto | "role check guards admin export" |
| Money paths | prices, discounts, tax, refunds, currency rounding | "refund rounds to cents" |
| Past bugs | `git log --grep='fix' -- <file>`, linked issues, reverts | "fixed twice in 60 days (see git log)" |

Uncovered lines that match no signal (trivial getters, re-exports, generated code) are
left alone. Handoff shape in `02-cover-analysis.json`:

```json
{"unit": [{"target": "src/billing/refund.ts:roundRefund", "why": "money path, fixed in #812"}]}
```

## What a generated test must do (Phase 3)

A test asserts an observable result: a return value, a thrown error type, persisted state,
an HTTP response, rendered output, a file the code wrote. The checker rejects three shapes.

| Rule | Rejected shape | Write this instead |
|------|----------------|--------------------|
| **a** | Every assertion is on mock calls: `toHaveBeenCalled*`, `.mock.calls`, `assert_called*`, `call_count`, `sinon.assert.*` | Also assert what the code returned or changed. Mock-call assertions are fine next to a result assertion. |
| **b** | No assertion (the test only runs code), only tautologies (`expect(true).toBe(true)`, `assert 1 == 1`: literals compared with literals, or a name with itself), or only `not.toThrow()` / `does_not_raise()` | Assert the result. Keep a not-throw-only test only when not throwing IS the contract, and say so in the test name, a comment, or the docstring ("does not throw on empty input"). |
| **c** | Reads the code under test as text: `readFileSync` / `open()` / `read_text()` of a source file, `inspect.getsource`, a `grep` / `rg` subprocess, a `?raw` import | Import or call the code and assert on what it does. Reading fixtures and test data is fine. |

## The checker (Phase 3b, again after Phase 5)

```bash
node "${CLAUDE_SKILL_DIR}/scripts/check-behaviour-tests.mjs" --json <new test files>
```

Run it over the NEW test files only, never the pre-existing suite. Per file it reports
`keep`, `partial` (some tests rejected), `drop` (at least one test recognised and every
recognised test rejected), or `unchecked`. A file is `unchecked` when its language is not
JavaScript, TypeScript, or Python, when no test case was recognised in it, or when every case
passes a function declared in another file as its body. A single case can also be
`unchecked` for that last reason. Each rejected test has its rule, line, and reason. Exit
`0` means nothing was rejected (every result is `keep` or `unchecked`), `1` means something
was rejected, and `2` means a usage or read error.

On exit `1`: delete each rejected test, or delete the whole file when its verdict is `drop`,
or rewrite the test to assert an observable result. Then re-run the checker until it exits
`0`. Re-run it after Phase 5 as well, because a heal that deletes an assertion to go green is
a rule (b) failure. Record the final verdicts in `03-cover-generation.json` and list every
dropped or rewritten test in the report's Behaviour Gate section.

**Never delete an `unchecked` file or test.** Unchecked means the checker could not see the
test, not that the test is hollow. Apply the same three rules by reading it, and fix it by
hand only when it breaks one.

## Known limits

The checker is static and heuristic. It recognises `it` / `test` / `fit` / `xit` cases
(Jest, Vitest, Jasmine, Mocha, Playwright, node:test), `Deno.test`, and tap `t.test`, with an
inline body or the name of a function declared in the same file. It recognises Jest, Vitest,
Playwright, node:assert, chai, chai-http, sinon, tap, ava, Deno, pytest, and unittest
assertions, and Supertest `.expect(...)` on a chain rooted at `request(` / `supertest(`, the
name the file imports `supertest` as, or an agent variable built from them. An assertion
whose operands are all literals counts as no assertion. An assertion hidden in a helper counts only if
the helper is named `expect*` or `assert*` (`assert_*` / `expect_*` in Python), so name
helpers that way. A read is treated as a source read when its path, or a variable declared
from that path, names a code file; paths containing `fixture`, `testdata`, or `__snapshots__`
are exempt.


### Claude Code

# Claude Code adapter: cover

Slash invoke: `/ork:cover`

YAML `hooks:` / `command:` lines in this skill's frontmatter stay Claude-only.
They use `${CLAUDE_PLUGIN_ROOT}/hooks/bin/run-hook.mjs` and are ignored by pi.

Session chain files live at `.claude/chain/` (state.json, decisions.json, capabilities.json).
Other hosts should persist equivalent run state in their own session store.

Agent spawn form, when used: `Agent(ork:...)` is Claude Code. Portable body text uses the skill name.

Lines that still mention a Claude-only path in the body:

```
Host-neutral workflow. Invoke by skill name (`cover`). Claude Code slash routing, YAML hook loaders, and `.claude/chain` live in `references/claude-code.md`.
Write(".claude/chain/capabilities.json", {
Read(".claude/chain/state.json")
```


### Coverage Report Template

# Coverage Report Template

Format for the Phase 6 report output.

## Report Structure

```markdown
# Coverage Report: {SCOPE}

## Summary

Coverage is shown as information, never as a target.

| Metric | Before | After | Delta |
|--------|--------|-------|-------|
| Unit coverage | {N}% | {N}% | +{N}% |
| Integration coverage | {N}% | {N}% | +{N}% |
| E2E coverage | {N}% | {N}% | +{N}% |
| **Overall** | **{N}%** | **{N}%** | **+{N}%** |

## Tests Generated

| Tier | Count | Pass | Healed | Failed |
|------|-------|------|--------|--------|
| Unit | {N} | {N} | {N} | {N} |
| Integration | {N} | {N} | {N} | {N} |
| E2E | {N} | {N} | {N} | {N} |
| **Total** | **{N}** | **{N}** | **{N}** | **{N}** |

Heal iterations used: {N}/{maxIterations}   # denominator is 2 or 3, see heal-loop-strategy.md

## Risk Targets

| Target | Why chosen | Tests kept |
|--------|------------|------------|
| {file}:{symbol} | {changed code / complex branches / error path / security / money / past bug} | {N} |

## Behaviour Gate

Checker: `scripts/check-behaviour-tests.mjs`, final run exit {0|1}.

| Test | Rule | Action |
|------|------|--------|
| {file}:{line} "{name}" | {a: mock calls only / b: no assertion / c: reads source} | {dropped / rewritten} |

## Files Created

{list of test files created, grouped by tier}

## Real Services Used

{list of services started via docker-compose or testcontainers, or "None (mocks only)"}

## Remaining Gaps

{risk targets still without a behaviour test, with reasons}

## Failures (if any)

{tests that could not be healed within the iteration budget, with failure reason and suggested fix}

## Possible Product Bugs (if any)

{one line per heal-loop `possible_product_bugs` entry, its `report` field verbatim:
"possible product bug: expected X, got Y (file:line)". These tests are left failing on
purpose: heal never rewrites an expected value to match current output.}

## Needs a Human (if any)

{one line per heal-loop `needs_human` entry, its `report` field verbatim:
"binding change needs a human: NAME old -> new", "value change needs a human: WHAT old -> new"
or "snapshot or golden edit needs a human: FILE". The change was reverted and the test left
failing; decide whether it is a fix or a product bug.}

## Next Steps

- `/ork:verify {SCOPE}`: grade the implementation + tests
- `/ork:commit`: commit generated test files
- Fix source bugs detected during test generation (if any)
- Confirm or fix each possible product bug listed above (if any)
- Decide each binding change listed under Needs a Human (if any)
```

## Delta Calculation

```python
# Before: run coverage with existing tests only
baseline = run_coverage(existing_tests)

# After: run coverage with existing + generated tests
final = run_coverage(existing_tests + generated_tests)

# Delta per file
for file in scope_files:
    delta = final[file] - baseline[file]
    # Report files with biggest delta first
```

## Coverage Tool Commands

| Stack | Command | Output |
|-------|---------|--------|
| Vitest | `npx vitest run --coverage --reporter=json` | `coverage/coverage-final.json` |
| Jest | `npx jest --coverage --json` | `coverage/coverage-final.json` |
| pytest | `pytest --cov={scope} --cov-report=json` | `coverage.json` |
| Go | `go test -coverprofile=coverage.out ./...` | `coverage.out` |
| Playwright | Coverage via Istanbul instrumentation | `coverage/` dir |

## No Coverage Targets

There is no percentage to reach. Coverage numbers describe the suite; they never decide
whether to write a test. Targets come from risk (see `behaviour-gate.md`), and a test is
kept only when it asserts behaviour.


### Heal Loop Strategy

# Heal Loop Strategy

Fix failing generated tests iteratively. Ceiling is 3 iterations (2 repair passes plus a
final verifying run) to prevent infinite loops. See "Iteration Budget" below for the split.
Phase 5 of `/ork:cover` runs this as `src/skills/cover/workflows/heal-loop.js`.

## Failure Classification

| Category | Example | Fix Strategy |
|----------|---------|-------------|
| **Assertion error** (value mismatch) | `expected 200, got 201`, snapshot diff, wrong status code or count | **Never healed.** Report `possible product bug: expected X, got Y (file:line)`; the test stays failing |
| **Import error** | `Cannot find module './auth'` | Fix import path, check tsconfig/conftest |
| **Setup error** | `Connection refused` | Add missing service setup, check fixture scope |
| **Timeout** | `Test exceeded 5000ms` | Add proper waits (Playwright: auto-wait; API: increase timeout) |
| **Selector stale** | `Element not found: [data-testid="submit"]` | Switch to semantic locator (`getByRole`) |
| **Type error** | `Property 'id' does not exist` | Fix type assertion or factory output |
| **Flaky** | Passes sometimes, fails others | Remove timing deps, add deterministic waits |

## Iteration Budget

Enforced by `workflows/heal-loop.js`, not by instruction. The ceiling is **3 diagnose runs
and 2 repair passes**: the final iteration verifies the previous repair and does not start a
new one, because there would be no run left to confirm it.

```
Iteration 1: diagnose -> repair  (obvious errors: imports, paths, setup)
Iteration 2: diagnose -> repair  (interaction errors: selectors, timing, state)
Iteration 3: diagnose only       (verify iteration 2's repair; no further repair)
```

Anything still failing after the final diagnose is reported with:
- Failure reason
- File and line number
- Suggested manual fix

## Fix Rules

1. **Never modify source code**: only fix test files
2. **Read source before fixing**: understand the actual behavior
3. **Never rewrite an expected value** to match current output. Heal repairs setup, fixtures,
   imports, paths, types, selectors, timeouts and flakes only. `heal-loop.js` withholds value
   mismatches from the repair agent, and rejects and reverts any reported fix that changes an
   expected value, a snapshot, or a test already reported as a possible product bug
4. **Binding changes go to a human.** In a test file, a fix that changes the value of a
   `const`/`let`/`var` binding, a bare reassignment, or a Python `NAME =` is not healed: the
   assertion reading it can sit outside every reported hunk, so no hunk rule can tell a
   setup fix from a moved expected value. The same holds for any other changed value and
   for any snapshot or golden edit. See Needs a Human below
5. **Don't suppress errors**: if a test exposes a real bug, report it
6. **Keep tests deterministic**: no `Date.now()`, no `Math.random()` without seeding

## Deterministic Checks

The agents' labels (failure `category`, fix `category`, `touches_expected_value`) are not
trusted on their own. `heal-loop.js` also decides from the text:

- **Classification.** A failure message in an expected/actual shape (`expected X, got Y`,
  Jest `Expected: / Received:`, chai/vitest `expected 409 to be 422`, pytest
  `assert 422 == 409`, unittest `422 != 409`, two status codes on one `status` line) is a
  value mismatch whatever category the run agent gave it. Two shapes are excluded: TS2554
  `Expected 2 arguments, but got 1` is a type error, and Playwright
  `Received: <element(s) not found>` is a locator that matched nothing, so it stays
  repairable as a stale selector. `toHaveCount` `Received: 0` and a real `toHaveText` diff
  (`Received string: "Submit"`) remain value mismatches.
- **Fix vetting.** Assertions are extracted from each fix's `before` and `after`: `expect(...)`
  keyed by its whole matcher chain and arguments, Python `assert`, `assertEqual`/`assertTrue`
  style calls, `assert.*`, `pytest.raises`, chai `should`, and status comparisons. The fix is
  rejected when:
  - an assertion from `before` is missing from `after` (changed, removed, negated, or
    weakened to `toBeDefined`, `toBeTruthy`, `not.toThrow`);
  - an `expect(...)` subject changes, unless the fix is `stale-selector`, both subjects are
    locator expressions (`getBy*`, `queryBy*`, `findBy*`, `locator(`, behind `page.`,
    `screen.` or `within(x).`), and the new subject holds no literal equal to the failure's
    expected value. `expect(409)` or `expect(Math.min(res.status, 409))` never passes;
  - an assignment changes at all for a name on an assertion's expected side: an `expect`
    matcher's arguments, the right of `assert a == b`, the expected argument of
    `assertEqual` and friends (`const want = 409` to `422`, Python `WANT = 409` to `422`,
    `const CODE = oldStatus` to `newStatus`), or for a name matching `expect`, `want` or
    `golden` even when no reported hunk asserts on it;
  - an assignment changes a literal for a name an assertion inspects (`const got = 409`
    for `expect(got).toBe(409)`), unless it is a stale-selector locator swap, the same
    rule as a subject rewritten in place (`page.getByRole('button', { name: 'Old' })` to
    `'New'` for `expect(button).toBeVisible()` is kept);
  - both name sets are collected per file across every hunk of one repair pass, before
    and after, so a value moved in one hunk is caught when the assertion sits in another;
  - `after` adds `try`, `catch`/`except`, `return`, `if`, a ternary, `&&` or `||` while it
    contains an assertion (counted against `before`);
  - `after` adds `skip`, `only`, `todo`, `fixme` or `xfail`.

  Edits that touch no assertion (imports, fixtures, waits) pass, and so does a locator swap
  in a stale-selector fix.

## Possible Product Bugs

Every value mismatch (`assertion`, `source-bug`), every fix the script rejected, and every
bug the repair agent reports lands in `possible_product_bugs` of the workflow result. Each
entry carries a `report` line to surface verbatim in the Phase 6 report:

```
possible product bug: expected 0, got -40 (tests/unit/test_payment.ts:23)
  test:   test_negative_quantity
  origin: classified | rejected-repair | repair-agent
  action: test left FAILING. Confirm the intended behavior, then fix the source or the test by hand.
```

If a test diagnosed as a value mismatch stops failing on any later run, red or green, a test
edit changed what it asserts. The workflow reports `status: "failed"`,
`value_mismatch_vanished: true`, names each one in `vanished_value_mismatches`, marks its
entry `still_failing: false`, and sets `fail_count: -1` so a caller cannot read it as healed.
Only failures the run agent diagnosed count here; rejected-repair and agent-reported entries
can carry incomplete identities. A green run with any possible product bug flagged is also
reported as `status: "failed"` with `fail_count: -1`.

## Needs a Human

A fix that passes every check above but changes a value in a test file is held, reverted,
and listed in `needs_human` with a `report` line to surface verbatim. A changed value is,
whatever the syntax:

- a binding: `const`/`let`/`var`, a bare reassignment, or a Python `NAME =`;
- a member or subscript assignment: `obj.x = v`, `obj['k'] = v`, Python `d['k'] = v`;
- a merge into an existing object: `Object.assign`, lodash `merge`, `deepmerge`, Python
  `d.update(...)`, or a spread in a changed literal;
- the contents of an object, array or dict literal (a mock return, a seed list, an options
  object such as `{ timeout: 5000 }`);
- any other literal the hunk adds, changes or removes (the closure rule), such as an
  argument to an `expect*` helper (`expectTotal(cart, 3)` to `2`) or a mutating call on an
  expected container (`expected.push(4)`). A literal is a number, string, boolean, regex,
  array or object literal. Literals are compared per enclosing call, so a value swapped
  between two calls is still a change.

Any edit to a snapshot or golden file is held the same way, test file or not: a path under
`__snapshots__/`, a `*.snap`, a `*.json` under `tests/`, or any path containing `golden` or
`expected`. So is a change that turns snapshot updating on (`-u`, the update-snapshot flag,
`updateSnapshot`); a repair whose own description names a snapshot update is already
rejected above as a possible product bug.

```
binding change needs a human: CODE STATUS.CONFLICT -> STATUS.UNPROCESSABLE
value change needs a human: Object.assign(expected) Object.assign(expected,{total:3}) -> Object.assign(expected,{total:2})
snapshot or golden edit needs a human: tests/golden/order.json
```

The test stays failing, is withheld from later repair passes, and is never reported as
healed or as a possible product bug: a person decides which it is. A green run with any
entry held is reported as `status: "failed"` with `fail_count: -1`. Four exceptions stay
healable:

- a plain binding whose own name ENDS in a safe word. The safe list never applies to a
  property of another object: `expected.url` and `want["path"]` are held. The name is split into words (camelCase,
  snake_case, kebab-case, SCREAMING_CASE), trailing unit words (`ms`, `s`, `sec`, `secs`,
  `seconds`, `millis`, `milliseconds`, `min`, `mins`, `minutes`) are dropped, and the last
  word, or the last two joined, must be one of `timeout`, `delay`, `retry`, `retries`,
  `wait`, `interval`, `poll`, `port`, `host`, `url`, `baseurl`, `path`, `dir`, `directory`,
  `fixture`, `fixtures`, `file`, `filename` (`TIMEOUT_MS`, `retryDelayMs`, `apiBaseUrl`,
  `dataDir`). A substring does not count: `redirectTarget`, `userProfile`, `reportTotal`,
  `hostName` and `timeoutMessage` are held;
- the fix is `stale-selector` and both values are locator expressions with no literal equal
  to the failure's expected value (`page.getByRole('button', { name: 'Old' })` to `'New'`),
  or the changed literal is an argument of a locator call;
- an import line (`import { login } from '../../src/auth'`, Python `from x import y`): a
  module path is not a value;
- the duration or load state passed to a timing call: `waitForTimeout`, `waitForLoadState`
  (`load`, `domcontentloaded`, `networkidle`), `setTimeout`, `sleep`,
  `advanceTimersByTime`.

Snapshot and golden edits have no exception.

A binding deleted in one hunk and re-added with a new value in another counts as a change,
and holds both hunks. Apart from snapshot and golden paths, files outside the test tree
(`playwright.config.ts`) are not covered by this rule.

## Flaky Test Prevention

Generated tests must avoid:
- `setTimeout`/`sleep` for synchronization
- Shared mutable state between tests
- Order-dependent test execution
- Hard-coded ports or file paths
- Time-sensitive assertions (`Date.now()`)

Instead use:
- Playwright auto-wait and `waitFor` assertions
- Fresh fixtures per test (function scope)
- Dynamic port allocation
- Relative paths and temp directories
- Frozen time (vi.useFakeTimers / freezegun)


### Real Service Detection

# Real-Service Detection

Detect and configure real-service infrastructure for integration tests. Real services catch bugs that mocks miss, mock/prod divergence caused silent failures in past incidents.

## Detection (Phase 1)

```python
# PARALLEL — scan for infrastructure:
Glob(pattern="**/docker-compose*.yml")
Glob(pattern="**/docker-compose.test.yml")
Grep(pattern="testcontainers", glob="**/package.json", output_mode="content")
Grep(pattern="testcontainers", glob="**/requirements*.txt", output_mode="content")
Grep(pattern="testcontainers", glob="**/pyproject.toml", output_mode="content")
```

## Decision Matrix

| Found | Strategy | Test Speed |
|-------|----------|------------|
| `docker-compose.test.yml` | Start services via compose, run tests, tear down | ~30s startup |
| `docker-compose.yml` (no test variant) | Use with `--profile test` if available | ~30s startup |
| `testcontainers` in deps | Per-test isolated containers | ~5s per container |
| Neither + `--real-services` flag | Error: suggest installing testcontainers | N/A |
| Neither, no flag | Fall back to mocks (MSW/VCR) | ~0s |

## Docker-Compose Workflow

```bash
# Start services before tests
docker compose -f docker-compose.test.yml up -d --wait

# Run integration tests
npm run test:integration  # or pytest tests/integration/

# Tear down after tests
docker compose -f docker-compose.test.yml down -v
```

Services to look for in compose files:
- `postgres` / `mysql` / `mongodb`, database
- `redis`: cache/sessions
- `rabbitmq` / `kafka`, message queues
- `elasticsearch` / `opensearch`, search
- `minio` / `localstack`, S3-compatible storage

## Testcontainers Workflow

### TypeScript

```typescript
import { PostgreSqlContainer } from '@testcontainers/postgresql';

let container: StartedPostgreSqlContainer;

beforeAll(async () => {
  container = await new PostgreSqlContainer()
    .withDatabase('test_db')
    .start();
  process.env.DATABASE_URL = container.getConnectionUri();
}, 30_000);

afterAll(async () => {
  await container.stop();
});
```

### Python

```python
import pytest
from testcontainers.postgres import PostgresContainer

@pytest.fixture(scope="session")
def postgres():
    with PostgresContainer("postgres:16-alpine") as pg:
        yield pg.get_connection_url()
```

## Environment Variables

When real services are running, set connection strings:

| Service | Env Variable | Source |
|---------|-------------|--------|
| PostgreSQL | `DATABASE_URL` | `container.getConnectionUri()` |
| Redis | `REDIS_URL` | `redis://localhost:{mapped_port}` |
| MongoDB | `MONGODB_URI` | `container.getConnectionString()` |

## Cleanup

Always tear down services after tests:
- Docker-compose: `docker compose down -v` (removes volumes)
- Testcontainers: `container.stop()` in afterAll/fixture teardown
- Database state: transaction rollback or truncate between tests
