feat(issue-21): item 5 review identity - per-round snapshot and snapshot diff, detached review worktree (--review-isolation), stale-round re-review, identity records with evidence, pre-promotion re-check; remote reviewers refused or diff-only (--remote-review)
Active with --worktree/--base only; without them prompts, flow and promotion are unchanged (a loop_id is recorded in the checkpoint either way). - Every review round is pinned (B3): the source's HEAD and its working state as a tree built through a temporary index (the real index/status/refs are untouched); a dirty state becomes a ref-less snapshot commit on HEAD (committed as mam-loop, not as the user). An untracked nested repository cannot be pinned and is refused. identity = (loop_id, base, head, tree). - Review isolation (B4): a detached review worktree of the snapshot under TMPDIR (default), or `--review-isolation env` (the source root with env and temp-dir instructions). Failing to open the tree refuses the round - the isolation is never lowered silently. Trees are removed after the round and on any exit (run() wraps the body; release_guard too). - The reviewers' diff comes from the snapshot (B13): diff_collect.sh takes an optional head commit for a commit-to-commit diff; its two-argument form is byte-identical. - Stale rounds (B7/B8): after the round the source is measured again; if it changed, the round's approvals are discarded and the next iteration re-reviews without a correction job (last iteration: rc 1). Only the last non-stale round's PASSes are promoted. - Records (B9/B15): review_rounds in the checkpoint with per-reviewer transport/evidence, and report-<jid>.identity.json next to each promoted report: evidence review-tree | source-tree | diff-only. - Pre-promotion re-check (B10): a source changed after approval is not promoted (rc 1). - Remote reviewers (B14, Creator challenge 91c81095): they cannot reach this host's review tree. Checked before anything is delegated: refused by default; `--remote-review diff-only` admits them with the snapshot SHAs and diff as their only evidence (no local path in their prompt), and refuses a round whose diff is truncated or unavailable. - run_loop.sh accepts both options (usage identical to the runner's); the legacy shell loop refuses them before the lock. Tests tests/test_issue21_loop_b.py L1-L16 (22). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017R3Eg7MEpSWnm1oB9ZPP6G
This commit is contained in:
1 parent
4ad622e372
commit
e57663ac4c
4 files changed
+748
-17
No files matched your search
@@ -0,0 +1,397 @@
|
||||
"""Issue #21 phase 2, loop B, item 5: review identity.
|
||||
|
||||
With --worktree/--base every review round is pinned to a snapshot of the
|
||||
source (loop_id, base, head, tree), reviewed in a detached review worktree (or
|
||||
env isolation), judged stale when the source changes during the round, and
|
||||
re-checked before promotion. Remote reviewers cannot reach this host's review
|
||||
tree: refused by default, admitted diff-only on request, recorded as such.
|
||||
Without the options nothing changes. All repositories live under tmp_path.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from test_issue21_worktree_prereqs import GIT_ID, RUN_LOOP_SH, SKILLS, _git, wt_repo # noqa: F401
|
||||
|
||||
DIFF_SH = SKILLS / "multi-agent-mux-loop" / "scripts" / "diff_collect.sh"
|
||||
REMOTE_ROW = {"name": "reviewer-far-01", "agent": "claude", "role": "reviewer", "status": "running",
|
||||
"transport": "remote"}
|
||||
LOCAL_ROWS = [{"name": "creator-claude-01", "agent": "claude", "role": "creator", "status": "running"},
|
||||
{"name": "reviewer-claude-01", "agent": "claude", "role": "reviewer", "status": "running"}]
|
||||
|
||||
|
||||
def _state(monkeypatch, rows):
|
||||
monkeypatch.setenv("MAM_STATE_JSON", json.dumps({"herdr_sessions": rows}))
|
||||
|
||||
|
||||
def _drive(monkeypatch, tmp_path, argv, on_review=None, reviewers="reviewer-claude-01", max_loop=1):
|
||||
"""run() with delegation stubbed: every job completes, every report is a
|
||||
PASS; returns (rc, [(session, role, prompt)], runner)."""
|
||||
from lib_py.loop import runner as lr
|
||||
monkeypatch.setattr(lr.signal, "signal", lambda *a, **k: None)
|
||||
args = ["--creator", "creator-claude-01", "--task", "t21", "--max-loop", str(max_loop), *argv]
|
||||
if reviewers:
|
||||
args += ["--reviewer", reviewers]
|
||||
r = lr.LoopRunner(args)
|
||||
calls = []
|
||||
report = tmp_path / "pass-report.md"
|
||||
report.write_text("looks fine\n\n[VERDICT: PASS]\n")
|
||||
|
||||
def fake_delegate(session, agent_type, role, prompt):
|
||||
calls.append((session, role, prompt))
|
||||
if role == "Reviewer" and on_review:
|
||||
on_review(r, len(calls), prompt)
|
||||
return f"j{len(calls)}"
|
||||
|
||||
monkeypatch.setattr(r, "delegate_job", fake_delegate)
|
||||
monkeypatch.setattr(r, "wait_for_job", lambda *a, **k: True)
|
||||
monkeypatch.setattr(r, "find_report", lambda jid: report)
|
||||
return r.run(), calls, r
|
||||
|
||||
|
||||
def _reviews(calls):
|
||||
return [(s, p) for s, role, p in calls if role == "Reviewer"]
|
||||
|
||||
|
||||
def _impl(wt: Path) -> str:
|
||||
"""Commit an implementation in the worktree; return the pre-implementation sha."""
|
||||
before = _git(wt, "rev-parse", "HEAD")
|
||||
(wt / "impl.txt").write_text("impl line\n")
|
||||
_git(wt, "add", "impl.txt")
|
||||
_git(wt, "commit", "-q", "-m", "impl")
|
||||
return before
|
||||
|
||||
|
||||
def _worktree_count(main: Path) -> int:
|
||||
return _git(main, "worktree", "list", "--porcelain").count("\nworktree ") + 1
|
||||
|
||||
|
||||
def _sidecars(main: Path) -> list:
|
||||
return sorted((main / ".agents" / "reports").rglob("*.identity.json"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_l1_without_options_nothing_changes(wt_repo, monkeypatch, tmp_path):
|
||||
main, _ = wt_repo
|
||||
(main / "a.txt").write_text("a\nchanged in main\n")
|
||||
n = _worktree_count(main)
|
||||
rc, calls, r = _drive(monkeypatch, tmp_path, [])
|
||||
assert rc == 0
|
||||
assert all("[리뷰 identity]" not in p and "[워크트리 컨텍스트]" not in p for _, _, p in calls)
|
||||
assert _worktree_count(main) == n
|
||||
assert r.loop_id.startswith("loop-") and r._rounds == []
|
||||
assert _sidecars(main) == []
|
||||
|
||||
|
||||
def test_l2_snapshot_pins_uncommitted_and_untracked_not_ignored(wt_repo):
|
||||
main, wt = wt_repo
|
||||
(wt / ".gitignore").write_text("*.log\n")
|
||||
_git(wt, "add", ".gitignore")
|
||||
_git(wt, "commit", "-q", "-m", "ignore logs")
|
||||
(wt / "a.txt").write_text("a\nmodified\n")
|
||||
(wt / "new.txt").write_text("untracked\n")
|
||||
(wt / "x.log").write_text("ignored\n")
|
||||
status0 = _git(wt, "status", "--porcelain")
|
||||
refs0 = _git(wt, "for-each-ref")
|
||||
from lib_py.loop.runner import LoopRunner
|
||||
r = LoopRunner(["--creator", "creator-claude-01", "--task", "t", "--worktree", str(wt)])
|
||||
r.parse_args()
|
||||
r.base_commit = _git(wt, "rev-parse", "HEAD")
|
||||
snap = r._snapshot(1)
|
||||
assert snap and snap["dirty"] and snap["review_commit"] != snap["head"]
|
||||
assert r._worktree_tree() == snap["tree"], "the snapshot tree is stable for an unchanged tree"
|
||||
path = Path(r._open_review_tree(snap, 1))
|
||||
try:
|
||||
assert (path / "new.txt").read_text() == "untracked\n"
|
||||
assert "modified" in (path / "a.txt").read_text()
|
||||
assert not (path / "x.log").exists(), "ignored files stay out of the snapshot"
|
||||
assert _git(path, "rev-parse", "HEAD") == snap["review_commit"]
|
||||
finally:
|
||||
r._close_review_tree(str(path))
|
||||
assert not path.exists()
|
||||
assert _git(wt, "status", "--porcelain") == status0 and _git(wt, "for-each-ref") == refs0
|
||||
|
||||
|
||||
def test_l3_l5_stale_round_is_re_reviewed_and_the_sidecar_matches(wt_repo, monkeypatch, tmp_path):
|
||||
main, wt = wt_repo
|
||||
before = _impl(wt)
|
||||
seen = {}
|
||||
|
||||
def creator_commits_during_first_review(r, n, prompt):
|
||||
if "h1" not in seen:
|
||||
seen["h1"] = _git(wt, "rev-parse", "HEAD")
|
||||
(wt / "late.txt").write_text("late\n")
|
||||
_git(wt, "add", "late.txt")
|
||||
_git(wt, "commit", "-q", "-m", "late")
|
||||
seen["h2"] = _git(wt, "rev-parse", "HEAD")
|
||||
|
||||
rc, calls, r = _drive(monkeypatch, tmp_path, ["--worktree", str(wt), "--base", before, "--review-only"],
|
||||
on_review=creator_commits_during_first_review, max_loop=2)
|
||||
assert rc == 0
|
||||
assert [role for _, role, _ in calls] == ["Reviewer", "Reviewer"], "no correction job for a stale round"
|
||||
assert r._rounds[0]["stale"] is True and r._rounds[1]["stale"] is False
|
||||
assert r._rounds[1]["head_sha"] == seen["h2"]
|
||||
assert f"- head: {seen['h2']}" in _reviews(calls)[1][1]
|
||||
(side,) = _sidecars(main)
|
||||
data = json.loads(side.read_text())
|
||||
assert data["loop_id"] == r.loop_id and data["head_sha"] == seen["h2"]
|
||||
assert data["tree_sha"] == r._rounds[1]["tree_sha"] and data["base_sha"] == before
|
||||
assert (data["transport"], data["evidence"], data["isolation"]) == ("herdr", "review-tree", "worktree")
|
||||
|
||||
|
||||
def test_l3b_stale_last_round_fails(wt_repo, monkeypatch, tmp_path, capsys):
|
||||
main, wt = wt_repo
|
||||
before = _impl(wt)
|
||||
|
||||
def commit(r, n, prompt):
|
||||
(wt / f"late{n}.txt").write_text("late\n")
|
||||
_git(wt, "add", ".")
|
||||
_git(wt, "commit", "-q", "-m", "late")
|
||||
|
||||
rc, calls, r = _drive(monkeypatch, tmp_path, ["--worktree", str(wt), "--base", before, "--review-only"],
|
||||
on_review=commit, max_loop=1)
|
||||
assert rc == 1 and "changed during review" in capsys.readouterr().err
|
||||
assert _sidecars(main) == []
|
||||
|
||||
|
||||
def test_l4_source_changed_after_approval_is_not_promoted(wt_repo, monkeypatch, tmp_path, capsys):
|
||||
main, wt = wt_repo
|
||||
before = _impl(wt)
|
||||
from lib_py.loop import runner as lr
|
||||
real = lr.LoopRunner._identity_now
|
||||
state = {"accepted": False}
|
||||
|
||||
def identity_now(self):
|
||||
return ("changed", "changed") if state["accepted"] else real(self)
|
||||
|
||||
def mark(r, n, prompt):
|
||||
state["accepted"] = False
|
||||
|
||||
monkeypatch.setattr(lr.LoopRunner, "_identity_now", identity_now)
|
||||
monkeypatch.setattr(lr.LoopRunner, "_round_is_stale",
|
||||
lambda self, snap: state.__setitem__("accepted", True) or False)
|
||||
rc, calls, r = _drive(monkeypatch, tmp_path, ["--worktree", str(wt), "--base", before, "--review-only"],
|
||||
on_review=mark)
|
||||
assert rc == 1 and "source changed after approval" in capsys.readouterr().err
|
||||
assert not list((main / ".agents" / "reports").rglob("report-*.md"))
|
||||
|
||||
|
||||
def test_l6_env_isolation_reviews_the_source_tree(wt_repo, monkeypatch, tmp_path):
|
||||
main, wt = wt_repo
|
||||
before = _impl(wt)
|
||||
n = _worktree_count(main)
|
||||
counts = []
|
||||
rc, calls, r = _drive(monkeypatch, tmp_path,
|
||||
["--worktree", str(wt), "--base", before, "--review-only", "--review-isolation", "env"],
|
||||
on_review=lambda r, i, p: counts.append(_worktree_count(main)))
|
||||
assert rc == 0 and counts == [n], "env isolation opens no review worktree"
|
||||
prompt = _reviews(calls)[0][1]
|
||||
assert f"- 검증 경로: {os.path.realpath(wt)}" in prompt and "PYTHONDONTWRITEBYTECODE=1" in prompt
|
||||
data = json.loads(_sidecars(main)[0].read_text())
|
||||
assert (data["evidence"], data["isolation"]) == ("source-tree", "env")
|
||||
|
||||
|
||||
def test_l7_review_tree_failure_refuses_without_lowering_isolation(wt_repo, monkeypatch, tmp_path, capsys):
|
||||
main, wt = wt_repo
|
||||
before = _impl(wt)
|
||||
from lib_py.loop import runner as lr
|
||||
real_git = lr.LoopRunner._git
|
||||
|
||||
def git(cwd, *args):
|
||||
if args[:2] == ("worktree", "add"):
|
||||
return subprocess.CompletedProcess(args, 128, "", "fatal: simulated")
|
||||
return real_git(cwd, *args)
|
||||
|
||||
monkeypatch.setattr(lr.LoopRunner, "_git", staticmethod(git))
|
||||
rc, calls, r = _drive(monkeypatch, tmp_path, ["--worktree", str(wt), "--base", before, "--review-only"])
|
||||
assert rc == 1 and _reviews(calls) == []
|
||||
assert "cannot open the review worktree" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_l8_release_guard_closes_open_review_trees(wt_repo):
|
||||
main, wt = wt_repo
|
||||
(wt / "a.txt").write_text("a\nx\n")
|
||||
from lib_py.loop.runner import LoopRunner
|
||||
r = LoopRunner(["--creator", "creator-claude-01", "--task", "t", "--worktree", str(wt)])
|
||||
r.parse_args()
|
||||
r.base_commit = _git(wt, "rev-parse", "HEAD")
|
||||
path = r._open_review_tree(r._snapshot(1), 1)
|
||||
assert os.path.isdir(path)
|
||||
r.release_guard()
|
||||
assert not os.path.exists(path) and path not in _git(main, "worktree", "list")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("opt", [["--review-isolation", "env"], ["--remote-review", "diff-only"]])
|
||||
def test_l9_l16_legacy_shell_loop_refuses_the_new_options(tmp_path, opt):
|
||||
marker = tmp_path / "loop-marker"
|
||||
env = {**os.environ, "WORKSPACE_ROOT": str(tmp_path), "MAM_LOOP_PY": "0", "MAM_LOOP_NO_FREEZE": "1",
|
||||
"MAM_LOOP_MARKER": str(marker)}
|
||||
res = subprocess.run(["bash", str(RUN_LOOP_SH), "--creator", "c", "--task", "t", *opt],
|
||||
capture_output=True, text=True, env=env, cwd=str(tmp_path), timeout=60)
|
||||
assert res.returncode == 1 and "require the Python loop (MAM_LOOP_PY=1)" in res.stderr
|
||||
assert not marker.exists()
|
||||
|
||||
|
||||
def test_l9b_invalid_values_are_rejected(wt_repo):
|
||||
from lib_py.loop.runner import LoopRunner
|
||||
for opt in (["--review-isolation", "bogus"], ["--remote-review", "maybe"]):
|
||||
with pytest.raises(SystemExit) as e:
|
||||
LoopRunner(["--creator", "c", "--task", "t", *opt]).parse_args()
|
||||
assert e.value.code == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize("variant", ["reviewer", "all-reviewer", "env", "self-review"])
|
||||
def test_l10_remote_reviewer_refused_before_any_delegation(wt_repo, monkeypatch, tmp_path, capsys, variant):
|
||||
main, wt = wt_repo
|
||||
before = _impl(wt)
|
||||
rows = LOCAL_ROWS + [REMOTE_ROW]
|
||||
argv = ["--worktree", str(wt), "--base", before, "--review-only"]
|
||||
reviewers = "reviewer-far-01"
|
||||
if variant == "all-reviewer":
|
||||
argv.append("--all-reviewer")
|
||||
reviewers = None
|
||||
elif variant == "env":
|
||||
argv += ["--review-isolation", "env"]
|
||||
elif variant == "self-review":
|
||||
rows = [dict(LOCAL_ROWS[0], transport="remote")]
|
||||
reviewers = None
|
||||
_state(monkeypatch, rows)
|
||||
rc, calls, r = _drive(monkeypatch, tmp_path, argv, reviewers=reviewers)
|
||||
err = capsys.readouterr().err
|
||||
assert rc == 1 and calls == [], "nothing is delegated"
|
||||
assert "--remote-review diff-only" in err
|
||||
assert ("creator-claude-01" if variant == "self-review" else "reviewer-far-01") in err
|
||||
|
||||
|
||||
def test_l11_diff_only_remote_reviewer_gets_shas_and_the_snapshot_diff(wt_repo, monkeypatch, tmp_path):
|
||||
main, wt = wt_repo
|
||||
before = _impl(wt)
|
||||
(wt / "a.txt").write_text("a\nuncommitted change\n")
|
||||
_state(monkeypatch, LOCAL_ROWS + [REMOTE_ROW])
|
||||
rc, calls, r = _drive(monkeypatch, tmp_path,
|
||||
["--worktree", str(wt), "--base", before, "--review-only", "--remote-review", "diff-only"],
|
||||
reviewers="reviewer-claude-01,reviewer-far-01")
|
||||
assert rc == 0
|
||||
prompts = dict(_reviews(calls))
|
||||
remote, local = prompts["reviewer-far-01"], prompts["reviewer-claude-01"]
|
||||
snap = r._rounds[0]
|
||||
assert os.path.realpath(wt) not in remote and "검증 경로" not in remote and "mam-review" not in remote
|
||||
assert f"- head: {snap['head_sha']}" in remote and f"- tree: {snap['tree_sha']}" in remote
|
||||
assert "diff 만으로" in remote and "+uncommitted change" in remote and "+impl line" in remote
|
||||
assert "- 검증 경로: " in local and "mam-review" in local
|
||||
assert snap["reviewers"]["reviewer-far-01"]["evidence"] == "diff-only"
|
||||
assert snap["reviewers"]["reviewer-claude-01"]["evidence"] == "review-tree"
|
||||
side = {json.loads(p.read_text())["reviewer"]: json.loads(p.read_text()) for p in _sidecars(main)}
|
||||
assert (side["reviewer-far-01"]["evidence"], side["reviewer-far-01"]["isolation"],
|
||||
side["reviewer-far-01"]["transport"]) == ("diff-only", "none (remote)", "remote")
|
||||
assert side["reviewer-claude-01"]["evidence"] == "review-tree"
|
||||
|
||||
|
||||
def test_l12_remote_reviewer_without_options_works_as_before(wt_repo, monkeypatch, tmp_path, capsys):
|
||||
main, _ = wt_repo
|
||||
(main / "a.txt").write_text("a\nchanged\n")
|
||||
_state(monkeypatch, LOCAL_ROWS + [REMOTE_ROW])
|
||||
rc, calls, r = _drive(monkeypatch, tmp_path, ["--remote-review", "diff-only"], reviewers="reviewer-far-01")
|
||||
assert rc == 0 and [s for s, _ in _reviews(calls)] == ["reviewer-far-01"]
|
||||
assert "[리뷰 identity]" not in _reviews(calls)[0][1] and _sidecars(main) == []
|
||||
assert "only apply with --worktree or --base" in capsys.readouterr().out
|
||||
|
||||
|
||||
def _diff_collect(repo, *args, env=None):
|
||||
cmd = "source " + str(DIFF_SH) + " && mam_collect_changes_diff " + " ".join(f"'{a}'" for a in (repo, *args))
|
||||
return subprocess.run(["bash", "-c", cmd], capture_output=True, text=True, env={**os.environ, **(env or {})})
|
||||
|
||||
|
||||
def test_l13_snapshot_diff_is_commit_to_commit(wt_repo, monkeypatch, tmp_path):
|
||||
main, wt = wt_repo
|
||||
base = _git(wt, "rev-parse", "HEAD")
|
||||
(wt / ".gitignore").write_text("*.log\n")
|
||||
(wt / "a.txt").write_text("a\nmodified\n")
|
||||
(wt / "new.txt").write_text("untracked\n")
|
||||
(wt / "x.log").write_text("ignored\n")
|
||||
from lib_py.loop.runner import LoopRunner
|
||||
r = LoopRunner(["--creator", "creator-claude-01", "--task", "t", "--worktree", str(wt)])
|
||||
r.parse_args()
|
||||
r.base_commit = base
|
||||
snap = r._snapshot(1)
|
||||
out = _diff_collect(wt, base, snap["review_commit"])
|
||||
assert out.returncode == 0 and "+modified" in out.stdout and "+untracked" in out.stdout
|
||||
assert "x.log" not in out.stdout
|
||||
bad = _diff_collect(wt, base, "0" * 40)
|
||||
assert bad.returncode == 2 and "CHANGE SET UNAVAILABLE" in bad.stdout
|
||||
cut = _diff_collect(wt, base, snap["review_commit"], env={"MAM_DIFF_MAX_LINES": "1"})
|
||||
assert "DIFF TRUNCATED" in cut.stdout and f"inspect the review snapshot ({snap['review_commit']})" in cut.stdout
|
||||
# the two-argument form is unchanged: same output as the script before this change
|
||||
old = subprocess.run(["git", "-C", str(SKILLS.parent.parent), "show", "25f4be3:.agents/skills/multi-agent-mux-loop/scripts/diff_collect.sh"],
|
||||
capture_output=True, text=True)
|
||||
old_sh = tmp_path / "diff_collect_old.sh"
|
||||
old_sh.write_text(old.stdout)
|
||||
new2 = _diff_collect(wt, base)
|
||||
old2 = subprocess.run(["bash", "-c", f"source '{old_sh}' && mam_collect_changes_diff '{wt}' '{base}'"],
|
||||
capture_output=True, text=True)
|
||||
assert (new2.returncode, new2.stdout) == (old2.returncode, old2.stdout)
|
||||
|
||||
|
||||
def test_l13b_prompt_diff_comes_from_the_snapshot(wt_repo, monkeypatch, tmp_path):
|
||||
main, wt = wt_repo
|
||||
before = _impl(wt)
|
||||
from lib_py.loop import runner as lr
|
||||
real = lr.LoopRunner._snapshot
|
||||
|
||||
def snapshot_then_edit(self, n):
|
||||
s = real(self, n)
|
||||
(wt / "after-snapshot.txt").write_text("written after the snapshot\n")
|
||||
return s
|
||||
|
||||
monkeypatch.setattr(lr.LoopRunner, "_snapshot", snapshot_then_edit)
|
||||
rc, calls, r = _drive(monkeypatch, tmp_path, ["--worktree", str(wt), "--base", before, "--review-only"])
|
||||
prompt = _reviews(calls)[0][1]
|
||||
assert "+impl line" in prompt and "after-snapshot.txt" not in prompt
|
||||
assert rc == 1, "the source changed after the snapshot: the round is stale"
|
||||
|
||||
|
||||
def test_l14_untracked_nested_repository_is_refused(wt_repo, monkeypatch, tmp_path, capsys):
|
||||
main, wt = wt_repo
|
||||
before = _impl(wt)
|
||||
nested = wt / "nested"
|
||||
nested.mkdir()
|
||||
_git(nested, "init", "-q")
|
||||
(nested / "n.txt").write_text("n\n")
|
||||
_git(nested, "add", "n.txt")
|
||||
_git(nested, "commit", "-q", "-m", "n")
|
||||
rc, calls, r = _drive(monkeypatch, tmp_path, ["--worktree", str(wt), "--base", before, "--review-only"])
|
||||
assert rc == 1 and _reviews(calls) == []
|
||||
assert "untracked nested git repository nested" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_l14b_nested_repository_without_options_proceeds(wt_repo, monkeypatch, tmp_path):
|
||||
main, _ = wt_repo
|
||||
nested = main / "nested"
|
||||
nested.mkdir()
|
||||
_git(nested, "init", "-q")
|
||||
rc, calls, r = _drive(monkeypatch, tmp_path, [])
|
||||
assert rc == 0 and len(_reviews(calls)) == 1
|
||||
|
||||
|
||||
def test_l15_truncated_diff_refuses_diff_only_but_not_local(wt_repo, monkeypatch, tmp_path, capsys):
|
||||
main, wt = wt_repo
|
||||
before = _impl(wt)
|
||||
# the runner uses the control root's collector (the python fallback never truncates)
|
||||
dst = main / ".agents" / "skills" / "multi-agent-mux-loop" / "scripts"
|
||||
dst.mkdir(parents=True)
|
||||
(dst / "diff_collect.sh").write_text(DIFF_SH.read_text())
|
||||
monkeypatch.setenv("MAM_DIFF_MAX_LINES", "1")
|
||||
_state(monkeypatch, LOCAL_ROWS + [REMOTE_ROW])
|
||||
argv = ["--worktree", str(wt), "--base", before, "--review-only", "--remote-review", "diff-only"]
|
||||
rc, calls, r = _drive(monkeypatch, tmp_path, argv, reviewers="reviewer-claude-01,reviewer-far-01")
|
||||
assert rc == 1 and _reviews(calls) == [] and "would have no evidence" in capsys.readouterr().err
|
||||
rc, calls, r = _drive(monkeypatch, tmp_path, argv, reviewers="reviewer-claude-01")
|
||||
assert rc == 0 and len(_reviews(calls)) == 1
|
||||
Reference in new issue
Block a user