fix(git): resolve diff HEAD ref per-workspace so QA/doc/PM see real diffs (#161 facet)

Smoke-14: QA's claim_review evidence had pr_diff_summary="" and
files_changed=[] on a PR with a real README change. Root cause: diff()
and list_changed_files() diffed against the bare local <branch_name>.
That ref only exists in the clone where the dev ran `git checkout -b`
at claim. QA / documenter / PM inspect from their OWN clones, where a
bare <branch_name> resolves refs/heads then refs/remotes/<name> but
NEVER refs/remotes/origin/<name> — so `git diff base...<branch>` had an
unresolvable HEAD and silently returned an empty diff (run with
check=False).

#161 previously fixed the BASE side (cell-PM parent never pushed → fall
back to default branch). This is the symmetric HEAD-side facet.
open_pr pushes the leaf branch, so origin/<branch> is the
workspace-independent source of truth. New _resolve_head_ref fetches the
branch and prefers the local branch (dev's own clone, unchanged
behaviour), falling back to origin/<branch> (QA/doc/PM clones), then the
bare name so the command stays well-formed. diff() and
list_changed_files() route through it; explicit base (incremental dev
path, base=HEAD~1) is preserved.
This commit is contained in:
Renn F
2026-05-16 03:27:23 +02:00
parent 1605d187f1
commit 954acff911
2 changed files with 180 additions and 12 deletions
@@ -88,3 +88,140 @@ async def test_default_branch_ref_fallback_when_no_head() -> None:
svc._ref_exists = AsyncMock(return_value=False) # type: ignore[method-assign]
ref = await svc._default_branch_ref(Path("/tmp/ws"))
assert ref == "origin/master"
# ---------------------------------------------------------------------------
# Task #161 (facet): the diff HEAD side must resolve in the inspecting
# clone. The local <branch> ref only exists in the dev's own clone (the
# clone that ran `git checkout -b` at claim). QA / doc / PM diff from
# their OWN clones, which only have origin/<branch> after a fetch. Diffing
# against the bare local name there yields an empty diff (smoke-14: QA saw
# no changes on a real PR). _resolve_head_ref + diff()/list_changed_files
# must prefer the local branch, then origin/<branch>.
# ---------------------------------------------------------------------------
_BR = "feature/backend/root1234--cellpm56--dev78901"
@pytest.mark.asyncio
async def test_resolve_head_ref_prefers_local_branch_in_dev_clone() -> None:
"""Dev's own clone has the local branch — use it unchanged."""
svc = _git_service()
svc._run_git = AsyncMock() # type: ignore[method-assign]
svc._ref_exists = AsyncMock(return_value=True) # type: ignore[method-assign]
head = await svc._resolve_head_ref(Path("/tmp/ws"), _BR)
assert head == _BR
@pytest.mark.asyncio
async def test_resolve_head_ref_falls_back_to_origin_in_foreign_clone() -> None:
"""QA/doc/PM clone has no local branch but origin/<branch> exists
(open_pr pushed it) — diff must target origin/<branch>."""
svc = _git_service()
svc._run_git = AsyncMock() # type: ignore[method-assign]
async def ref_exists(_ws: Any, ref: str) -> bool:
# local branch absent; only the remote-tracking ref resolves.
return ref == f"origin/{_BR}"
svc._ref_exists = ref_exists # type: ignore[method-assign]
head = await svc._resolve_head_ref(Path("/tmp/ws"), _BR)
assert head == f"origin/{_BR}"
@pytest.mark.asyncio
async def test_resolve_head_ref_fetches_branch_before_resolving() -> None:
"""The branch is fetched into the workspace so origin/<branch> is
available even on a clone that never saw it."""
svc = _git_service()
calls: list[list[str]] = []
async def fake_run(_ws: Any, args: list[str], **_kw: Any) -> Any:
calls.append(args)
return type("R", (), {"returncode": 0, "stdout": ""})()
svc._run_git = fake_run # type: ignore[method-assign]
svc._ref_exists = AsyncMock(return_value=True) # type: ignore[method-assign]
await svc._resolve_head_ref(Path("/tmp/ws"), _BR)
assert ["fetch", "origin", _BR] in calls
@pytest.mark.asyncio
async def test_diff_targets_origin_head_in_foreign_clone() -> None:
"""Regression for smoke-14: diff() from QA's clone must compare
base...origin/<branch>, not base...<bare-local-branch> (which is
unresolvable there and silently produced an empty diff)."""
svc = _git_service()
svc._workspace_for_branch = AsyncMock( # type: ignore[method-assign]
return_value=Path("/tmp/qa-ws")
)
svc._resolve_diff_base = AsyncMock( # type: ignore[method-assign]
return_value="origin/master"
)
svc._resolve_head_ref = AsyncMock( # type: ignore[method-assign]
return_value=f"origin/{_BR}"
)
captured: list[list[str]] = []
async def fake_run(_ws: Any, args: list[str], **_kw: Any) -> Any:
captured.append(args)
return type("R", (), {"returncode": 0, "stdout": "diff body"})()
svc._run_git = fake_run # type: ignore[method-assign]
out = await svc.diff(branch_name=_BR)
assert out == "diff body"
assert captured == [["diff", f"origin/master...origin/{_BR}"]]
@pytest.mark.asyncio
async def test_list_changed_files_targets_origin_head_in_foreign_clone() -> None:
"""Same fix on the files_changed path (#154 evidence)."""
svc = _git_service()
svc._workspace_for_branch = AsyncMock( # type: ignore[method-assign]
return_value=Path("/tmp/qa-ws")
)
svc._resolve_diff_base = AsyncMock( # type: ignore[method-assign]
return_value="origin/master"
)
svc._resolve_head_ref = AsyncMock( # type: ignore[method-assign]
return_value=f"origin/{_BR}"
)
captured: list[list[str]] = []
async def fake_run(_ws: Any, args: list[str], **_kw: Any) -> Any:
captured.append(args)
return type("R", (), {"returncode": 0, "stdout": "README.md\nsrc/app.py\n"})()
svc._run_git = fake_run # type: ignore[method-assign]
files = await svc.list_changed_files(branch_name=_BR)
assert files == ["README.md", "src/app.py"]
assert captured == [["diff", "--name-only", f"origin/master...origin/{_BR}"]]
@pytest.mark.asyncio
async def test_diff_honours_explicit_base_with_resolved_head() -> None:
"""The incremental dev path (base=HEAD~1) still works: explicit base
is preserved, head still goes through _resolve_head_ref."""
svc = _git_service()
svc._workspace_for_branch = AsyncMock( # type: ignore[method-assign]
return_value=Path("/tmp/dev-ws")
)
svc._resolve_diff_base = AsyncMock( # type: ignore[method-assign]
return_value="SHOULD_NOT_BE_USED"
)
svc._resolve_head_ref = AsyncMock( # type: ignore[method-assign]
return_value=_BR
)
captured: list[list[str]] = []
async def fake_run(_ws: Any, args: list[str], **_kw: Any) -> Any:
captured.append(args)
return type("R", (), {"returncode": 0, "stdout": ""})()
svc._run_git = fake_run # type: ignore[method-assign]
await svc.diff(branch_name=_BR, base="HEAD~1")
assert captured == [["diff", f"HEAD~1...{_BR}"]]
svc._resolve_diff_base.assert_not_awaited()