feat(skills): make tdd-workflow test-runner aware (npm/pnpm/yarn/bun) (#2347)

* feat(skills): make tdd-workflow test-runner aware (npm/pnpm/yarn/bun)

Add "Step 0: Detect the Test Runner" so the RED/GREEN cycle no longer
hardcodes `npm test`. Distinguishes the package manager from the test
runner (a project can install with Bun yet run Jest/Vitest), adds a runner
command matrix, and warns about `bun test` (native bun:test runner) vs
`bun run test` (runs the package.json script) — a common ESM failure mode.
Adds a Bun native test pattern section and links the bun-runtime skill.

Applied to both the canonical skills/ copy and the .agents/skills/ Codex
subset (manual sync per CONTRIBUTING).

* docs(skills): apply <test>/<coverage> placeholders in tdd-workflow steps

Address review feedback on PR #2347: Step 0 instructs the agent to substitute
the detected runner command, but Steps 3/5/7, Run Coverage Report, Watch Mode,
Pre-Commit, and CI/CD still showed literal `npm test` / `npm run test:coverage`
— so an agent reaching those blocks could run npm test on a pnpm/bun project.
Replace them with the <test> / <test-watch> / <coverage> placeholders from
Step 0. Left untouched: the plan-handoff allowlist example and the Step 8
evidence-table samples (illustrative, not run-this instructions). Applied to
both the canonical and Codex-subset copies.

* docs(skills): make pre-commit lint runner-agnostic via <lint> placeholder

Follow-up to PR #2347 review (CodeRabbit): the pre-commit example still used
`npm run lint`, coupling it to npm after test/coverage were made runner-aware.
Add a `<lint>` column to the Step 0 runner matrix (npm run lint / pnpm lint /
yarn lint / bun run lint) and change the Pre-Commit Hook example to
`<test> && <lint>`. Applied to both the canonical and Codex-subset copies.

* chore: re-trigger CI (flaky windows/node20 npm cell)
This commit is contained in:
JongHyeok Park 2026-06-30 10:38:33 +09:00 committed by GitHub
parent 3a46c82b0c
commit 7976e6faf2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 128 additions and 14 deletions

View File

@ -48,6 +48,34 @@ ALWAYS write tests first, then implement code to make tests pass.
## TDD Workflow Steps ## TDD Workflow Steps
### Step 0: Detect the Test Runner
Do not assume `npm test`. The commands in the steps and examples below use `<test>`, `<test-watch>`, and `<coverage>` as placeholders for the project's actual runner. Resolve them once before starting:
1. **Run the package-manager detector** (ships with ECC):
```bash
node scripts/setup-package-manager.js --detect
```
It resolves the package manager (npm / pnpm / yarn / bun) from, in order: `CLAUDE_PACKAGE_MANAGER`, `.claude/package-manager.json`, the `package.json` `packageManager` field, the lockfile, then global config.
2. **Distinguish the package manager from the test runner — they are not the same.** A project can use Bun to install dependencies yet still run Jest or Vitest. Inspect `package.json` `scripts.test` and the test files:
- `scripts.test` invokes `jest` / `vitest` -> run through the detected PM (`npm test`, `pnpm test`, `yarn test`, or `bun run test`).
- `scripts.test` is `bun test`, or test files `import { test, expect } from "bun:test"`, or there is no jest/vitest config but Bun is present -> use **Bun's native runner** (`bun test`). See [Bun Native Test Pattern](#bun-native-test-pattern-buntest) below.
Runner command matrix:
| Runner | `<test>` | `<test-watch>` | `<coverage>` | `<lint>` |
|--------|----------|----------------|--------------|----------|
| npm | `npm test` | `npm test -- --watch` | `npm run test:coverage` | `npm run lint` |
| pnpm | `pnpm test` | `pnpm test --watch` | `pnpm test:coverage` | `pnpm lint` |
| yarn | `yarn test` | `yarn test --watch` | `yarn test:coverage` | `yarn lint` |
| Bun (script runs jest/vitest) | `bun run test` | `bun run test --watch` | `bun run test:coverage` | `bun run lint` |
| Bun (native `bun:test`) | `bun test` | `bun test --watch` | `bun test --coverage` | `bun run lint` |
> `bun test` (Bun's built-in runner) is **not** the same as `bun run test` (which runs the `package.json` `test` script). Picking the wrong one is a common failure — e.g. invoking Jest through `npx`/`bun run` in an ESM-only project breaks, while `bun test` runs the suite natively. Confirm which the project expects before the RED gate, then substitute `<test>` / `<coverage>` everywhere `npm test` appears below.
### Step 1: Write User Journeys ### Step 1: Write User Journeys
``` ```
As a [role], I want to [action], so that [benefit] As a [role], I want to [action], so that [benefit]
@ -82,7 +110,7 @@ describe('Semantic Search', () => {
### Step 3: Run Tests (They Should Fail) ### Step 3: Run Tests (They Should Fail)
```bash ```bash
npm test <test>
# Tests should fail - we haven't implemented yet # Tests should fail - we haven't implemented yet
``` ```
@ -98,7 +126,7 @@ export async function searchMarkets(query: string) {
### Step 5: Run Tests Again ### Step 5: Run Tests Again
```bash ```bash
npm test <test>
# Tests should now pass # Tests should now pass
``` ```
@ -111,7 +139,7 @@ Improve code quality while keeping tests green:
### Step 7: Verify Coverage ### Step 7: Verify Coverage
```bash ```bash
npm run test:coverage <coverage>
# Verify 80%+ coverage achieved # Verify 80%+ coverage achieved
``` ```
@ -144,6 +172,35 @@ describe('Button Component', () => {
}) })
``` ```
### Bun Native Test Pattern (`bun:test`)
When the project uses Bun's built-in runner (see [Step 0](#step-0-detect-the-test-runner)), import from `bun:test` and run with `bun test` — not `bun run test`. The API is Jest-like, so `describe` / `it` / `expect` and most matchers carry over. See the `bun-runtime` skill for runtime, install, and bundler details.
```typescript
import { describe, it, expect, mock } from 'bun:test'
import { searchMarkets } from './search'
describe('searchMarkets', () => {
it('returns an empty list for an empty query', async () => {
expect(await searchMarkets('')).toEqual([])
})
it('sorts results by similarity score', async () => {
const results = await searchMarkets('election')
expect(results).toEqual([...results].sort((a, b) => b.score - a.score))
})
})
```
```bash
bun test # run once (RED/GREEN gate)
bun test --watch # watch mode during development
bun test --coverage # coverage report
```
- Mock modules with `mock.module(...)` / `mock(...)` from `bun:test` instead of `jest.mock(...)`.
- Configure coverage thresholds in `bunfig.toml` under `[test]` (e.g. `coverageThreshold`) rather than the Jest `coverageThresholds` config block.
### API Integration Test Pattern ### API Integration Test Pattern
```typescript ```typescript
import { NextRequest } from 'next/server' import { NextRequest } from 'next/server'
@ -292,7 +349,7 @@ jest.mock('@/lib/openai', () => ({
### Run Coverage Report ### Run Coverage Report
```bash ```bash
npm run test:coverage <coverage>
``` ```
### Coverage Thresholds ### Coverage Thresholds
@ -363,21 +420,21 @@ test('updates user', () => {
### Watch Mode During Development ### Watch Mode During Development
```bash ```bash
npm test -- --watch <test-watch>
# Tests run automatically on file changes # Tests run automatically on file changes
``` ```
### Pre-Commit Hook ### Pre-Commit Hook
```bash ```bash
# Runs before every commit # Runs before every commit
npm test && npm run lint <test> && <lint>
``` ```
### CI/CD Integration ### CI/CD Integration
```yaml ```yaml
# GitHub Actions # GitHub Actions
- name: Run Tests - name: Run Tests
run: npm test -- --coverage run: <coverage>
- name: Upload Coverage - name: Upload Coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3
``` ```

View File

@ -85,6 +85,34 @@ ALWAYS write tests first, then implement code to make tests pass.
## TDD Workflow Steps ## TDD Workflow Steps
### Step 0: Detect the Test Runner
Do not assume `npm test`. The commands in the steps and examples below use `<test>`, `<test-watch>`, and `<coverage>` as placeholders for the project's actual runner. Resolve them once before starting:
1. **Run the package-manager detector** (ships with ECC):
```bash
node scripts/setup-package-manager.js --detect
```
It resolves the package manager (npm / pnpm / yarn / bun) from, in order: `CLAUDE_PACKAGE_MANAGER`, `.claude/package-manager.json`, the `package.json` `packageManager` field, the lockfile, then global config.
2. **Distinguish the package manager from the test runner — they are not the same.** A project can use Bun to install dependencies yet still run Jest or Vitest. Inspect `package.json` `scripts.test` and the test files:
- `scripts.test` invokes `jest` / `vitest` -> run through the detected PM (`npm test`, `pnpm test`, `yarn test`, or `bun run test`).
- `scripts.test` is `bun test`, or test files `import { test, expect } from "bun:test"`, or there is no jest/vitest config but Bun is present -> use **Bun's native runner** (`bun test`). See [Bun Native Test Pattern](#bun-native-test-pattern-buntest) below.
Runner command matrix:
| Runner | `<test>` | `<test-watch>` | `<coverage>` | `<lint>` |
|--------|----------|----------------|--------------|----------|
| npm | `npm test` | `npm test -- --watch` | `npm run test:coverage` | `npm run lint` |
| pnpm | `pnpm test` | `pnpm test --watch` | `pnpm test:coverage` | `pnpm lint` |
| yarn | `yarn test` | `yarn test --watch` | `yarn test:coverage` | `yarn lint` |
| Bun (script runs jest/vitest) | `bun run test` | `bun run test --watch` | `bun run test:coverage` | `bun run lint` |
| Bun (native `bun:test`) | `bun test` | `bun test --watch` | `bun test --coverage` | `bun run lint` |
> `bun test` (Bun's built-in runner) is **not** the same as `bun run test` (which runs the `package.json` `test` script). Picking the wrong one is a common failure — e.g. invoking Jest through `npx`/`bun run` in an ESM-only project breaks, while `bun test` runs the suite natively. Confirm which the project expects before the RED gate, then substitute `<test>` / `<coverage>` everywhere `npm test` appears below.
### Step 1: Write User Journeys ### Step 1: Write User Journeys
If a `*.plan.md` file was provided, extract the user journeys and acceptance criteria from that plan first. Only write new journeys for gaps the plan does not cover. If a `*.plan.md` file was provided, extract the user journeys and acceptance criteria from that plan first. Only write new journeys for gaps the plan does not cover.
@ -122,7 +150,7 @@ describe('Semantic Search', () => {
### Step 3: Run Tests (They Should Fail) ### Step 3: Run Tests (They Should Fail)
```bash ```bash
npm test <test>
# Tests should fail - we haven't implemented yet # Tests should fail - we haven't implemented yet
``` ```
@ -163,7 +191,7 @@ If the repository is under Git, stage the minimal fix now but defer the checkpoi
### Step 5: Run Tests Again ### Step 5: Run Tests Again
```bash ```bash
npm test <test>
# Tests should now pass # Tests should now pass
``` ```
@ -191,7 +219,7 @@ Recommended commit message format:
### Step 7: Verify Coverage ### Step 7: Verify Coverage
```bash ```bash
npm run test:coverage <coverage>
# Verify 80%+ coverage achieved # Verify 80%+ coverage achieved
``` ```
@ -261,6 +289,35 @@ describe('Button Component', () => {
}) })
``` ```
### Bun Native Test Pattern (`bun:test`)
When the project uses Bun's built-in runner (see [Step 0](#step-0-detect-the-test-runner)), import from `bun:test` and run with `bun test` — not `bun run test`. The API is Jest-like, so `describe` / `it` / `expect` and most matchers carry over. See the `bun-runtime` skill for runtime, install, and bundler details.
```typescript
import { describe, it, expect, mock } from 'bun:test'
import { searchMarkets } from './search'
describe('searchMarkets', () => {
it('returns an empty list for an empty query', async () => {
expect(await searchMarkets('')).toEqual([])
})
it('sorts results by similarity score', async () => {
const results = await searchMarkets('election')
expect(results).toEqual([...results].sort((a, b) => b.score - a.score))
})
})
```
```bash
bun test # run once (RED/GREEN gate)
bun test --watch # watch mode during development
bun test --coverage # coverage report
```
- Mock modules with `mock.module(...)` / `mock(...)` from `bun:test` instead of `jest.mock(...)`.
- Configure coverage thresholds in `bunfig.toml` under `[test]` (e.g. `coverageThreshold`) rather than the Jest `coverageThresholds` config block.
### API Integration Test Pattern ### API Integration Test Pattern
```typescript ```typescript
import { NextRequest } from 'next/server' import { NextRequest } from 'next/server'
@ -409,7 +466,7 @@ jest.mock('@/lib/openai', () => ({
### Run Coverage Report ### Run Coverage Report
```bash ```bash
npm run test:coverage <coverage>
``` ```
### Coverage Thresholds ### Coverage Thresholds
@ -480,21 +537,21 @@ test('updates user', () => {
### Watch Mode During Development ### Watch Mode During Development
```bash ```bash
npm test -- --watch <test-watch>
# Tests run automatically on file changes # Tests run automatically on file changes
``` ```
### Pre-Commit Hook ### Pre-Commit Hook
```bash ```bash
# Runs before every commit # Runs before every commit
npm test && npm run lint <test> && <lint>
``` ```
### CI/CD Integration ### CI/CD Integration
```yaml ```yaml
# GitHub Actions # GitHub Actions
- name: Run Tests - name: Run Tests
run: npm test -- --coverage run: <coverage>
- name: Upload Coverage - name: Upload Coverage
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3
``` ```