Codex Characterization Tests for Legacy Code: Lock In Current Behavior, Target Edge Cases, Zero Behavior Changes
Has Codex write characterization tests for untested legacy code: it first reads and documents what the code 'actually does right now' (bugs included), systematically covers the happy path plus edge cases and error paths, and is strictly forbidden from modifying the code under test.
Write tests for an EXISTING piece of code that currently has insufficient or no test coverage. Your job is to capture and lock in its CURRENT behavior — a safety net for future refactoring — NOT to fix or change it.
## Target
Code under test: {{TARGET}}
Test framework / command: {{TEST_FRAMEWORK}}
## Method
**1. Understand actual behavior first.** Read the target code AND its real callers in this repo. Determine what it *actually does today* for each input class — including any surprising or arguably-buggy behavior. You are documenting reality, not the ideal. Use the project's existing test files as a style template (read AGENTS.md / CLAUDE.md for conventions and the test command).
**2. Cover these categories systematically** (only those that apply):
- Happy path: the normal, expected inputs and their outputs.
- Boundaries: empty string/array/collection, zero, negative, one, the max, off-by-one neighbors of any limit.
- Null / undefined / None / missing-key inputs where they can reach the code.
- Error paths: inputs that should throw/reject/return an error — assert the error happens (and its type/message if the code guarantees one).
- Type/format edges relevant to the domain (unicode, whitespace, very large numbers, timezone/locale if dates, etc.).
- For pure functions: a few representative input→output pairs that pin the contract.
**3. Assert on real, observed values.** Run the code (or reason precisely from it) to get the ACTUAL output, then assert that. Do NOT assert what you think it *should* return — assert what it *does* return. If a current output looks like a bug, still write the test that captures it, and add a comment `// NOTE: current behavior may be a bug — locked for now` so a human can decide later. Never silently 'correct' it in the assertion.
**4. Keep tests independent and clear.** One behavior per test, descriptive names that state the scenario (e.g. `returns_empty_for_blank_input`), no shared mutable state between tests, no network/filesystem unless the unit genuinely needs it (mock those). Use the framework's idioms already present in the repo.
## Hard rules
- Do NOT modify the code under test. Not one line. If you believe it has a bug, list it at the end under 'Suspected bugs (not fixed)' — do not touch the source.
- Every test must actually run and pass against the current code. Run `{{TEST_FRAMEWORK}}` and paste the result proving they pass. A test that asserts wrong-expected values and fails is useless here.
- No trivial/tautological tests (`expect(true).toBe(true)`, asserting a mock was called with no real coverage). Each test must exercise real logic.
- Tell me the coverage gaps you deliberately left and why, so I know what's NOT protected.
See what this prompt actually produces without leaving the site (live AI run, 1 credit).
Don't just copy-paste — download and drop it at ~/.claude/skills/characterization-tests-legacy-code/SKILL.md and every future session can use it automatically.
mkdir -p ~/.claude/skills/characterization-tests-legacy-code && mv ~/Downloads/SKILL.md ~/.claude/skills/characterization-tests-legacy-code/SKILL.mdNew-Item -ItemType Directory -Force "$env:USERPROFILE\.claude\skills\characterization-tests-legacy-code" | Out-Null; Move-Item "$env:USERPROFILE\Downloads\SKILL.md" "$env:USERPROFILE\.claude\skills\characterization-tests-legacy-code\SKILL.md"When to use it: when you're taking over legacy code, about to refactor, or a module is too scary to touch because it has no tests. Why it works: the essence of characterization testing is 'lock in the current state, not the ideal one' — a lot of AI will take it upon itself to write 'it should return X' assertions that immediately fail. This prompt hammers three times on 'assert the actual output, not what you think it should be,' locks in bugs too (with a NOTE comment left for a human to decide), strictly forbids changing the code under test, and requires pasting the passing test output. The resulting test net can genuinely catch regressions when you refactor later. Technique: use a workspace-write sandbox in Codex so it can actually run the code to get ground truth before asserting on it; at the end it lists 'gaps deliberately left uncovered' and 'suspected bugs (left unfixed)' — both lists are valuable in their own right.
// src/pricing.test.ts — characterization tests for calcTax() test('applies 5% for standard rate', () => expect(calcTax(100,'std')).toBe(105)) test('returns input unchanged for unknown rate code', () => expect(calcTax(100,'???')).toBe(100)) // NOTE: current behavior may be a bug — locked for now test('negative amount yields negative tax (not clamped)', () => expect(calcTax(-100,'std')).toBe(-105)) test('throws on null amount', () => expect(() => calcTax(null,'std')).toThrow(TypeError)) $ vitest run src/pricing.test.ts ✓ 4 passed Suspected bugs (not fixed): 負數金額未夾零、未知 rate code 靜默回傳原值不報錯。 Coverage gaps left: 未測 currency rounding(需 i18n 環境、另案處理)。
[TARGET]要補測試的目標(檔案路徑、類別或函式名,如 src/pricing.ts 的 calcTax 函式)
[TEST_FRAMEWORK]測試框架與執行指令,如 vitest run、pytest tests/、go test ./pkg/...
填下面的欄位,上方 prompt 會即時替換 [方括號] 內容。填好後按「複製組好的 prompt」直接丟進工具。
Write tests for an EXISTING piece of code that currently has insufficient or no test coverage. Your job is to capture and lock in its CURRENT behavior — a safety net for future refactoring — NOT to fix or change it.
## Target
Code under test: {{TARGET}}
Test framework / command: {{TEST_FRAMEWORK}}
## Method
**1. Understand actual behavior first.** Read the target code AND its real callers in this repo. Determine what it *actually does today* for each input class — including any surprising or arguably-buggy behavior. You are documenting reality, not the ideal. Use the project's existing test files as a style template (read AGENTS.md / CLAUDE.md for conventions and the test command).
**2. Cover these categories systematically** (only those that apply):
- Happy path: the normal, expected inputs and their outputs.
- Boundaries: empty string/array/collection, zero, negative, one, the max, off-by-one neighbors of any limit.
- Null / undefined / None / missing-key inputs where they can reach the code.
- Error paths: inputs that should throw/reject/return an error — assert the error happens (and its type/message if the code guarantees one).
- Type/format edges relevant to the domain (unicode, whitespace, very large numbers, timezone/locale if dates, etc.).
- For pure functions: a few representative input→output pairs that pin the contract.
**3. Assert on real, observed values.** Run the code (or reason precisely from it) to get the ACTUAL output, then assert that. Do NOT assert what you think it *should* return — assert what it *does* return. If a current output looks like a bug, still write the test that captures it, and add a comment `// NOTE: current behavior may be a bug — locked for now` so a human can decide later. Never silently 'correct' it in the assertion.
**4. Keep tests independent and clear.** One behavior per test, descriptive names that state the scenario (e.g. `returns_empty_for_blank_input`), no shared mutable state between tests, no network/filesystem unless the unit genuinely needs it (mock those). Use the framework's idioms already present in the repo.
## Hard rules
- Do NOT modify the code under test. Not one line. If you believe it has a bug, list it at the end under 'Suspected bugs (not fixed)' — do not touch the source.
- Every test must actually run and pass against the current code. Run `{{TEST_FRAMEWORK}}` and paste the result proving they pass. A test that asserts wrong-expected values and fails is useless here.
- No trivial/tautological tests (`expect(true).toBe(true)`, asserting a mock was called with no real coverage). Each test must exercise real logic.
- Tell me the coverage gaps you deliberately left and why, so I know what's NOT protected.
Suno Engineer's Mindset: 4 Steps to a Song That Doesn't Sound Like AI
A studio engineer's breakdown of Suno's fatal weaknesses (fried vocals, high-frequency artifacts), plus a 4-step DAW workflow and a Suno Studio cleanup prompt.
5 Claude Weekly Workflows That Stuck After 6 Months
Proposal generator / meeting processor / content repurposer / Friday review / shutdown reset — out of 40 I tried, only these 5 survived, each saving 30+ minutes per run.