fix(incremental): add stop_at to find_repo_root / find_project_root (#241)#242
Open
azizur100389 wants to merge 1 commit intotirth8205:mainfrom
Open
fix(incremental): add stop_at to find_repo_root / find_project_root (#241)#242azizur100389 wants to merge 1 commit intotirth8205:mainfrom
azizur100389 wants to merge 1 commit intotirth8205:mainfrom
Conversation
…irth8205#241) Two tests in tests/test_incremental.py have been failing on any developer machine whose tmp_path has a git-initialized ancestor: tests/test_incremental.py::TestFindRepoRoot::test_returns_none_without_git tests/test_incremental.py::TestFindProjectRoot::test_falls_back_to_start Root cause ---------- pytest's tmp_path lives under $TMPDIR, which on Windows 11 resolves to ``C:/Users/<user>/AppData/Local/Temp/pytest-of-<user>/...``. If the user has ever run ``git init`` anywhere under their home directory (dotfiles repo, chezmoi, yadm, a bare ``git init ~`` — all very common on developer machines), then ``find_repo_root()`` walking up from ``tmp_path/no_git`` finds the ancestor .git and returns ``C:/Users/<user>`` instead of ``None``. The same failure mode affects Linux and macOS users with a git-initialized home directory. ``find_repo_root`` has no way to bound its ancestor walk — it always climbs to the filesystem root. This is correct production behavior (a nested script inside a git repo *should* find its repo) but makes the function un-testable in environments where the tmp path sits inside a git-initialized ancestor. Fix --- Add an optional ``stop_at: Path | None = None`` parameter to both ``find_repo_root()`` and ``find_project_root()``. When provided, the walk examines ``stop_at`` for a .git directory and then stops without crossing above it. ``stop_at=None`` preserves the existing "walk to filesystem root" semantics, so this is a zero-risk backward-compatible API addition. All 7 production callers in cli.py (x4), tools/refactor_tools.py (x1), tools/_common.py (x1), and the internal call in find_project_root pass no argument for stop_at — their behavior is unchanged. The two tests above are updated to pass ``stop_at=tmp_path`` so the walk is bounded to the test sandbox. ``test_falls_back_to_start`` is also hardened with ``monkeypatch.delenv("CRG_REPO_ROOT", raising=False)`` so a developer env var can no longer shadow the test's expectation. stop_at is also a legitimate production API — callers that want to bound the ancestor walk (multi-repo orchestrators, CI containers with bind-mounted volumes, embedded sandboxes, Docker build contexts) can now do so without resorting to monkeypatching. Tests added / updated --------------------- 1. TestFindRepoRoot::test_returns_none_without_git - Now passes ``stop_at=tmp_path`` (the actual fix for the flakiness). 2. TestFindRepoRoot::test_stop_at_prevents_escape_to_outer_git [NEW] - Positive regression test: when an outer directory has .git, an unbounded walk finds it but a bounded walk with stop_at=inner returns None. Locks in the new parameter's semantics. 3. TestFindRepoRoot::test_stop_at_finds_git_at_boundary [NEW] - Regression test: stop_at must NOT suppress a .git that lives at the boundary itself. The walk examines stop_at for .git before stopping, so a repo rooted exactly at the boundary is still found. 4. TestFindProjectRoot::test_falls_back_to_start - Now passes stop_at=tmp_path AND monkeypatches CRG_REPO_ROOT. 5. TestFindProjectRoot::test_stop_at_forwarded_to_find_repo_root [NEW] - Regression test: find_project_root must forward stop_at to find_repo_root, not silently drop it. Also verifies that when the bounded walk finds nothing, the fall-through to "return start" still works correctly. Test results ------------ Stage 1 (TestFindRepoRoot + TestFindProjectRoot): 8/8 passed (up from 6/8 on unchanged main — the 2 previously-flaky tests and the 3 new regression tests all green). Stage 2 (tests/test_incremental.py full): 48 passed, 1 pre-existing failure (TestDataDir::test_default_uses_repo_subdir — UTF-8 gitignore bug fixed on separate branch in the PR that closes tirth8205#239). Stage 3 (tests/test_tools.py adjacent — find_project_root used there): 62 passed, 15 pre-existing Windows file-lock teardown errors. Stage 4 (full suite): 737 passed, 6 pre-existing Windows failures (all covered by the parallel tirth8205#239 fix PR — none are regressions from this change), 165 pre-existing Windows teardown errors. Net effect: this PR independently removes 2 failures from the baseline (8 -> 6); once both PRs merge, all 8 pre-existing Windows failures will be resolved. Stage 5 (ruff check on incremental.py + test_incremental.py): clean. Zero regressions. Fully backward-compatible.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two tests in
tests/test_incremental.pyhave been failing on any developer machine whosetmp_pathhas a git-initialized ancestor:TestFindRepoRoot::test_returns_none_without_gitTestFindProjectRoot::test_falls_back_to_startThis PR adds an optional
stop_at: Path | None = Noneparameter to bothfind_repo_root()andfind_project_root(), uses it in the two flaky tests to bound the walk totmp_path, and adds 3 new positive regression tests to lock in the new parameter's semantics.Closes #241.
Root cause
pytest's
tmp_pathlives under$TMPDIR, which on Windows 11 resolves toC:/Users/<user>/AppData/Local/Temp/pytest-of-<user>/.... If the user has ever rungit initanywhere under their home directory (dotfiles repo, chezmoi, yadm, a baregit init ~— all very common on developer machines), thenfind_repo_root()walking up fromtmp_path/no_gitfinds the ancestor.gitand returnsC:/Users/<user>instead ofNone. The same failure mode affects Linux and macOS users with a git-initialized home directory.find_repo_rootpreviously had no way to bound its ancestor walk — it always climbed to the filesystem root. This is correct production behavior (a nested script inside a git repo should find its repo) but makes the function un-testable in environments where the tmp path sits inside a git-initialized ancestor.Fix
Add an optional
stop_at: Path | None = Noneparameter to bothfind_repo_root()andfind_project_root():Semantics:
stop_at=None(default): existing behavior, walk all the way to the filesystem rootstop_at=<path>: the walk examinesstop_atfor.gitand then stops — it never crosses above the boundary.gitat exactly the boundary is still found (it's examined before the stop check)find_project_rootforwardsstop_attofind_repo_root, so both public APIs gain the same testability handle.Fully backward-compatible. All 7 production callers pass no argument:
cli.py(4 call sites)tools/refactor_tools.py(1 call site)tools/_common.py(1 call site)find_project_rootitselfstop_atis also a legitimate production API — callers that want to bound the ancestor walk (multi-repo orchestrators, CI containers with bind-mounted volumes, embedded sandboxes, Docker build contexts) can now do so without resorting to monkeypatching.Tests updated / added
TestFindRepoRoot::test_returns_none_without_git— now passesstop_at=tmp_path(the actual fix for the flakiness). Docstring explains why the bound is needed.TestFindRepoRoot::test_stop_at_prevents_escape_to_outer_git[NEW] — positive regression test: when an outer directory has.git, an unbounded walk finds it but a bounded walk withstop_at=innerreturnsNone. Locks in the new parameter's semantics.TestFindRepoRoot::test_stop_at_finds_git_at_boundary[NEW] — regression test:stop_atmust not suppress a.gitthat lives at the boundary itself. The walk examinesstop_atfor.gitbefore stopping, so a repo rooted exactly at the boundary is still found.TestFindProjectRoot::test_falls_back_to_start— now passesstop_at=tmp_pathand monkeypatchesCRG_REPO_ROOTviamonkeypatch.delenv(...)so a developer env var can no longer shadow the test expectation.TestFindProjectRoot::test_stop_at_forwarded_to_find_repo_root[NEW] — regression test:find_project_rootmust forwardstop_attofind_repo_root, not silently drop it. Also verifies the fall-through path (returnstartwhen the bounded walk finds nothing) still works.Test results
TestFindRepoRoot+TestFindProjectRootmain)tests/test_incremental.pyfulltest_default_uses_repo_subdir— UTF-8 gitignore bug, fixed on the parallel branch in PR #240)tests/test_tools.py(find_project_rootis used there)ruff checkonincremental.py+test_incremental.pyNet baseline effect: this PR independently removes 2 failures (8 → 6). Combined with PR #240, the full Windows baseline goes from 8 failures to 0.
Relationship to #240
PR #240 and this PR are independent and touch disjoint code paths:
get_data_dir()UTF-8,parse_bytesCRLF handling, and the stale FastMCP API intest_main.py.stop_atparameter tofind_repo_root/find_project_rootand fixes the 2 test environment flakiness.They can be merged in either order — no conflicts, no shared files.
Why this fix is safe
stop_atdefault isNone, preserving existing behavior exactly.stop_at.test_stop_at_finds_git_at_boundary) locks in the "inclusive of boundary" semantics so a future refactor can't accidentally change the behavior.