Test
Success
I := π written ∧ test passes
V := {commands.test} {test_file} → exit 0
Let:
τ := target file(s) under test
π := test file adjacent to source ({name}.test.ts | {name}.spec.ts | __tests__/{name}.test.ts)
Σ := {standards.testing}
Stack: Read .dev/stack.yml first — every {field} placeholder below resolves from it. ¬∃ → output: ".dev/stack.yml not found — run /R-env-setup to generate it." and stop.
Generate tests for changed/specified files. Follow existing codebase patterns.
Usage
/R-test → Generate tests for files changed vs base branch
/R-test src/auth/login.ts → Generate tests for a specific file
/R-test --e2e → Generate Playwright e2e tests for changed files
/R-test --run → Run existing tests ({commands.test})
Pipeline
| Step | ID | Required | Verifies via | Notes |
|---|---|---|---|---|
| 1 | run-shortcut | — | {commands.test} exit 0 |
--run flag only |
| 2 | identify-targets | ✓ | Δ files listed | — |
| 3 | read-standards | ✓ | Σ read | — |
| 4 | check-coverage | ✓ | π ∃? | — |
| 5 | generate-tests | ✓ | tests generated | — |
| 6 | approval | ✓ | user confirms | — |
| 7 | write-and-verify | ✓ | test exit 0 | retry 1 |
| 8 | falsify | ✓ | broke {file} → {error} |
mechanical preferred; e2e skip |
Pre-flight
Success: π written ∧ test passes
Evidence: {commands.test} {test_file} exit 0
Steps: identify-targets → read-standards → check-coverage → generate-tests → approval → write-and-verify
¬clear → STOP + ask: "Which file(s) need tests?"
Step 1 — --run Shortcut
--run ⇒ {commands.test} → report results → stop.
Step 2 — Identify Target Files
BASE=$(. "${CLAUDE_SKILL_DIR}/../shared/lib.sh" && detect_base_branch)
git diff origin/${BASE}...HEAD --name-only
Include: .ts, .tsx. Exclude: *.config.ts, *.d.ts, *.test.*, *.spec.*, files with no exports.
Specific file arg ⇒ use directly. ¬testable τ ⇒ inform + stop.
Step 3 — Read Standards + Find Patterns
Read Σ before generating — contains framework config, AAA requirements, mocking strategies, coverage targets.
Glob *.test.ts / *.spec.ts near τ → read 1–2 examples → extract: describe/it nesting, mock approach, assertion style, naming.
Framework: Vitest on Bun. Always import explicitly:
import { describe, it, expect, vi } from 'vitest'
Bun compat constraints:
| Avoid | Use instead |
|---|---|
vi.mocked(fn) |
fn as ReturnType<typeof vi.fn> |
vi.stubGlobal('fetch', mock) |
globalThis.fetch = mock as typeof fetch |
vi.stubGlobal('Bun', {...}) |
vi.spyOn(Bun, 'spawn').mockImplementation(...) |
vi.restoreAllMocks() in beforeEach |
vi.clearAllMocks() |
Mock factory hoisting: Bun validates vi.mock factories against real module at hoist time. Side-effectful imports run before process.env assignments. Fix: vi.mock('../../shared/config', factory) to intercept directly.
Step 4 — Check Existing Coverage
∀ τ → check for π. ∃ π ⇒ read, compare with source exports, offer to add missing coverage (¬overwrite). ¬π ⇒ generate full test file.
Step 5 — Generate Tests
∀ τ:
- Read source → understand exports, signatures, types, behavior
- Generate: happy path, edge cases (empty/null/boundary), error cases
- Structure using AAA with explicit comments:
it('should return user by id', () => {
// Arrange
const userId = 'abc-123'
// Act
const result = getUser(userId)
// Assert
expect(result).toBeDefined()
})
- Coverage targets: 90% business logic | 80% controllers/modules | 70% overall
- Follow discovered patterns exactly
- Place adjacent to source:
login.ts→login.test.ts
Step 6 — Approval
→ present choice Approve and write all | Approve with modifications | Skip specific files ¬write without approval.
Step 7 — Write + Verify
∀ approved τ: write via Write tool → {commands.test} {test_file_path} → report pass/fail.
∃ failures ⇒ → present choice show failing test + error → propose fix → re-run.
Step 8 — Falsification Gate (standalone /R-test)
Applies to: unit + fast-integration tests only. Triggered after Step 7 green run. Ownership: when invoked by /R-dev-implement, the implement orchestrator drives the gate (¬R-tester). When invoked standalone (no implement orchestrator), /R-test owns the cycle itself — the R-tester agent still only writes tests; the runner is driven by the /R-test flow.
e2e exemption: tests generated via --e2e → set Status to ⚠ NO FALSIFY — e2e (do not leave ⏳ not run). Stop. ¬run stash cycle.
Precondition: all newly created source files must be git add-ed before the gate runs — the Write tool does NOT auto-stage new files, and unstaged new files are invisible to git diff HEAD.
Evidence is mandatory. A test without a broke {file} → {error} line stays ⏳ not run, never ✓ proven.
Spec SCs with a priced-quantity block: test priced + oracles, never not.
Runner — plugin-owned (default, ADR-019):
bash ${CLAUDE_PLUGIN_ROOT}/skills/pr/run-falsify.sh --map <map.json> --out artifacts/reviews/{N}-falsify.json --issue {N}
Consumer {commands.test:falsify} / package.json test:falsify / scripts/test-falsify.sh allowed only if they exec the plugin helper without swallowing non-zero — else stub-refuse. LLM git stash is ¬an alternate oracle.
On oracle_ok=true, set ✓ proven from JSON rows. Persist JSON (+ optional md render). Evidence lines come from the helper output. Matrix Status = ✓ proven only from runner rows — append evidence block to output before reporting done.
E2E Mode (--e2e)
Check Playwright:
bunx playwright --version 2>/dev/null
¬installed ⇒ inform install command for {package_manager}:
- bun:
bun add -d @playwright/test && bunx playwright install - pnpm:
pnpm add -D @playwright/test && pnpm exec playwright install - npm:
npm install --save-dev @playwright/test && npx playwright install - yarn:
yarn add --dev @playwright/test && yarn playwright installStop (¬install deps).
E2E dir: {frontend.path}/e2e/ (fall back to e2e/ if {frontend.path} not set).
Check existing patterns first. Name: {feature}.spec.ts.
Follow approval + verification flow (Steps 6–7).
Playwright Patterns
∃ page objects in {frontend.path}/e2e/ → follow them.
// e2e/pages/login.page.ts
import { type Page, type Locator } from '@playwright/test'
export class LoginPage {
readonly emailInput: Locator
readonly passwordInput: Locator
readonly submitButton: Locator
constructor(readonly page: Page) {
this.emailInput = page.getByLabel('Email')
this.passwordInput = page.getByLabel('Password')
this.submitButton = page.getByRole('button', { name: 'Sign in' })
}
async goto() { await this.page.goto('/login') }
async login(email: string, password: string) {
await this.emailInput.fill(email)
await this.passwordInput.fill(password)
await this.submitButton.click()
}
}
// e2e/auth/login.spec.ts
import { test, expect } from '@playwright/test'
import { LoginPage } from '../pages/login.page'
test.describe('Login flow', () => {
test('should login with valid credentials', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login('user@example.com', 'password123')
await expect(page).toHaveURL('/dashboard')
})
})
Selectors: page.getByRole(), page.getByLabel(), page.getByText() (¬page.locator('css') unless no semantic alternative).
Edge Cases
| Scenario | Behavior |
|---|---|
| File has no exports | Skip, inform user |
| Tests already exist | Offer to add missing coverage, ¬overwrite |
| Test framework not detected | → ask user which framework to use |
--run flag |
Run {commands.test} and report only |
| React component | Generate component tests with appropriate render approach |
| File in monorepo package | Place tests relative to package, ¬root |
Safety Rules
- ¬overwrite existing test files without explicit approval
- ¬install dependencies — inform + stop
- Always present generated tests for approval before writing
- Always run tests after writing
- Always match existing patterns — ¬impose different style
$ARGUMENTS