Test Fixer — Bulk Test Failure Resolution
When to Use
- 5+ failing tests in an existing test suite
- Collection errors masking real test failures
- Assertion mismatches between tests and evolved source code
- Async test framework misconfiguration
Critical: Fix Collection Errors FIRST
Collection errors (ImportError, ModuleNotFoundError) cause pytest to stop collecting and skip subsequent test files. The "2 errors" you see initially may mask 50+ real failures.
Workflow:
- Run
pytest --collect-only -qto see only collection errors - Fix import paths, missing dependencies, pytest.ini markers one at a time
- Re-run
pytest -q --tb=line | grep FAILEDto reveal the FULL failure count
# Step 1: Check collection, then full run
uv run python -m pytest tests/ --collect-only -q 2>&1 | grep -E "^ERROR"
# Fix each one
uv run python -m pytest tests/ -q --tb=line 2>&1 | grep "^FAILED"
Strategy: Categorize Before Fixing
Group failures by root cause, then fix each category:
- Async/framework config — 1-2 file changes fix many tests (install pytest-asyncio, add markers)
- Import paths — hardcoded /mnt/github/github/... to /mnt/local-analysis/... (1 fix per file)
- Assertion mismatches — output text changed, method names changed (1+ fix per assertion)
- Missing methods — tests call APIs that no longer exist (requires source change or test deletion)
- Mock failures — exception types don't match what source catches
Fix ordering: categories 1→2 fix many at once, then 3→5 per-file.
Safe Patching Patterns
NEVER use replace_all=True with short strings
# CATASTROPHIC: replaces ALL occurrences including in unrelated methods
patch(mode="replace", old_string=' assert "25%" in output', replace_all=True)
DO include 3+ lines of surrounding context
# SAFE: context makes it unique
patch(mode="replace", old_string=""" progress.update_percentage(25)
output = mock_stdout.getvalue()
assert "25%" in output""", new_string="""...""")
Use write_file for complex multi-fix scripts
When patch() fails due to string escaping (multiline with quotes, braces), write a fix script:
# Write to file, then execute
write_file("/tmp/fix_tests.py", fix_script_content)
terminal(command="python3 /tmp/fix_tests.py")
WARNING: Python f-strings break when the script content contains {}. Use r"""...""" raw strings for the script content to avoid this.
Alternative: sed on line numbers
sed -i '124old_line/new_line/' tests/agent_os/commands/test_file.py
Common Failure Patterns and Fixes
Async tests: "async def functions are not natively supported"
- Fix:
uv pip install pytest-asyncio, addasyncio_mode = autoto pytest.ini, ensurepytest.mark.asynciomarker exists
ModuleNotFoundError with hardcoded paths
- Fix: Convert hardcoded paths to relative using
os.path.join(os.path.dirname(__file__), '...')
Assertion mismatches (expected X, got Y)
- Probe actual API first:
uv run python3 -c "from module import Class; c = Class(); print(repr(c.method()))" - Then update test to match actual behavior — don't guess
AttributeError: object has no attribute
- Source API evolved; method removed. Either restore it in source or delete/skip the test.
execute() crashes: "unhashable type: 'slice'"
- Root cause: source
execute(args: List[str])calledparser.parse(args)which doesargs[1:], but tests passdict. - Fix source to accept both:
if isinstance(args, dict): parsed = dict_to_args(args); else: parsed = self.parser.parse(args)
Subagent Gotchas for Test Fixing
delegate_task IS viable for test fixing — with caveats
The subagent's patch/terminal calls DO execute on the real filesystem (not a sandbox). However:
- Subagent summaries are USELESS — 1-2 sentences despite consuming M of tokens. Always
git diffor re-run tests after. - Subagents OVERREACH — they modify unrelated files (result ymls, agent configs). Always
git diff --statandgit checkout -- <unrelated>. - Max iterations kill progress — always check how many failures remain when a subagent exits.
Subagent delegate_task vs execute_code for test fixing
| Approach | Pros | Cons |
|---|---|---|
| execute_code + write_file patch script | Full control, no summary loss, single call | Script file writing needed, f-string escaping traps |
| delegate_task | Subagent can reason about test/source alignment | 99% summary loss, may overreach, max iterations |
| Direct patch() calls | Precise, safe | Tedious for many fixes, escaping issues with multiline |
Recommendation for >20 fixes: delegate_task to do initial work, then git diff --stat to verify, then direct patches for remaining.
The replace_all Anti-Pattern (SEVERE)
replace_all=True with generic substrings like 'assert "25%"' corrupts UNRELATED test methods. Real incident: replacing "25%" in a 645-line test file broke 6 additional tests. If you MUST use replace_all, verify every changed line in git diff before committing.
Indentation Corruption Gotcha
Partial patch replacements inside with, if, for, or try blocks can corrupt indentation.
After every patch, run:
python3 -c "import py_compile; py_compile.compile('tests/test_file.py', doraise=True)"
Common corruption: a replace inside a with block changes only the first line of a multi-line call, leaving subsequent lines at the wrong indent.
GitHub Issues
gh issue create --label "phase:2" silently fails (prints warning, still creates issue) if label doesn't exist. Always use existing labels: gh label list | grep -E "priority|cat:".
Windows Line Ending Trap (\r\n)
Files originating from Windows (e.g., agents/, *_integration.py with \r\n) cause the patch tool to produce massive spurious diffs — 700+ line diffs from a 10-line change. The patch tool doesn't handle CR/LF gracefully.
Detection: cat -A filename.py — if lines end with ^M$, it has \r\n.
Fix: Always use Python/sed for these files instead of patch:
# Read, modify, write with explicit newline handling
with open(path, 'r') as f:
content = f.read()
content = content.replace(old_block, new_block, 1)
with open(path, 'w') as f:
f.write(content)
Or use sed -i for single-line fixes.
init vs execute() Timing Trap
When tests pass config via dict args to execute() (e.g., agents_base_dir), but the class initializes components (generators, loaders) in __init__ with default paths, the override will be ignored.
Pattern to fix:
def execute(self, args):
if isinstance(args, dict):
base_dir = args.get("agents_base_dir")
if base_dir:
self.base_dir = Path(base_dir)
self.agents_dir = self.base_dir / "agents"
# CRITICAL: reinitialize ALL components that depend on the path
self.structure_generator = AgentStructureGenerator(self.agents_dir)
Better fix: Pass base_dir to the constructor in tests instead of the dict workaround.
SQLAlchemy PostgreSQL Models vs SQLite in CI
Tests using PgEnum (PostgreSQL-specific enum types) or schema-qualified columns ALWAYS fail under SQLite in-memory DB. Symptoms:
TypeError: 'name' is an invalid keyword argument for Model— fixture uses old field name; model has evolved (e.g.name->company_name)RuntimeError: dictionary changed size during iteration— source bug in code under test (fix source, not test)AssertionError: assert None is not None— computed field (IRR, etc.) returns None for test data; relax assertion or fix datasetAttributeError: 'dict' object has no attribute 'data'— API returns dict, test expects object; update assertion
Fix strategy for PostgreSQL-only tests:
- Skip entire test classes (not individual tests) with clear reason:
@pytest.mark.skip(
reason="PostgreSQL-specific ENUM types (PgEnum) are incompatible with SQLite in-memory DB. "
"Run with --database marker against a real PostgreSQL instance."
)
class TestSomeModel:
...
- Always skip the WHOLE class — individual test skips miss fixture errors that trigger before the test body
Check actual model field names FIRST before patching fixtures:
grep -n 'Mapped\[' src/worldenergydata/module/database/models.py
Then update fixtures to match canonical field names (e.g. company_name=, country_of_registration=).
Legacy Namespace Compat (worldenergydata._compat pattern)
When tests import from worldenergydata.modules.X.Y.Z but the canonical layout is worldenergydata.X.Y.Z:
_compat.pyhandles top-level redirect (worldenergydata.modules.bsee->worldenergydata.bsee) but NOT deep nested subpackages- Fix deep nesting via symlink:
ln -s ../../modules/bsee/analysis/type_curves src/worldenergydata/bsee/analysis/type_curves - For test files with wrong prefixes (e.g.
src.worldenergydata.modules.fdas,sodir_module.*), fix the test imports directly to canonical paths
Wrong prefix patterns seen in practice:
# BAD — src. prefix invalid when package is installed
from src.worldenergydata.modules.fdas.analysis.cashflow import CashflowEngine
# BAD — stale module name no longer exists
from sodir_module.analysis import SodirAnalyzer
# GOOD — canonical path
from worldenergydata.fdas.analysis.cashflow import CashflowEngine
from worldenergydata.sodir.analysis import SodirAnalyzer
pytest.ini norecursedirs for Broken Test Dirs
When _archive/, legacy/, date-based dirs (e.g., 2025-08-*), or project dirs contain broken tests:
- Add them to
norecursedirsin pytest.ini — but use glob patterns likelegacy*(not justlegacy_) to catch plainlegacy/dirs --noconftestdisablescollect_ignore_globin conftest.py, so use--ignore=in addopts for single-file exclusions- Scan ALL errors at once with
pytest --collect-only 2>&1 | grep "ERROR collecting"before iteratively adding — the iterative approach wastes tokens
Verification
After each batch of fixes:
uv run python -m pytest tests/ -q --tb=line 2>&1 | tail -5
# Should show: X failed, Y passed, Z skipped in Ns
# Track: failures should be monotonically decreasing