Skip to content

fix(test): scrub the git environment in every test-fixture git write (#15246) - #15286

Merged
mrveiss merged 15 commits into
Dev_new_guifrom
issue-15246-git-env
Aug 29, 2026
Merged

mrveiss merged 15 commits into
Dev_new_guifrom
issue-15246-git-env

Conversation

@mrveiss

@mrveiss mrveiss commented Aug 29, 2026 •

Copy link
Copy Markdown
Owner

Closes #15246

Thinking Path

Started from the issue's premise that one found-while-changing-something instance is not evidence there is only one, so treated the twelve named files as a floor, not the answer: grepped every _test.py/test_*.py for a subprocess-family call plus a git literal, then hand-classified each hit as a genuine unscrubbed write, an already-scrubbed write, a deliberate real-repo read (exempt), or a false positive (a git mention in prose/a mocked call/a non-git command). That produced a ~60-file candidate list and, after classification, ~27 files with a genuine unscrubbed write.

Verified criteria 1/2 (fresh worktree, git add -A stages nothing) still hold post-#15253/#15262 before touching anything else: git status --porcelain=v1 on a fresh git worktree add returns 0 entries.

Built the durable guard (tools/lint/check_git_write_env_scrubbed.py) against the same census, matching the existing check_git_toplevel_env_scrubbed.py (#15176) precedent's AST-based shape rather than inventing a second mechanism. Then used the guard's own full-repository self-check as a second, independent sweep on every push — this repo's own pre-push hook runs the touched tests, and each run surfaced something the manual grep-based census had missed: a production function with the identical bug (only reachable because the test fixture was no longer confusable with the real repo, so the test's assertion started depending on the production code being right too), an operational CLI tool that matched the guard's test_*.py naming heuristic without being a test, and two classifier bugs in the guard itself (a dynamic path argument after an already-resolved read verb misclassified as an unknowable write; a env = <trusted-function>() local-variable indirection the trust resolver didn't chase). Each is its own commit, in the order it was found.

What Changed

Sweep and fix (test files, all via autobot_shared.paths.scrubbed_git_env()):

  • All 17 autobot-infrastructure/shared/scripts/hooks/pre-commit-*_test.py files + lib/_common_test.py — each had a _git/_make_git_repo fixture helper (init/config/add/commit) and several bash <hook> invocations with no env= at all.
  • pipeline-scripts/check-pre-commit-hook-pr_test.py, pipeline-scripts/check_baseline_no_growth_test.py — same shape; the former needed a byte-tight rewrite to stay within its recorded KNOWN_LARGE line ceiling (751) while adding the fix.
  • repo_tests/bandit_exclude_anchoring_test.py, repo_tests/flake8_exclude_anchoring_test.py — a git init -q in a tmp_path fixture (their git ls-files reads against the real repo root are correctly untouched — deliberate).
  • repo_tests/git_hooks_installer_test.py, repo_tests/release_range_test.py, scripts/lint_conventions_test.py, scripts/verify_done_test.py, scripts/check_script_exec_bits_test.py, repo_tests/unreferenced_shell_script_test.py — same _git helper shape.
  • autobot-backend/services/knowledge/code_graph_provenance_test.py, autobot-backend/tasks/test_task_workspace.py — same shape, no shared helper (inline subprocess.run per call site).
  • scripts/check_script_exec_bits_test.py's _GIT_ENV looked scrubbed ({**os.environ, "GIT_CONFIG_GLOBAL": ...}) but only neutralised two config vars, not GIT_DIR — the same bug wearing a name that reads like the fix.

Fixed but out of the test-file sweep (production code reachable in-process from a test's own git fixture, so no test-side env= could protect it — found via the guard's own live pre-push runs and, for the last one, review):

  • autobot-backend/services/knowledge/code_indexer.py's _git() — reachable in-process from code_graph_provenance_test.py.
  • scripts/check_script_exec_bits.py's _tracked_files()/_is_executable() — reached in-process by check_script_exec_bits_test.py.
  • scripts/install-git-hooks.sh (shell) — git rev-parse --show-toplevel with no protection; repo_tests/git_hooks_installer_test.py invokes it as a real subprocess, so unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE at the top of the script is the shell-side equivalent of scrubbed_git_env() (the check_git_toplevel_env_scrubbed.py docstring already names this as the intended shell fix, just not previously implemented anywhere).
  • autobot-backend/services/task_workspace.py's release() (git worktree remove --force, git branch -D/-d) and _git_add_worktree() (two git worktree add forms) — reachable in-process from autobot-backend/tasks/test_task_workspace.py, which calls allocate/release/cleanup_stale directly. Found in review, not by the guard (production files are out of its scope — see below). Verified every caller (heartbeat_scheduler.py, tasks/workspace_cleanup.py, llc/services/work_item_service.py and handoff.py's release_for_task) passes an explicit repo_root or falls back to the module's static _REPO_ROOT; none depends on an ambient git environment, so scrubbing changes nothing about which repository each caller targets.

In-process production sweep, repeated with this shape specifically in mind ("production code reachable in-process from a test that constructs a temporary repository"): re-swept all git-invoking production code (subprocess.run/Popen/check_output/check_call and asyncio.create_subprocess_exec — the latter family wasn't in the original census's grep pattern) and, for every unscrubbed site, checked whether any test imports the containing module and reaches it in-process. Found zero further sites: task_workspace.py was the only one. Every other unscrubbed production git call (skills/promoter.py::_lint_and_commit — a git add/git commit with no cwd= at all, the worst-shaped one found; api/git_mcp.py; utils/branch_metrics.py; autobot-slm-backend/services/git_tracker.py; api/analytics_code_review.py/analytics_precommit.py; code_intelligence/precommit_analyzer.py/code_review_engine.py; scripts/utilities/optimize_agents.py; deployment_rollback.py; migrate_tests.py) is either never called from any test at all or is fully mocked/patched at every call site — confirmed per-file, not inferred. Filed as #15287 rather than fixed here: each needs the same caller-by-caller verification task_workspace.py just got, none is causing a test (or this PR's own pushes) to fail today, and rushing that verification under a push-is-blocked clock is the wrong way to do it.

Exempted, with the reason recorded at each site (no code change):

Guard

tools/lint/check_git_write_env_scrubbed.py (+ _test.py), wired into .pre-commit-config.yaml as git-write-env-scrubbed, scoped to _test.py/test_*.py files. AST-based, mirroring check_git_toplevel_env_scrubbed.py's shape: resolves subprocess bindings, classifies a git argv's verb (skipping -C/-c flag pairs) against WRITE_VERBS, and accepts env= when it resolves — directly, through a local trusted wrapper/variable, or as an unpack-free dict literal — to scrubbed_git_env/hermetic_git_env.

Reach floor: TEST_FILE_FLOOR = 1500 against 2064 tracked _test.py/test_*.py files measured at write time (scan_repo() walks and counts; main() fails loudly, not clean, if the walk lands below the floor).

Scope recommendation (production vs. test files): left the guard scoped to test files, and filed #15287 rather than extending it here. The guard's AST machinery is reusable for production, but the production sweep above shows why extending it now would be premature: production callers are far more varied (an MCP tool surface, Celery workers, ansible tooling, CLI scripts with no tests at all) than a pytest fixture's, so a production-scoped version needs its own reach floor and a caller-by-caller review per write site before each is fixed — exactly the verification this PR just did for task_workspace.py, not a mechanical env= insertion. That review is #15287's job; the guard (or a second one scoped to production) can follow once enough of that list is closed to know what a production allowlist actually needs to say.

Contrast mutation: test_the_unscrubbed_helper_is_reported / test_the_scrubbed_helper_is_accepted run the identical _git helper shape this PR fixed twice, differing by an added import plus the env= keyword the import makes resolvable — the unscrubbed fixture is reported (1 finding, #15246 in the message), the scrubbed one is clean. test_a_walk_below_the_floor_fails_main_loudly covers the reach-floor failure mode directly (monkeypatched scan_repo returning (0, []) must still fail main, not pass it).

Verification

  • Every touched Python file: python3 -m py_compile (parses/compiles, does not execute) and python3 -m pyflakes — no new unused imports or syntax errors; three pre-existing unused-import findings (_common_test.py, pre-commit-no-tag-pinned-action_test.py, check_baseline_no_growth_test.py) confirmed present on origin/Dev_new_gui before this PR, left alone as out of scope.
  • Every touched file: black --line-length 120 --check against both the new and the pre-PR version, confirmed identical pre-existing-debt counts (0 new debt introduced anywhere; one file's debt count improved incidentally).
  • pipeline-scripts/check-pre-commit-hook-pr_test.py and autobot-backend/services/knowledge/code_indexer.py stayed at/under their recorded KNOWN_LARGE/ratchet line ceilings (747/751 and 1179/1179) — measured with wc -l, no ceiling raised.
  • Criteria 1/2 (already landed by bug(hooks): post-checkout chmods +x onto 13 pytest files and a sourced library, dirtying every working tree on every checkout #15253/fix(hooks): gate post-checkout's chmod on a shebang so worktrees stop being born dirty (#15253) #15262): git status --porcelain=v1 on a fresh git worktree add from origin/Dev_new_gui returns 0 entries — re-verified at this PR's base commit, not re-implemented.
  • This PR's own git push ran the repo's real pre-push hook five times against the touched tests over the course of this PR (including the coordinator-review round that found task_workspace.py); four runs failed on exactly the defect class this issue is about (each fixed in its own commit, described above), the fifth passed. No --no-verify used at any point, and a remote-side Merge branch 'Dev_new_gui' commit that appeared on the PR branch mid-review was reconciled with a normal git merge (no rebase, no force-push).
  • Did not run pytest directly and did not execute any application code myself; all verification is either static (compile/parse/lint/line-count) or came from the repository's own pre-push gate running as a side effect of a normal git push, exactly as the issue's hard constraints require.

Model Used

Claude Opus 5 (1M context)

test and others added 7 commits August 29, 2026 14:48
…15246)

A test that runs `git init`/`add`/`commit`/etc. in a tmp_path fixture with
subprocess.run's default of inheriting os.environ writes to whatever GIT_DIR
names under the pre-push hook -- the pushing worktree's own git directory,
not the fixture -- exactly as autobot_shared/paths_test.py did when it
dropped 86 tracked files from the live index. Fixes every other unscrubbed
git write this sweep found and adds a durable guard against a fourth
recurrence.
…it helper (#15246)

_git() in services/knowledge/code_indexer.py passed -C root_dir with no env
override, so an inherited GIT_DIR (this repo's own pre-push hook exports one)
silently answers for the wrong repository instead of root_dir. Found live:
this repo's own pre-push hook run of code_graph_provenance_test.py -- itself
fixed in this PR to no longer corrupt the real repo -- failed for exactly
this reason once the fixture repo was no longer confusable with the real one.
git rev-parse --show-toplevel/--git-common-dir had no protection from an
inherited GIT_DIR. Every checkout in this repo is a worktree, and the
pre-push hook exports GIT_DIR pointing at the pushing worktree's own git
directory -- the installer then silently resolves the wrong repository root
and destination for every downstream git call. Reproduced live: this repo's
own pre-push hook run of repo_tests/git_hooks_installer_test.py installed
hooks into the real repository instead of the test's throwaway fixture, once
the fixture itself was no longer confusable with the real one (#15246's own
fix). Unset once at the top, the shell-side equivalent of
autobot_shared.paths.scrubbed_git_env().
…it reads (#15246)

_tracked_files() and _is_executable() ran git ls-files with no env override,
so an inherited GIT_DIR silently answers for the wrong repository instead of
root. Reproduced live: this repo's own pre-push hook run of
check_script_exec_bits_test.py, which calls these functions directly
in-process (not through a subprocess whose env the test controls), failed
once its own fixture was no longer confusable with the real repo.
…t-write guard (#15246)

scripts/test_first_remediation.py matches the guard's own test-file naming
heuristic (test_*.py, pytest's own collection glob) but is a CLI tool that
deliberately creates real worktrees and pushes real branches -- the opposite
of a throwaway fixture. The guard found it live on this PR's own push.
…ads (#15246)

_is_git_write treated ANY dynamic argv element as making the verb unknowable,
not just a dynamic element at the verb's own position -- so `git -C
str(root) ls-files "*.py"` (a read, common and correct elsewhere in this
suite) misclassified as an unresolvable write. Also: trusted_names now
resolves `env = _test_git_env()` (a local variable holding an
already-trusted wrapper's result, pre-commit-hardcoded-values_test.py's
actual shape) and env={"PATH": ..., ...} with no `**` unpacking is accepted
outright, since it cannot carry an ambient GIT_DIR it was never given. Fixes
repo_tests/unreferenced_shell_script_test.py's one remaining genuine finding.
All caught by the guard's own full-repository self-check on this PR's push.
@github-actions

Copy link
Copy Markdown
Contributor

✅ SSOT Configuration Compliance: Passing

🎉 No new hardcoded values of either class — ssot and other both block.

Known backlog in pipeline-scripts/hardcoded_values_baseline.txt is suppressed and tracked in #14371.

test and others added 8 commits August 29, 2026 15:27
…branch commands (#15246)

release()'s git worktree remove --force and git branch -D/-d, and
_git_add_worktree()'s two git worktree add calls, ran with cwd=str(root) but
no env= at all -- an inherited GIT_DIR overrides cwd, so under the pre-push
hook this can force-remove a worktree and force-delete a branch in the real
repository instead of the throwaway one autobot-backend/tasks/
test_task_workspace.py builds in tmp_path. Reachable in-process (allocate/
release/cleanup_stale are called directly, not via subprocess), so no
test-side env= could have protected it -- the same shape as code_indexer.py
and check_script_exec_bits.py, found via review rather than the guard, since
the guard is scoped to test files. Verified every caller (heartbeat_scheduler,
workspace_cleanup, work_item_service/handoff's release_for_task) passes an
explicit repo_root or relies on _REPO_ROOT, never on ambient git state.

A systematic re-sweep for the same shape (production code reachable
in-process from a test with a temporary-repository fixture) found no further
sites; broader unscrubbed production git calls with no test coverage at all
are filed as #15287, out of scope for this issue.
Tracked 100644 despite .pre-commit-config.yaml invoking it as a bare
`entry: tools/lint/check_git_write_env_scrubbed.py` (language: python),
so pre-commit could not execute it -- the guard against tests silently
corrupting the repository was itself silently not running. core.fileMode
is not reliable here, so `git update-index --chmod=+x` (not a plain
`chmod +x`) is what actually reaches the index; the working-tree bit is
set to match so no unstaged mode diff remains.
…it-write guard (#15246)

The reach-floor failure message's print() lacked the '# noqa: print'
annotation its two siblings in this file already carry (and that
check_git_toplevel_env_scrubbed.py, the guard this one is modeled on,
uses throughout) -- the actual CI-blocking finding
("1 violation(s) on lines this PR added") was this line, not
scripts/check_script_exec_bits.py's pre-existing prints, which the same
hook run explicitly reported as "3 pre-existing violation(s) on lines
this PR did not touch" and "Not failing the build for these." A CLI lint
reporter's own findings-to-stderr output is the sanctioned exception here,
not a logger call -- matching the established sibling pattern exactly.
…-pr_test.py to 747 (#15246)

This PR's env-scrub edit shrank the file by four lines (751 -> 747), and
the ratchet audits down as well as up: a ceiling left above the real size
would silently re-permit growth back to the old number. Verified
`git show HEAD:pipeline-scripts/check-pre-commit-hook-pr_test.py | wc -l`
== 747 before editing. Lowered both KNOWN_LARGE (scripts/
python_file_size_known_large.py) and its paired RATCHET_BASELINE
(repo_tests/python_file_size_ratchet_baseline.py) -- the audit checks
them together. Cross-referenced every other file this PR touches against
both tables: code_indexer.py is the only other entry among them (1179,
unchanged, matches exactly); the remaining ~32 touched files carry no
KNOWN_LARGE entry at all and stayed comfortably under 600 lines.
@mrveiss
mrveiss merged commit 6147fd0 into Dev_new_gui Aug 29, 2026
68 checks passed
@mrveiss
mrveiss deleted the issue-15246-git-env branch August 29, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant