fix(git): reviewer reads must prefer origin over a diverged local ref (#690)

* fix(git): reviewer reads must prefer origin over a diverged local ref

_resolve_head_ref (GitService.diff/list_changed_files/read_file_at_branch)
kept local priority on ANY divergence from origin, real or rewritten. A
reviewer's clone parked on pre-rebase history after the branch's routine
force-push sync stayed frozen there across every subsequent review round,
while origin held every fix commit — QA repeatedly bounced work that had
already landed. Every caller here is a reader, never the branch's own
author mid-write, so origin now wins whenever it carries anything the
local ref lacks; local keeps priority only when it strictly contains
origin (unpushed commits, or equal).

The read-only git MCP surface (roboco_git_log) hit the same staleness
through a separate path: /api/git/log resolved the requested branch as a
bare name straight off whatever the caller's own clone had on disk, with
no fetch at all. It now routes through the same fixed _resolve_head_ref.

* test(e2e): give the armed flow-verb timeout real headroom

The armed value is also verb-2's entire execution budget (claim + every
claim guard + set_plan + start + tracing gate), which grows as guards
land; 1s flaked on loaded CI runners while passing locally. The
cancel-and-release semantics only need the timeout far below the hang.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-24 17:50:13 +02:00
committed by GitHub
co-authored by Renn F
parent eb0dcb6ecb
commit e97f46af6e
8 changed files with 277 additions and 34 deletions
+31
View File
@@ -266,6 +266,37 @@ async def test_log_with_branch_success(git_client: dict) -> None:
assert [c["author"] for c in commits] == ["me", "you"]
@pytest.mark.asyncio
async def test_log_resolves_through_head_ref_not_bare_branch(
git_client: dict,
) -> None:
"""The route must route the requested branch through
``_resolve_head_ref`` (fetch + prefer origin) instead of handing git the
bare branch name straight off whatever this clone happens to have on
disk — this clone is the CALLER's own, never the branch owner's, and a
left-over local ref from an earlier inspection can be pinned stale
(live 2026-07-24: a QA clone read a commit 5 review rounds old)."""
log_result = MagicMock()
log_result.returncode = 0
log_result.stdout = ""
with patch("roboco.api.routes.git.get_git_service") as mock_get:
svc = AsyncMock()
svc.get_workspace = AsyncMock(return_value="/tmp/ws")
svc._token_for_branch = AsyncMock(return_value="tok")
svc._resolve_head_ref = AsyncMock(return_value="origin/feature/x")
svc._run_git = AsyncMock(return_value=log_result)
mock_get.return_value = svc
response = await git_client["client"].get(
f"/api/git/log?project_slug={git_client['project'].slug}&branch=feature/x",
headers=_HDR,
)
assert response.status_code == HTTPStatus.OK
svc._resolve_head_ref.assert_awaited_once_with("/tmp/ws", "feature/x", token="tok")
svc._run_git.assert_awaited_once()
logged_args = svc._run_git.await_args.args[1]
assert logged_args[-1] == "origin/feature/x"
@pytest.mark.asyncio
async def test_log_no_branch_fetches_current(git_client: dict) -> None:
log_result = MagicMock()