--- name: opsmill-dev-test-driving-bugs description: >- Writes a single failing test that reproduces a bug after its root-cause analysis is complete, before any fix is written. TRIGGER when: a bug has a completed root-cause analysis and you need the failing reproduction test, writing a test that proves a bug exists, the second step of the bug-fixing pipeline. DO NOT TRIGGER when: still triaging or diagnosing the bug → opsmill-dev-analyzing-bugs; implementing the fix once the test exists → opsmill-dev-fixing-bugs; general feature test-first work → superpowers test-driven-development. argument-hint: [pr] compatibility: >- Works in any git repo with a test suite; discovers testing conventions from the OpsMill `dev/` layout with auto-detection fallback. The draft-PR step needs `gh` and a GitHub remote. metadata: pipeline: bug-fixing (2 of 3 — analyze → test-drive → fix) version: 0.1.0 author: OpsMill user-invocable: true disable-model-invocation: true --- # Bug test-writer ## User Input ```text $ARGUMENTS ``` ## Your role You are a senior QA engineer writing a targeted failing test that reproduces a confirmed bug. `/opsmill-dev-analyzing-bugs` has already identified the root cause. Your job is to write **ONE** test that fails on the current code, proving the bug exists. ## Tool usage - Use the `Read` tool to read files -- do NOT use `cat` or `head`/`tail` in Bash. - Use the `Glob` tool to find files -- do NOT use `find` or `ls -R` in Bash. - Use the `Grep` tool to search file contents -- do NOT use `grep` or `rg` in Bash. - Reserve Bash for git commands, `gh` CLI, and commands that require shell execution. - *Shell* state (variables, `cd`) does **not** persist across separate Bash calls -- re-derive any shell value you reuse rather than relying on one set in an earlier step. The pipeline's logical flags like `OPEN_PR` are decisions you carry in your own reasoning, not shell variables, so they do persist across steps. ## Input and setup Parse `$ARGUMENTS` for an optional **`pr`** flag: if the word `pr` appears anywhere in the arguments (case-insensitive), set `OPEN_PR=true`. Otherwise `OPEN_PR=false`. The rest of `$ARGUMENTS` (issue number / URL / description) is only used to disambiguate when more than one analysis exists -- do **not** re-derive the slug from it. Discover the handoff file `/opsmill-dev-analyzing-bugs` wrote, using `Glob` for `.bug-analysis-*.md` in the repo root: - **No match:** inform the developer "Run `/opsmill-dev-analyzing-bugs ` first." and **STOP**. - **Exactly one match:** use it. - **Multiple matches:** pick the one whose `` best matches `$ARGUMENTS` (issue number, then slug words). If still ambiguous, list them and ask the developer which to use. Read the full analysis. Take the canonical `` and branch from its **`Key:`** and **`Branch:`** header fields rather than re-deriving a slug -- this keeps the branch/PR names consistent across steps. (If those fields are absent -- an older analysis -- fall back to the key embedded in the filename, `.bug-analysis-.md`, and `ai-bug-pipeline-`.) If the analysis file is missing required fields (Root cause, Affected files), inform the developer and **STOP**. ## Write the test Follow steps 0--9 below. **Step 10 (draft PR) runs only when `OPEN_PR=true`.** If `OPEN_PR=false`, the run stays **fully local**: stop after step 9 with the test committed on the local branch `` -- do NOT push and do NOT open a PR. Display the test results and the branch name, and tell the developer that `/opsmill-dev-fixing-bugs` will pick it up from that local branch. ### Step 0: Create working branch Detect the default branch and create a working branch from it: ```bash DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@') # Fallback when origin/HEAD is unset (shallow/CI clones, manually-added remotes): [ -z "$DEFAULT_BRANCH" ] && DEFAULT_BRANCH=$(git remote show origin 2>/dev/null | sed -n 's/.*HEAD branch: //p') [ "$DEFAULT_BRANCH" = "(unknown)" ] && DEFAULT_BRANCH="" # git prints "(unknown)" when remote HEAD is indeterminate DEFAULT_BRANCH=${DEFAULT_BRANCH:-main} git fetch origin "$DEFAULT_BRANCH" || { echo "Cannot fetch origin/$DEFAULT_BRANCH -- set the default branch manually and retry."; exit 1; } # Fetch the working branch into a tracking ref with an explicit refspec, so origin/ is # created even on single-branch/shallow clones (a bare `git fetch origin ` only updates # FETCH_HEAD there). Silently a no-op when the branch doesn't exist on the remote yet. git fetch origin "+refs/heads/:refs/remotes/origin/" 2>/dev/null # Resume the branch reconciled to the pushed tip if it exists on the remote; else a local-only # branch (a no-pr run not yet pushed); else create it fresh from the default branch: if git rev-parse --verify -q "origin/" >/dev/null; then git checkout -B "" "origin/" # remote tip wins -> never reuse a stale local copy elif git rev-parse --verify -q "" >/dev/null; then git checkout "" # local-only branch (no-pr run, never pushed) else git checkout -b "" "origin/$DEFAULT_BRANCH" fi ``` - `` is the `Branch:` value from the analysis (`ai-bug-pipeline-`) -- use it verbatim so `/opsmill-dev-fixing-bugs` finds the same branch. - Re-runs are deterministic -- see the inline comments for how each branch state (pushed, local-only, absent) is handled and why the remote tip wins. - If the **default-branch** `git fetch` fails, **STOP** and report it -- do not continue to write a test with no working branch (the `exit 1` only fails that one shell call). The working-branch fetch is best-effort (a missing remote branch is normal); a network failure there only means the branch can't be resumed from the remote, in which case STOP rather than recreating from default. ### Step 1: Read testing documentation Determine which area the bug lives in (backend, frontend, etc.) and read the project's testing conventions. Anchor on the `dev/` layout, falling back to detection: - Read root `AGENTS.md` and `dev/documentation-architecture.md` (if present) to map the bug to a code package. - Backend testing docs, if present: `dev/knowledge/**/testing.md` and `dev/guidelines/**/testing.md`. - Frontend testing docs, if present: `dev/guides/frontend/writing-unit-tests.md`, `writing-component-tests.md`, `writing-e2e-tests.md`. - **Fallback** (no `dev/` testing docs): detect the test runner and layout from the project itself -- `pyproject.toml` / `tox.ini` / `pytest.ini` (pytest), `package.json` scripts (vitest / jest / playwright), or a `Makefile` / `AGENTS.md` documenting the test command. ### Step 2: Discover existing test setup Read the test setup local to the target test directory and its parents (e.g. `conftest.py` files up the tree for pytest, shared setup/`setup.ts` for JS suites). Understand the available fixtures, their scopes, and setup/teardown patterns. Do NOT reinvent setup logic that already exists. ### Step 3: Check reusable utilities Look for the project's reusable test helpers, base classes, factories, and fakes/adapters (commonly under `tests/helpers/`, `tests/adapters/`, `tests/fake/`, or similar). Use these instead of writing your own test infrastructure. ### Step 4: Read existing tests Read 2--3 existing tests in the target test directory to learn the naming conventions, class structure, and import patterns before writing your own. ### Step 5: Choose the test type Pick the right test level using whatever guidance you found in step 1. Examples of common levels: - **Python / pytest:** unit, component, functional, integration. Reuse existing schema fixtures and helpers when available. - **Frontend:** unit/component (e.g. Vitest, colocated `.test.ts`), E2E (e.g. Playwright). Use a GIVEN/WHEN/THEN structure and existing factories when the project does. ### Step 6: Write the test Write a single targeted test that reproduces the bug: - **Assert the CORRECT/EXPECTED behavior.** The test fails because the bug prevents the expected behavior. Do NOT assert that the buggy behavior succeeds. For example: if the bug is "duplicate records can be created," assert that the second creation raises an error or that only one record exists -- this FAILS on buggy code and PASSES once fixed. - The test MUST exercise the **actual production code path**, not a reimplementation. Call the real functions/classes from the source. Do NOT copy production logic into the test. - Test **observable behavior**, not internal implementation details (test that an API returns wrong data or that a constraint is violated -- not that a constructor received specific kwargs). - **Test the affected code path** identified in the analysis "Affected files" section, not a lower-level abstraction it calls internally. A test that can pass without changing the affected code path is testing the wrong thing. - Place it in the correct test folder following project conventions (test files usually mirror source structure). If adding to an existing test file is more appropriate than creating a new one, do that. ### Step 7: Verify the test FAILS **CRITICAL: verify the test FAILS on the current code.** Run just this test using the project's detected runner (e.g. `uv run pytest path::Test::test -x -v`, `npm run test -- `, `npx playwright test path`). Note the `--` for `npm run`: without it npm swallows the path instead of forwarding it to the test runner. - If a test run takes more than ~5 minutes, kill it and investigate why. - The failure must be an **assertion error that directly relates to the root cause** (e.g. `AssertionError: Expected ValidationError but none was raised`, or `assert 2 == 1` when duplicates were found). - If the test **PASSES**, your assertions are wrong -- you are likely asserting buggy behavior. Flip them to assert what SHOULD happen. - If it fails for the **wrong reason** (import error, missing fixture, syntax error), fix those and re-run until it fails for the reason in the root cause. > **Gate (T2-verify · P1):** paste the actual failing test run proving it fails for the documented reason (not an import/collection error). See `../quality-gates/gates/primitives/evidence-before-done.md`. ### Step 8: Format and lint Run the project's formatter and linter on the test file(s) and fix any issues before committing. Use whatever the project defines (e.g. `uv run invoke format` / `uv run invoke lint`, `npx biome check --write .`, `ruff format`, `prettier`/`eslint`). Detect these from `AGENTS.md` / `Makefile` / `pyproject.toml` / `package.json`. ### Step 9: Commit test files Commit ONLY the test file(s). Do NOT touch production code. Stage files **by name** (`git add path/to/test_file`) -- never `git add .` or `git add -A`, as unrelated working-tree files would be committed by mistake. Commit message: `test: add failing test for `. ### Step 10: Open draft PR (only if `OPEN_PR=true`) > **Ship gate (T2 · P2 + P3) — before opening the draft PR or stamping `AGENT_TEST_COMPLETE`.** Run the ship gate per `../quality-gates/gates/primitives/independent-judge.md` (judge → on-FAIL STOP → R2 degrade → write receipt on PASS, all defined there). R1 criteria: the `.bug-analysis-.md` verbatim (the root cause the test must prove — NOT your summary). Artifact: the test diff. Forbidden evasions: the test-gate evasions from `../quality-gates/gates/primitives/anti-gaming.md`. The judge confirms the test genuinely exercises the bug (not trivially-passing, not always-failing). Push the branch and open a **draft** Pull Request against the repo's default branch: ```bash git push -u origin "" || { echo "Push rejected (non-fast-forward / protected / network). STOP and resolve before retrying -- do not open or edit a PR."; exit 1; } # Idempotent: on a resumed run a draft PR may already exist for this branch -- edit it # instead of creating a duplicate (gh pr create errors if one already exists). Default to # CREATE when the count is empty/non-numeric (a gh error), since create reports a real # duplicate clearly, whereas edit on a missing PR would just fail. PR_COUNT=$(gh pr list --head "" --state open --json number --jq 'length' 2>/dev/null) if [ "${PR_COUNT:-0}" -gt 0 ] 2>/dev/null; then gh pr edit "" --title "" --body-file <tmp-body-file> else # No --base: gh pr create defaults the base to the repo's default branch, so the # default-branch detection from Step 0 (which doesn't persist across Bash calls) isn't needed here. gh pr create --draft --title "<title>" --body-file <tmp-body-file> fi ``` Write the PR body to a temp file first (use the `Write` tool, e.g. under the system temp dir), then pass it via `--body-file`. - Title: `test: failing test for <key> -- <short description>` - PR body with this exact structure: ````markdown ## Analyst's findings (summary) > **Root cause:** <copied from analysis> > **Affected files:** > <copied from analysis> ## Replication test **Test file:** `path/to/test_file.ext` **Test name:** `test_name_here` **What it tests:** <one sentence explaining the observable behavior being asserted> **Verification:** Test confirmed FAILING on current code. **Failure reason:** <one sentence explaining HOW the test fails and why that proves the bug> <details><summary>Failure output (last 20 lines)</summary> ```text <paste the relevant failure output> ``` </details> ## Test expectations <what the test asserts and any edge cases it covers -- NOT how to fix the bug> <!-- AGENT_TEST_COMPLETE --> ```` If the analysis was triggered by a GitHub issue, post a short comment on the issue linking to the draft PR. ## Escalation If the test cannot be made to fail for the right reason after 3 attempts, inform the developer explaining what was tried and **STOP**. Do NOT open a PR or include the `AGENT_TEST_COMPLETE` marker. ## Quality gates Gates for this skill follow `../quality-gates/gates/gate-model.md`. `test-driving-bugs` is **Tier 2 — it writes a failing reproduction test and stamps `AGENT_TEST_COMPLETE` unconditionally** (in PR mode it also opens a draft PR; both paths require the full ship gate). | Gate | Step / trigger | Tier | Primitives | Pass criteria | On-fail | |---|---|---|---|---|---| | Test-fails-right | after writing the test | T2-verify | P1 | The new test FAILS for the documented reason. Paste the failing run. | STOP; fix the test | | Test-catches-bug | before opening the draft PR / stamping `AGENT_TEST_COMPLETE` | T2-ship | P2 + P3 | A fresh judge, given the `.bug-analysis-<key>.md` verbatim and the test diff, confirms the test actually exercises the bug (not trivially-passing, not always-failing). | STOP; do not stamp; fix and re-judge | ## Common mistakes | 🚩 Red flag | Do instead | | --- | --- | | Test passes on the current (buggy) code | You're asserting the buggy behavior — flip the assertions to the expected behavior so it FAILS now | | Copying production logic into the test | Call the real functions/classes from the source; test the actual code path | | Test passes without touching the affected files | Exercise the affected code path from the analysis, not a lower-level abstraction it calls | | Editing production code "to help the test" | This step writes tests only — production code is `/opsmill-dev-fixing-bugs`'s job | | `git add .` / `git add -A` | Stage the test file(s) by name | | Opening the PR / writing `AGENT_TEST_COMPLETE` after a failed attempt | The marker is the "test ready" signal — only emit it once the test fails for the right reason |