python-guard-clean-tree-vacuous-skip
Root cause (BUG-004): guard_manager._check_tests unconditionally early-returned PASS with "No files staged — skipped" on clean trees, so chained suites (totalstack ACM parity) never executed on idle audits. Vacuous greens hid BUG-004 for ~2 days.
Fix: a test_on_clean config flag (default False) on GuardConfig. When True, a clean-tree guard runs the full test_command — diff-mode _discover returns None and control falls through to the full-suite path instead of the vacuous skip. Default False preserves legacy skip semantics for callers that opt out.
Files: gitreins_guard/guard_manager.py, tests/test_guard_manager.py (10 unit tests, all passing).
@dataclass
class GuardConfig:
test_command: str = "pytest -q"
test_on_clean: bool = False # <-- the fix: run full suite on clean trees
diff_mode: bool = True
timeout: float = 600.0
class GuardManager:
def _discover(self) -> Optional[List[str]]:
"""Non-empty list of changed files, or None when nothing discovered.
Full mode (diff_mode=False) always returns None."""
if not self.config.diff_mode:
return None
files = self.git.staged_files()
return list(files) if files else None
def _check_tests(self, suite: Optional[Suite] = None) -> GuardResult:
"""Tests step. Clean-tree semantics:
* diff-mode + _discover() -> None + test_on_clean=False
-> vacuous PASS, "No files staged — skipped" (legacy, the bug)
* diff-mode + _discover() -> None + test_on_clean=True
-> FULL FALLTHROUGH: suite command runs
* diff-mode + changed files / full mode -> suite always runs"""
if self.config.diff_mode and self._discover() is None:
if not self.config.test_on_clean:
self.log(f"clean tree: skipping tests — {NO_FILES_STAGED}")
return GuardResult.passed(NO_FILES_STAGED)
self.log("clean tree: test_on_clean=True — falling through to full suite")
command = suite.command if suite is not None else self.config.test_command
try:
proc = self.runner.run(command, self.config.timeout)
except subprocess.TimeoutExpired:
return GuardResult.failed(f"tests timed out after {self.config.timeout}s: {command}")
if proc.returncode == 0:
return GuardResult.passed(f"tests passed: {command}")
return GuardResult.failed(f"tests failed (exit {proc.returncode}): {command}")
The Guard chain runner (5-suite totalstack, 4-suite parity) calls _check_tests per suite with fail-fast, so a test_on_clean=True idle audit executes every chained suite.
**Unit tests — 10/10 passed** (`python3 -m pytest -v`, 0.02s): - `test_clean_tree_default_skips_vacuously` — default flag: PASS with skip message, **zero** commands run (documents the bug-hiding path). - `test_clean_tree_test_on_clean_runs_full_command` — flag on: clean tree runs the **full** `test_command`, not the skip. - `test_changed_files_runs_tests_with_default_flag` — staged files: tests run even with default flag. - `test_full_mode_always_runs_on_clean_tree` — `diff_mode=False`: always runs, never skipped. - `test_failing_tests_propagate_fail` / `test_timeout_is_fail_not_skip` — failures/timeouts surface as FAIL, never masked as skip. - `test_totalstack_clean_tree_runs_all_5_suites_with_flag` / `test_parity_clean_tree_runs_all_4_suites_with_flag` — 5/5 and 4/4 suites executed and pass on clean tree with the flag. - `test_legacy_chain_on_clean_tree_executes_zero_suites` — default flag: chain "passes" but 0 suites ran (the exact false-green). - `test_chain_fail_fast_aborts` — suite 3 failure aborts chain; suites 4–5 never run. **Live-style demo** (real subprocesses, 5× 0.2s sleep suites, clean tree): ``` clean tree, test_on_clean=False -> PASS in 0.0 ms (instant skip) [5/5 passed (0 executed)] clean tree, test_on_clean=True -> PASS in 1.08 s (full suite) [5/5 passed (5 executed)] ``` Matches the reported live check (totalstack guard 1m52s full tests vs instant skip before) at small scale: same PASS on clean tree, but the chained suites now genuinely execute on idle audits. Edge cases covered: clean vs staged trees, full mode, failing suite, timeout, fail-fast abort.
{"model": "deepseek-v4-flash", "problem_class": "python-guard-clean-tree-vacuous-skip", "result": "passed", "tests": 10}