Reads directly from the canonical spec files. Re-run after a spec change and tests regenerate
from source-of-truth automatically.
-
Discover project conventions (do this first — never hardcode)
This plugin runs in any repo. Detect the host project's testing setup, in priority order:
.openspec-tdd.json at repo root, if present. Fields (all optional):
{
"testRunner": "npx vitest run",
"renderHelper": "@/test/render",
"canonicalTest": "src/example.test.tsx",
"testIdQuery": "getByTestId",
"testFileGlob": "**/*.test.{ts,tsx}"
}
CLAUDE.md / AGENTS.md / .cursorrules — read for testing conventions, the
canonical-reference test file, render-helper location, and selector rules.
package.json — infer the runner from scripts.test / devDependencies
(vitest → npx vitest run; jest → npx jest; bun → bun test; otherwise
node --test). Infer React/Testing-Library presence from deps.
- Fallback defaults — bare
vitest, plain testing-library render, getByTestId.
Announce what you detected: "Runner: · Render helper: <path|none> · Canonical ref: <file|none>".
See references/frameworks.md for per-runner commands and references/non-react.md /
references/e2e.md for non-component scenarios.
-
Select the change
If a name is provided, use it. Otherwise:
- Infer from conversation context
- Auto-select if only one active change exists
- If ambiguous, run
openspec list --json and use AskUserQuestion tool to let user select
Announce: "Using change: "
-
Parse scenarios from specs
find openspec/changes/<name>/specs -name "*.md" 2>/dev/null
If no specs/**/*.md exist in the change, stop:
"No spec files found in openspec/changes/<name>/specs/. Run opsx:propose first to
generate specs with #### Scenario: blocks, then re-run /opsx:tdd."
Parse every specs/**/*.md file. For each file:
a. Extract scenarios — collect every #### Scenario: block with its parent
### Requirement: name and its WHEN / THEN lines.
If a file has no #### Scenario: blocks, skip it and note it in the report.
b. Derive the test file path — in priority order:
- design.md Implementation Map: scan
openspec/changes/<name>/design.md for a
markdown table with columns Capability, File (or similar). Extract the row
matching this capability's name.
| `ability-card` | `features/combat/AbilityCard.tsx` | `AbilityCard` |
- design.md file path mentions: scan design.md for any
.ts / .tsx paths
that mention the capability name or a related module.
- Project search:
find . \( -name "*.ts" -o -name "*.tsx" \) | grep -v node_modules | grep -i <capability>
— pick the best match (non-test file).
- Prompt the user: use AskUserQuestion with the capability name and a list
of candidate paths found. Never guess silently.
Test file = implementation file path with .test.ts / .test.tsx extension, same
directory. Example: features/combat/AbilityCard.tsx → features/combat/AbilityCard.test.tsx.
c. Translate THEN clauses to assertion intent — for each scenario, capture the full
THEN text. Step 4 uses this to write the real assertion body.
THEN clauses that are too vague to assert (e.g. "THEN the UI updates") are flagged
as spec quality gaps in the step-7 report — never padded with a stub assertion.
Announce: " specs · scenarios · source-of-truth: specs/**/*.md"
-
Write REAL failing tests (solid RED, not forced fail)
The goal is a genuine red: the test renders the real component, calls the real function,
queries real data-testids, and fails because the implementation does not exist yet.
NOT expect.fail(...) placeholders.
Before writing, gather concrete selectors and behavior from:
openspec/changes/<name>/proposal.md and design.md — component names, props,
data-testid values, expected copy/values, API/mutation shapes.
- The WHEN/THEN clauses extracted in step 2 — translate THEN directly into assertions.
- Existing sibling code and the canonical test reference discovered in step 0.
For each scenario group:
- If the target file does not exist, create it using the conventions from step 0: the
project's custom render/wrapper helper,
screen, userEvent, and the shared mock setup —
never a bare testing-library render for component tests when the project has a helper.
- If a
describe block with the exact name already exists, append it()s inside it.
Otherwise append a new describe block.
- For each scenario, write a real test body:
- Render the component / invoke the unit under test (import it for real — if the module
or export does not exist yet, that import failure IS a valid RED).
- Query by real
data-testid, role, or text — the exact selectors the implementation
must satisfy. Use the project's testIdQuery convention.
- Assert real outcomes: presence, values, call args, emitted state.
- Drive interactions with
userEvent where the scenario describes user action.
data-testid contract: when a scenario needs a testid that does not exist yet, pick
the canonical name now and assert against it. The test defines the contract; opsx:apply
must add that exact testid to make it GREEN. List every new testid in the step-6 report.
- When a value is genuinely unknowable from the artifacts (e.g. exact copy) or a branch is
unreachable under current config, assert the observable behavior instead — or mark the
scenario
it.skip with a one-line reason and note it as a behavior gap. Never pad with a
forced fail.
Re-running /opsx:tdd after spec changes replaces existing test files for the same
capability — tests stay in sync with specs.
-
Run tests and confirm RED
Run the project's test runner (from step 0) against each written file. Where the runner
supports it, filter per scenario by name (-t "<scenario>").
For each result:
- If FAIL → confirm it fails for the right reason: missing element/testid, missing
export/module, wrong value — i.e. the absent implementation. Capture the failure message.
(A whole-suite collection error from a missing import is a valid solid RED.)
- If FAIL for a wrong reason (typo, bad import path, malformed selector, missing mock) → fix
the test and re-run. A red test must be a correct test failing only on missing impl.
- If PASS → flag: "WARNING: '' passes without implementation — already covered or
the test asserts nothing real. Review before proceeding."
Track per-test results (pass/fail + failure reason) to populate the report in step 6.
-
Mark tests artifact done
openspec done tests --change "<name>"
If the command fails because the tests artifact is not defined in the schema, skip
silently. Any other error must be surfaced — do not suppress it.
-
Report
## opsx:tdd complete — <change-name>
**Runner:** <cmd> **Render helper:** <path|none>
**Specs read:** N files · M scenarios
**Tests written:** X
**RED (expected):** X — each with its failure reason (missing testid/export/value)
**Unexpected passes:** Y (review required)
**New data-testid contract** (apply must implement these exact ids):
- `<testid>` → <component/file>
**Spec quality gaps:** <vague THEN clauses that need the spec revised>
**Behavior gaps:** <scenarios skipped / asserting observable behavior only>
Then: "Run /opsx:apply to implement tasks and make tests GREEN."