generated from coulomb/repo-seed
feat(consistency): fix-consistency-remote works without REPO for all repos
Adds --remote CLI flag and fix_all_remote() function. When run without a REPO argument, the target checks all registered repos and: - Skips repos whose local path does not exist on this machine - Skips repos that are already clean (no fixable issues, no FAILs, not behind remote, only C-08 background noise allowed) - For repos that need work: git pull --ff-only then fix_repo() Prints a summary of CLEAN (skipped) and NOT ON THIS HOST (skipped) repos before the detailed fix reports. Simplifies the Makefile target from shell-level curl+git to a single uv run call using --remote. Same flag handles both single-repo and all-repos. Also adds _git_pull() helper and 13 new tests (71 total in consistency suite). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,8 +25,11 @@ from consistency_check import (
|
||||
Issue,
|
||||
FILE_TO_DB_WORKSTREAM_STATUS,
|
||||
STATUS_ORDER,
|
||||
_BACKGROUND_CHECKS,
|
||||
_detect_behind_remote,
|
||||
_git_pull,
|
||||
_patch_task_status_in_file,
|
||||
_report_needs_action,
|
||||
get_tasks_from_workplan,
|
||||
normalise_workstream_status,
|
||||
parse_frontmatter,
|
||||
@@ -462,7 +465,7 @@ class TestDetectBehindRemote:
|
||||
["git", "-C", str(clone), "commit", "--allow-empty", "-m", "init"],
|
||||
capture_output=True,
|
||||
)
|
||||
subprocess.run(["git", "-C", str(clone), "push", "origin", "HEAD:main"], capture_output=True)
|
||||
subprocess.run(["git", "-C", str(clone), "push", "origin", "HEAD"], capture_output=True)
|
||||
# Add a local-only commit (not pushed)
|
||||
subprocess.run(
|
||||
["git", "-C", str(clone), "commit", "--allow-empty", "-m", "local only"],
|
||||
@@ -483,7 +486,7 @@ class TestDetectBehindRemote:
|
||||
["git", "-C", str(clone), "commit", "--allow-empty", "-m", "init"],
|
||||
capture_output=True,
|
||||
)
|
||||
subprocess.run(["git", "-C", str(clone), "push", "origin", "HEAD:main"], capture_output=True)
|
||||
subprocess.run(["git", "-C", str(clone), "push", "origin", "HEAD"], capture_output=True)
|
||||
|
||||
# Add a commit directly in the bare repo (simulates remote-only progress)
|
||||
work = tmp_path / "work"
|
||||
@@ -554,9 +557,121 @@ class TestPatchTaskStatusInFile:
|
||||
assert "id: T01\nstatus: todo" in patched
|
||||
assert "id: T02\nstatus: done" in patched
|
||||
|
||||
def test_does_not_touch_non_status_fields(self, tmp_path):
|
||||
content = (
|
||||
"---\nid: WP\n---\n"
|
||||
"```task\nid: T01\nstatus: todo\npriority: high\nstate_hub_task_id: \"abc\"\n```\n"
|
||||
)
|
||||
f = self._make_workplan(tmp_path, content)
|
||||
_patch_task_status_in_file(f, "T01", "done")
|
||||
patched = f.read_text()
|
||||
assert "priority: high" in patched
|
||||
assert 'state_hub_task_id: "abc"' in patched
|
||||
|
||||
def test_idempotent_when_already_correct(self, tmp_path):
|
||||
content = "---\nid: WP\n---\n```task\nid: T01\nstatus: done\n```\n"
|
||||
f = self._make_workplan(tmp_path, content)
|
||||
result = _patch_task_status_in_file(f, "T01", "done")
|
||||
# status matches, re.sub produces same text → no write → False
|
||||
assert result is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _git_pull (T02 remote fix helper)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestGitPull:
|
||||
def test_returns_false_for_nonexistent_path(self):
|
||||
ok, msg = _git_pull("/nonexistent/path")
|
||||
assert ok is False
|
||||
assert msg
|
||||
|
||||
def test_returns_true_for_up_to_date_repo(self, tmp_path):
|
||||
import subprocess
|
||||
bare = tmp_path / "bare.git"
|
||||
clone = tmp_path / "clone"
|
||||
bare.mkdir()
|
||||
subprocess.run(["git", "init", "--bare", str(bare)], capture_output=True)
|
||||
subprocess.run(["git", "clone", str(bare), str(clone)], capture_output=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(clone), "commit", "--allow-empty", "-m", "init"],
|
||||
capture_output=True,
|
||||
)
|
||||
subprocess.run(["git", "-C", str(clone), "push", "origin", "HEAD"], capture_output=True)
|
||||
ok, msg = _git_pull(str(clone))
|
||||
assert ok is True
|
||||
|
||||
def test_pulls_new_remote_commit(self, tmp_path):
|
||||
import subprocess
|
||||
bare = tmp_path / "bare.git"
|
||||
clone = tmp_path / "clone"
|
||||
work = tmp_path / "work"
|
||||
bare.mkdir()
|
||||
subprocess.run(["git", "init", "--bare", str(bare)], capture_output=True)
|
||||
subprocess.run(["git", "clone", str(bare), str(clone)], capture_output=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(clone), "commit", "--allow-empty", "-m", "init"],
|
||||
capture_output=True,
|
||||
)
|
||||
subprocess.run(["git", "-C", str(clone), "push", "origin", "HEAD"], capture_output=True)
|
||||
# Push a new commit from a second clone
|
||||
subprocess.run(["git", "clone", str(bare), str(work)], capture_output=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(work), "commit", "--allow-empty", "-m", "remote work"],
|
||||
capture_output=True,
|
||||
)
|
||||
subprocess.run(["git", "-C", str(work), "push"], capture_output=True)
|
||||
# Pull should succeed and bring in the new commit
|
||||
ok, msg = _git_pull(str(clone))
|
||||
assert ok is True
|
||||
head = subprocess.run(
|
||||
["git", "-C", str(clone), "log", "--oneline", "-1"],
|
||||
capture_output=True, text=True,
|
||||
).stdout
|
||||
assert "remote work" in head
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _report_needs_action (smart skip logic)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestReportNeedsAction:
|
||||
def _make_report(self, issues: list[tuple[str, str, bool]]) -> ConsistencyReport:
|
||||
"""Build a report from (severity, check_id, fixable) tuples."""
|
||||
r = ConsistencyReport(repo_slug="test", repo_path="/tmp/test")
|
||||
for severity, check_id, fixable in issues:
|
||||
r.add(severity=severity, check_id=check_id, message="test", fixable=fixable)
|
||||
return r
|
||||
|
||||
def test_clean_report_not_behind_is_skipped(self):
|
||||
r = self._make_report([])
|
||||
assert _report_needs_action(r, behind_remote=False) is False
|
||||
|
||||
def test_behind_remote_always_needs_action(self):
|
||||
r = self._make_report([])
|
||||
assert _report_needs_action(r, behind_remote=True) is True
|
||||
|
||||
def test_fail_always_needs_action(self):
|
||||
r = self._make_report([("FAIL", "C-07", False)])
|
||||
assert _report_needs_action(r, behind_remote=False) is True
|
||||
|
||||
def test_c08_only_is_background_noise(self):
|
||||
"""C-08 (legacy archived workstream) alone should not trigger a fix cycle."""
|
||||
r = self._make_report([("INFO", "C-08", False)])
|
||||
assert _report_needs_action(r, behind_remote=False) is False
|
||||
|
||||
def test_c08_plus_actionable_warn_needs_action(self):
|
||||
r = self._make_report([("INFO", "C-08", False), ("WARN", "C-10", True)])
|
||||
assert _report_needs_action(r, behind_remote=False) is True
|
||||
|
||||
def test_fixable_warn_needs_action(self):
|
||||
r = self._make_report([("WARN", "C-15", True)])
|
||||
assert _report_needs_action(r, behind_remote=False) is True
|
||||
|
||||
def test_non_fixable_warn_needs_action(self):
|
||||
"""Non-fixable WARNs (e.g. C-07 ghost) still warrant attention."""
|
||||
r = self._make_report([("WARN", "C-14", False)])
|
||||
assert _report_needs_action(r, behind_remote=False) is True
|
||||
|
||||
def test_background_checks_constant_contains_c08(self):
|
||||
assert "C-08" in _BACKGROUND_CHECKS
|
||||
|
||||
Reference in New Issue
Block a user