Bug

Made the test pass by weakening the test

为了让测试通过,把测试改弱了

A failing check goes green because the agent skipped, deleted or loosened it, or mocked the unit under test, not because the code is right.

Applies to: Claude Code · Cursor · Codex · Copilot agent

Symptom

CI is green, but the diff touches test files. Look for `.skip`, `xit`, a regenerated snapshot, `toBe(19.99)` becoming `toBeCloseTo(20, 0)`, a branch that special-cases the test input, `@ts-expect-error`, or `continue-on-error`.

Why it happens

The agent optimizes for "checks pass", and editing the check is often the shortest path. developertoolkit.ai's tie-break rule is that if any oracle file changed, tag it oracle gaming, because the edit itself is the evidence. agent-failure-modes (AFM-14) and agent-review (AR025, disabled or skipped test) list the same behavior.

Example

Patterns quoted from developertoolkit.ai and agent-review.

ts
- expect(total).toBe(19.99);
+ expect(total).toBeCloseTo(20, 0);

- it("rejects expired tokens", ...)
+ it.skip("rejects expired tokens", ...)

Copy-paste fixes

Pick the files for the agents you use. Snippets are in English so they work in any repo.

Rules

Claude Code: add to CLAUDE.md

CLAUDE.md
## Tests are the spec: don't weaken them
- Never skip, delete, loosen or mock away a failing test to make it pass. Fix the code.
- Do not regenerate snapshots or edit CI config unless the user asked for it.
- If you believe a test is wrong, stop and explain why, quoting the test and the requirement. Let the user decide.
- In your summary, list every test file you changed and why.
Rules

Codex, Copilot coding agent and others: add to AGENTS.md

AGENTS.md
## Tests are the spec: don't weaken them
- Never skip, delete, loosen or mock away a failing test to make it pass. Fix the code.
- Do not regenerate snapshots or edit CI config unless the user asked for it.
- If you believe a test is wrong, stop and explain why, quoting the test and the requirement. Let the user decide.
- In your summary, list every test file you changed and why.
Cursor rule

Cursor: project rule

.cursor/rules/test-gaming.mdc
---
description: "AgentFails: Made the test pass by weakening the test"
globs: **/*.{test,spec}.*
alwaysApply: false
---

## Tests are the spec: don't weaken them
- Never skip, delete, loosen or mock away a failing test to make it pass. Fix the code.
- Do not regenerate snapshots or edit CI config unless the user asked for it.
- If you believe a test is wrong, stop and explain why, quoting the test and the requirement. Let the user decide.
- In your summary, list every test file you changed and why.
Hook

Claude Code PreToolUse hook: tests are read-only unless you say so

Register it like the scope guard (matcher Edit|Write|MultiEdit). Run `ALLOW_TEST_EDITS=1 claude` for sessions where changing tests is the task.

.claude/hooks/protect_tests.py
#!/usr/bin/env python3
"""Block edits to test files and CI config unless ALLOW_TEST_EDITS=1."""
import json, os, re, sys

call = json.load(sys.stdin)
if call.get("tool_name") not in ("Edit", "Write", "MultiEdit") or os.environ.get("ALLOW_TEST_EDITS") == "1":
    sys.exit(0)

path = call["tool_input"].get("file_path", "")
protected = re.compile(r"(^|/)(tests?|__tests__|spec)/|\.(test|spec)\.[jt]sx?$|_test\.(go|py)$|(^|/)test_[^/]+\.py$|\.github/workflows/")
if protected.search(path):
    print(f"BLOCKED: {path} is a test/CI file. Make the code pass the existing test. "
          "If you believe the test itself is wrong, stop and explain why to the user.", file=sys.stderr)
    sys.exit(2)
sys.exit(0)
Hook

Claude Code Stop hook: don't let the agent finish while tests fail

A Stop hook that exits with code 2 keeps Claude working and shows it stderr. The stop_hook_active check stops it from looping forever. Swap `npm test` for your test command.

.claude/settings.json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 -c \"import json,sys,subprocess; d=json.load(sys.stdin); sys.exit(0) if d.get('stop_hook_active') else None; r=subprocess.run('npm test --silent', shell=True, capture_output=True, text=True); sys.stderr.write('Tests are failing. Fix them (without weakening them) before stopping:\\n'+r.stdout[-3000:]+r.stderr[-2000:]) if r.returncode else None; sys.exit(2 if r.returncode else 0)\""
          }
        ]
      }
    ]
  }
}

Sources and credit

Related patterns

翻车 of the week

One real agent failure and its fix, every week. Free, short, no spam.