headroom/tests/test_learn
Parideboy a09ba6c087
fix(learn): treat unreadable candidate paths as absent in project decode (#2446)
## Description

`headroom learn` crashes with an uncaught `PermissionError` when the
current user's username contains a dash. `_decode_project_path` (in
`headroom/learn/plugins/claude.py`) probes speculative candidate paths
when reconstructing an original filesystem path from a Claude Code
encoded project directory name. When the username is e.g. `marco-rocha`,
one candidate becomes `/home/marco/rocha`, which can collide with
another user's home directory whose parent isn't stat-able.
`Path.exists()` calls `os.stat` internally, raising `PermissionError`
instead of returning `False`, so the whole `learn` command crashes
before returning any recommendations.

Fixes #2443

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Add `_path_exists()` to `headroom/learn/plugins/claude.py` — a thin
wrapper around `Path.exists()` that returns `False` on any `OSError`
(including `PermissionError`), mirroring the existing `OSError` handling
already used in `_greedy_path_decode`.
- Route every speculative candidate-path existence check in the decode
path through `_path_exists()`: the Windows drive/path probes in
`_decode_windows_path`, the `simple` POSIX candidate and greedy-branch
bases in `_decode_project_path`/`_greedy_path_decode`, and the decoded
`project_path`/`CLAUDE.md` checks in `discover_projects`.
- Add regression tests covering the exact issue shape (`PermissionError`
on `/home/marco/rocha`) and the `_path_exists` helper directly.
- Leave `CHANGELOG.md` untouched — release-please generates it from
conventional commits.

## Testing

- [x] Unit tests pass (`python -m pytest
tests/test_learn/test_scanner.py::TestDecodePermissionError -q`)
- [x] Linting passes (`ruff check`, `ruff format --check` on the two
changed files)
- [x] New tests added for new functionality

### Test Output

```text
$ python -m pytest tests/test_learn/test_scanner.py::TestDecodePermissionError -q
collected 2 items
tests\test_learn\test_scanner.py ..                                      [100%]
2 passed in 1.86s

$ ruff check headroom/learn/plugins/claude.py tests/test_learn/test_scanner.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13, local dev checkout of headroom
on branch off upstream/main
- Exact command / steps: Simulated the issue by monkeypatching
`Path.exists` to raise `PermissionError` for the colliding candidate
`/home/marco/rocha`, then calling
`_decode_project_path("-home-marco-rocha-butterfly-sylphina")`
- Observed result: Before the fix the call propagates `PermissionError`
(crash, matching the reported traceback); after the fix it returns
without raising and the unreadable candidate is treated as non-existent.
Both regression tests pass.
- Not tested: End-to-end `headroom learn --apply` on a real Linux
multi-user box with an actually unreadable `/home/<prefix>` — reproduced
via the documented minimal logic instead.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 06:16:44 -07:00
..
__init__.py Add headroom learn: offline failure learning for coding agents 2026-02-27 21:19:03 -08:00
test_analyzer.py fix(learn): parse fenced JSON even with a prose preamble (#1988) 2026-07-10 11:15:36 -07:00
test_claude_config_dir.py fix(learn): honor CLAUDE_CONFIG_DIR when locating Claude logs and memory (#1642) 2026-07-01 23:22:16 -05:00
test_error_classification.py fix(learn): don't shadow TIMEOUT/CONNECTION with the generic RUNTIME_ERROR (#2099) 2026-07-13 10:50:54 -04:00
test_gemini_scanner.py Plugin architecture for headroom learn + live traffic flush 2026-04-09 20:30:20 -07:00
test_integration.py feat(learn): write per-project learnings to CLAUDE.local.md by default (#1115) 2026-06-22 15:05:06 -05:00
test_loop_weighting.py feat(learn): weight loops in Headroom Learn + RTK-loop eval (#1160) 2026-06-22 18:49:08 -05:00
test_opencode_scanner.py feat: add first-class OpenCode support (wrap, learn, mcp install) (#559) 2026-06-25 13:38:58 -05:00
test_plugin_encoding.py Fix headroom learn crashing/no-op on Windows from missing UTF-8 encoding (#1239) 2026-06-21 10:37:37 -07:00
test_registry.py feat: add first-class OpenCode support (wrap, learn, mcp install) (#559) 2026-06-25 13:38:58 -05:00
test_rtk_loop_eval.py feat(learn): weight loops in Headroom Learn + RTK-loop eval (#1160) 2026-06-22 18:49:08 -05:00
test_scanner.py fix(learn): treat unreadable candidate paths as absent in project decode (#2446) 2026-07-22 06:16:44 -07:00
test_subagent_scanning.py fix(learn/claude): don't abort the whole scan on a null message line (#2299) 2026-07-16 14:36:11 -07:00
test_writer.py feat(learn): write per-project learnings to CLAUDE.local.md by default (#1115) 2026-06-22 15:05:06 -05:00