| name | bug-reproduce-test |
| description | Reproduce a php-src bug captured in SPEC.md and convert it into a failing .phpt test. Use when the user says "reproduce the bug", "write a failing test for this issue", "/bug-reproduce-test", "make a phpt that captures GH-12345", "let's prove the bug exists", or otherwise wants to lock down a reproduction case for a php-src bug after /bug-context has been run. The test MUST fail on the current build — that is the post-guardrail. |
bug-reproduce-test
Second step of the bug-fix flow. Reads the Context already in SPEC.md, builds a minimal PHP reproduction, converts it into a .phpt test under $PHP_SRC_DIR, runs it, and asserts it FAILS (TDD).
Inputs
- SPEC.md in the task directory resolved per
_shared/task-dir.md (env var → .current → cwd) with type: bug and Context phase done. Refuse otherwise.
$PHP_SRC_DIR with a built sapi/cli/php.
What it produces
repro.php — minimal standalone reproduction script in the task directory, for the user's eyeballing
$PHP_SRC_DIR/<tests-path>/ghNNNNN.phpt — the new test, following the conventions of the surrounding directory
- Updated SPEC.md: Reproduction phase populated, status →
done once post-guardrail is green
Phase owned
Reproduction.
Steps this skill adds:
repro-script — minimal repro.php reproduces the issue against $PHP_SRC_DIR/sapi/cli/php. Notes observed vs expected.
phpt-failing — .phpt test created at the appropriate path; run-tests.php returns failure.
Workflow
1. Pre-guardrails
- SPEC.md exists and has
type: bug. If type: idea, refuse with: idea SPECs don't use bug-reproduce-test.
- Context phase is
done. If not, refuse with: Run /bug-context first.
- Research phase soft-check: if a Research phase exists with status
todo, print one warning line and proceed:
⚠ Research phase still todo. Run /research first for prior-art (or mark it `skipped` in SPEC.md to silence this warning).
done, skipped, or blocked → no warning. Status done → read Research findings; they may already pin a subsystem or hint at a prior test that captures the bug.
- Reproduction phase is
todo or in-progress. If done, refuse with: Reproduction phase is already done. Run /bug-fix next. If blocked, refuse with: Reproduction is blocked: <reason>. Resolve before retrying.
$PHP_SRC_DIR/sapi/cli/php exists. If missing, do not auto-build — output the build commands and stop:
$PHP_SRC_DIR/sapi/cli/php missing. Build it first:
cd "$PHP_SRC_DIR"
./buildconf
./configure --enable-debug
make -j$(nproc)
- target_branch matches the checked-out branch. Read
target_branch from SPEC.md frontmatter. If set (not null) and not equal to git -C "$PHP_SRC_DIR" rev-parse --abbrev-ref HEAD, refuse with:
SPEC.md target_branch is <X>; $PHP_SRC_DIR is on <Y>.
Switch with:
git -C "$PHP_SRC_DIR" checkout <X>
cd "$PHP_SRC_DIR" && make -j$(nproc)
…or update target_branch in SPEC.md if the resolution was wrong.
Do not auto-switch branches (CLAUDE.md: "Executing actions with care" — branch swaps trigger rebuilds the user may not want unprompted).
- If
target_branch: null (resolver couldn't decide), refuse with: target_branch is null in SPEC.md. Resolve the Open Question raised by /bug-context before reproducing.
2. Build a minimal reproduction
Read Context in SPEC.md. Use the snippet there if present; otherwise construct one from the description.
Write repro.php in the task directory (not in php-src). Run it:
"$PHP_SRC_DIR/sapi/cli/php" -n "$TASK_DIR/repro.php"
Record observed vs expected. If outputs match expected behaviour, the bug doesn't reproduce — mark repro-script as blocked with the evidence and stop. Note that the bug may already be fixed on the current ref.
Mark repro-script as done once the observed output clearly demonstrates the bug.
3. Convert to .phpt
Pick the test path based on the subsystem hint from Context:
| Subsystem | Test location pattern |
|---|
ext/<name>/ | ext/<name>/tests/ghNNNNN.phpt |
Zend/ | Zend/tests/ghNNNNN.phpt |
main/streams/ etc. | tests/<area>/ghNNNNN.phpt |
sapi/<name>/ | sapi/<name>/tests/ghNNNNN.phpt |
Name with the GH issue number: ghNNNNN.phpt. If a more descriptive name fits the surrounding directory's convention (look at sibling files), use it — but the GH number should appear in --TEST--.
Use --EXPECT-- when the output is deterministic. Switch to --EXPECTF-- only when the output legitimately contains a path, version, address, or other variable substring. See _shared/phpt-format.md for the full schema.
When target_branch is not master, add a --SKIPIF-- clause encoding the minimum version derived from the branch name per _shared/branch-detection.md § 4. For target_branch: PHP-8.3:
--SKIPIF--
<?php if (version_compare(PHP_VERSION, '8.3.0', '<')) die('skip 8.3+ only'); ?>
Derive the version string from the branch name, not from a hardcoded literal. When target_branch: master, omit --SKIPIF--.
Skeleton:
--TEST--
<short title> (GH-NNNNN)
--FILE--
<?php
<minimal code from repro.php>
?>
--EXPECT--
<the OUTPUT WE WANT after the fix is applied>
The expected output is the post-fix expectation, not what the buggy code emits today. That's why the test fails now and passes after the fix.
4. Run the test — expect failure
cd "$PHP_SRC_DIR"
sapi/cli/php run-tests.php -v <test-path-relative-to-PHP_SRC_DIR>
Inspect the result:
- FAIL → expected. Mark
phpt-failing as done, Reproduction phase as done. Append Progress Log.
- PASS → the test doesn't capture the bug. Mark
phpt-failing as blocked with: Expected output matches current buggy output — adjust the --EXPECT-- to reflect post-fix behaviour. Stop.
- SKIP / BORK → infrastructure issue. Mark
blocked with the runner's reason.
The .diff file shows the divergence — if it matches the documented bug behaviour, you're good.
5. Update SPEC.md
Populate Reproduction phase steps with the file paths and command outputs. Append one Progress Log line:
2026-05-16 14:18 — bug-reproduce-test — phpt-failing at <path> FAILs as expected; .diff shows <one-line summary>
6. Report
Reproduction phase complete.
- repro.php: <task-dir>/repro.php
- new test: $PHP_SRC_DIR/<tests-path>/ghNNNNN.phpt — FAIL (expected)
- .diff: $PHP_SRC_DIR/<tests-path>/ghNNNNN.diff
Next: run /bug-fix
Boundaries
- No source-code changes in
Zend/, main/, ext/<name>/<src>, etc. Only the test file.
- Stage the new
.phpt with git add <path> after run-tests.php FAILs. Do NOT commit — /bug-fix will commit test + fix together as one commit. Never git push, git remote *, or git pull.
- No
--CREDITS-- in the new test. Git tracks authorship.
- No new test infrastructure: use only
--SKIPIF--, --EXTENSIONS--, --INI--, --FILE--, --EXPECT[F]--, --CLEAN--. Don't introduce a new test-runner pattern just for this bug.
Edge cases
| Situation | Handling |
|---|
| Bug only reproduces under specific INI | Add --INI-- section. |
| Bug only reproduces with a specific extension | Add --EXTENSIONS--. |
| Bug requires a specific PHP version | The --SKIPIF-- is already emitted when target_branch != master (see § 3). For tighter constraints than the branch minimum, narrow the version_compare check and document the constraint in Context. |
| Output contains paths/PIDs/addresses | Use --EXPECTF-- with %s/%i/%a. Prefer narrow specifiers. |
| Bug needs interactive input | Use --CAPTURE_STDIO-- + --FILE-- driving stdin via a heredoc. If genuinely interactive (e.g. phpdbg), mark phpt-failing as blocked with reason; document a manual repro script. |
| Test path's parent directory doesn't exist | Create it (mkdir -p); flag in the step body. |
| Sibling tests use snake_case names | Match their style (some_feature_001.phpt); still mention GH-NNNNN in --TEST--. |
| Reproduction depends on a build of a specific branch | The new target_branch pre-guardrail (§ 1) already catches a mismatch. If target_branch was set wrong, the user updates SPEC.md and re-runs; if right, the user switches $PHP_SRC_DIR. Never switch on their behalf. |
Quality checklist
Out of scope
- Locating the offending C code —
/bug-fix
- Fixing the bug —
/bug-fix
- Running the broader regression suite —
/bug-verify