---
name: test-quality-assurance
description: Test quality assurance workflow for LeetCode problems - verifies reproducibility, fixes linting issues, and ensures test structure matches JSON templates. Use ONLY when user explicitly requests test quality assurance via /test-quality-assurance command.
---

# Test Quality Assurance Rules

## CRITICAL: Follow These Steps EXACTLY - No Deviations

### 1. Problem Resolution

- Use active file context or user-provided problem name
- If unclear, run: `uv run python -m leetcode_py.tools.check_test_cases --threshold=10 --max=1`

### 2. Test Reproducibility Verification Process

**MANDATORY 6-Step Process - Execute in Order:**

```bash
# Step 1: Backup original files
cp -r leetcode/{problem_name} leetcode/{problem_name}_backup

# Step 2: Regenerate from JSON template (use bake, NOT uv run)
bake p-gen -p {problem_name} -f

# Step 3: Restore original solution ONLY
cp leetcode/{problem_name}_backup/solution.py leetcode/{problem_name}/solution.py

# Step 4: Verify linting pass (CRITICAL for CI)
bake lint

# Step 5: Verify tests pass (expected to fail if solution is incomplete)
bake p-test -p {problem_name}
# NOTE: in the batch-problem-creation flow the solution is implemented BEFORE this
# step, so tests MUST pass here. A failure = real defect: wrong expected values in
# the JSON test_cases (most common — see problem-creation.md tree gotchas) or a
# wrong solution. Debug it; do not dismiss it as "incomplete solution".
# SPIN GUARD: 12-20 tiny cases finish in well under a minute. If a pytest run
# exceeds ~1 min wall, it is an INFINITE LOOP in the solution or helper, not
# slowness — kill it (check `ps aux` for a pytest process burning ~100% CPU),
# run the parametrize cases one by one to find the spinner, fix the root cause.
# Never trust a truncated/backgrounded output file as a pass: an exit code
# observed on a run that ended early after logging only the first test is NOT
# a pass (hit with 1265 — a spinning pytest burned ~34 min and an early-truncated
# "exit 0" output masqueraded as green).

# Step 6: Cleanup
rm -rf leetcode/{problem_name}_backup
```

### 3. What NOT to Do

- **NEVER edit cookiecutter templates** (`{{cookiecutter.problem_name}}/` files)
- **NEVER use `uv run python -m leetcode_py.cli.main gen`** - use `bake p-gen` instead
- **NEVER modify helpers.py manually** - let regeneration handle it
- **NEVER skip ty verification** - this is the main CI issue
- **NEVER assume tests will pass** - they may fail if solution is incomplete
- **NEVER use `null` in JSON templates** - use `None` for Python None values

### 4. What to Do

- **ALWAYS use `bake p-gen -p {problem_name} -f`** for regeneration
- **ALWAYS verify ty passes** before considering task complete
- **ALWAYS restore original solution** after regeneration
- **ALWAYS check JSON template** if ty fails (look for `assert_assert_` bugs)
- **ALWAYS use `None` not `null` in JSON templates** for Python None values

## Test Case Standards

### Coverage Requirements

- **Minimum 12 test cases** per problem
- **Edge cases**: Empty inputs, single elements, boundary values
- **Corner cases**: Maximum/minimum constraints, duplicates, sorted arrays
- **Normal cases**: Mixed scenarios with varied complexity

### JSON Format

- **CRITICAL**: Use `None` NOT `null` for Python None values in test cases
    - JSON templates use `None` directly: `"[1, None, 2]"` NOT `"[1, null, 2]"`
    - This ensures generated Python code passes linting (ruff/ty check for undefined name `null`)
- Use single quotes for Python strings: `'hello'` not `"hello"`
- Follow existing parametrize format
- Ensure valid Python list syntax in test_cases field

## Quick Commands

### CLI Commands (Recommended)

```bash
# Generate enhanced problem
uv run lcpy gen -s {problem_name} -o leetcode --force

# Test specific problem
bake p-test -p {problem_name}

# Lint check
bake lint
```

### Development Commands

```bash
# Find problems needing enhancement
uv run python -m leetcode_py.tools.check_test_cases --threshold=10

# Check all problems (no limit)
uv run python -m leetcode_py.tools.check_test_cases --threshold=10 --max=none

# Check with custom threshold
uv run python -m leetcode_py.tools.check_test_cases --threshold=12

# Generate from JSON template (uses uv run lcpy internally)
bake p-gen -p {problem_name} -f
```

## Common Issues & Solutions

### Issue: \`assert_assert_missing_number\` Error

**Cause**: JSON template has \`helpers*assert_name: "assert_missing_number"\` but template adds \`assert*\` prefix
**Solution**: Change JSON to \`helpers_assert_name: "missing_number"\` so template generates \`assert_missing_number\`

### Issue: ty Import Errors

**Cause**: Regenerated helpers.py doesn't match test imports
**Solution**: Use \`bake p-gen\` (not uv run) and verify JSON template is correct

### Issue: Tests Fail After Regeneration

**Expected**: Tests may fail if solution is incomplete (returns 0 or placeholder)
**Action**: This is normal - focus on ty passing, not test results
**Exception**: if the solution is already implemented (batch flow), failures are real
defects — fix the JSON test cases or the solution, then re-run the 6 steps

### Issue: pre-commit flags solution.py (B905 / E741 / RUF012 / N806 / ty list invariance)

**Cause**: `bake lint` during QA runs a narrower ruff config than the pre-commit hook, so `zip()` without `strict=` (B905), variable names like `l` (E741), mutable class-attribute defaults (RUF012), uppercase locals like `MOD` (N806), and list-invariance type mismatches in ty pass QA and surface only at batch finalization.

- Fix: `zip(s, t)` → `zip(s, t, strict=True)`; rename `l`/`I`/`O` variables (`left_val`, etc.); class-level constant lists → tuples (`BELOW_20: tuple[str, ...] = (...)`) or annotate with `typing.ClassVar`; `MOD = 1_000_000_007` → lowercase `mod` (N806, recurs on every modulo problem); annotate accumulators with the full return element type (`result: list[TreeNode[int] | None]`, not `list[TreeNode[int]]` returned against `-> list[TreeNode[int] | None]` — ty rejects the narrowing under list invariance); a `float('-inf')` sentinel in an int-returning DP poisons derived values into `float` (ty `invalid-return-type`) — prefer a type-pure int sentinel like `-1` for non-negative domains (hit with 741 Cherry Pickup); a nested `dfs(node: TreeNode[int] | None, ...)` whose body touches `node.val`/`node.left`/`node.right` passes `bake lint` but fails pre-commit ty with one `unresolved-attribute` per access — ty does not narrow through the call site when `root` is `Optional`, so add `if node is None: return` at the top of the DFS (hit with 988, 7 errors at once). Generated helpers add four more: RUF005 list concatenation in return expressions → unpack instead (`[node.val, *left, *right]`, hit with 889); E731 lambda assigned to a name → rewrite as a `def` with return type (hit with 894); ty `unresolved-attribute` when calling `.to_list()` on a `TreeNode | None` element — `assert all(x is not None ...)` does NOT narrow a comprehension variable; filter into a new list (`roots = [t for t in result if t is not None]`) and assert equal length (hit with 894); ty does NOT narrow subscripts — `while vals[i] is not None` and even `assert vals[i] is not None` still yield `int | None` at the use site (`invalid-argument-type` in `helpers_content`); hoist to a local first (`v = vals[i]; assert v is not None; ... v`, hit with 428). All are fixed at the JSON level (`helpers_assert_body` / `helpers_content`) or in `solution.py`, never in other generated files
- Hit with 273 Integer to English Words: lookup-table lists (`BELOW_20 = [...]`) at class level flagged `RUF012 Mutable default value for class attribute`
- Hit with 552/576/629 (`MOD` → N806) and 652 (ty `invalid-return-type` on list invariance)
- E501 also fires on COMMENTS: a complexity comment listing per-method costs (`# Time: get O(index), add_at_index O(index), ...`) overflows col 100 — compress or split it (hit with 707 Design Linked List)
- Solution-defined doubly-linked `Node` classes (custom linked-list/design problems, e.g. 708 circular list, 716 MaxStack DLL) whose pointer fields are `Node | None` pass `bake p-gen` QA and `bake lint` but fail pre-commit ty with one `unresolved-attribute`/`invalid-assignment` error per pointer access — ty cannot narrow attribute fields, and the dead-node marker `node.prev = None` assignment also fails (`invalid-assignment`). Fix at the solution level with self-linking non-Optional fields: `self.next: Node = next if next is not None else self` in `__init__`, dead marker `node.prev = node.next = node` plus `node.prev is not node` liveness checks (hit with 708/716, 12 errors at once). Recurs on linked-list/design problems in the unscrapable queue (919, 428, 431, 510, 369)
- Prevention: write solutions ruff-clean from the start (see problem-creation.md, Batch Flow Notes section)
- Prevention (batch flow, where `bake lint` is orchestrator-owned and skipped in the QA chain): scoped lint is STILL REQUIRED before reporting PASS, and it must read the repo config — from the repo root run `uv run ruff check leetcode/{problem_name}`, `uv run ruff format --check leetcode/{problem_name}`, and `uv run ty check leetcode/{problem_name}` (uv resolves the pyproject ruff/ty settings, so these mirror what pre-commit's repo-wide run enforces). A bare `ruff check` outside `uv run`, or skipping the scoped lint entirely because the step was "not yours", lets SIM110/E501-class issues surface first at batch pre-commit and cost a fix + full pre-commit rerun (hit with 1849, whose agent skipped scoped lint and shipped a SIM110 `for`-loop-return that pre-commit caught)

### Issue: \`null\` vs \`None\` in JSON Templates

**Cause**: JSON template uses \`null\` which causes linting errors in generated Python code

- Error: \`F821 Undefined name 'null'\` from ruff/ty
- Generated test files contain \`null\` which is not valid Python

**Solution**: Update JSON template to use \`None\` instead of \`null\`

- Change: \`"([1, null, 2], 3, 1)"\` → \`"([1, None, 2], 3, 1)"\`
- This applies to \`test_cases\` list and \`playground_setup\` fields
- After fixing JSON, regenerate with \`bake p-gen -p {problem_name} -f\`
- Generated code will now pass linting without manual edits

### Issue: hand-invented inputs pass value-range checks but violate STRUCTURAL constraints

**Cause**: verifying each entry against value ranges (\`grid[i][j]\` is 0 or 1, \`nums[i] <= 10^4\`) is not enough — shape invariants stated in the constraints must be checked too. Hit with 827 Making A Large Island: the statement guarantees an \`n x n\` (square) grid, but a test case like \`[[1], [1]]\` is 2x1; it passes all value checks and then crashes matrix-indexing solutions with \`IndexError\`, which reads as a solution bug rather than a test-data bug

- **Fix**: include shape invariants (\`grid.length == grid[i].length\`, row/col counts, ordering guarantees like "graph[i] is strictly increasing") in the reference-implementation verification script — assert the STRUCTURE of every input, not just element values
- Drop or reshape any case that violates a structural constraint, even if the expected output "looks right"

## Success Criteria

- **ty passes** with no errors (CRITICAL for CI)
- **Test structure matches JSON template** exactly
- **Original solution preserved** (user's code intact)
- **helpers.py generated correctly** (no \`assert*assert*\` bugs)
- **Reproducibility verified** (can regenerate consistently)

## When to Use This Workflow

- GitHub Actions CI failures due to ty errors
- Test reproducibility verification requests
- Need to ensure test structure matches JSON template
- CI test failures in reproducibility checks
