[a1f52d13] Rebuild evidence-assembly timeout fix on current main (#796)

* [a1f52d13] feat(gateway): re-apply evidence-assembly timeout fix on current main

Rebuilds the bounded-timeout fix for claim_review/evidence()/roboco_git_diff
that PR #765 carried as contaminated pre-Board-Programs file copies. The
rebase restored the current-main baseline; this commit re-applies ONLY the
validated timeout-fix hunks on top of that baseline:

- config.py: evidence_assembly_timeout_seconds (90.0s) and
  conventions_validator_timeout_seconds (45s) Settings fields
- envelope.py: Envelope.gateway_timeout classmethod for structured
  timeout errors naming the stalled component
- git.py: GitService.diff_and_files (single resolution, concurrent
  diff+--name-only subprocesses), settings-backed conventions-validator
  timeout, and changed_files passthrough to conventions_check_for_task
- qa.py: claim_review bounded-timeout wrapper around
  _build_qa_claim_evidence, split into _qa_git_and_conventions and
  _qa_db_reads concurrent segments (pass_review signature and gate
  untouched)
- content_actions.py: evidence() bounded wrapper with _evidence_git_and_fetch
  and _evidence_db_reads concurrent split (all other methods untouched)
- git routes: GET /diff bounded asyncio.wait_for guard with 504 on trip

The pre-existing methods (sync_env_branch, open_sync_pr, pass_review with
criteria_verified gate, all propose_*/request_render methods) are preserved
and untouched. The test files already exist on current main.

* [a1f52d13] fix(git-routes): wrap workspace resolution in bounded timeout for GET /diff

The bounded asyncio.wait_for must cover the workspace resolution step too,
not just the diff+stat subprocesses — the integration test makes
get_workspace slow (not _run_git), so the timeout guard needs to enclose
the entire resolve-and-diff path to return a structured 504 on trip.

* [a1f52d13] test: update mock setups from git.diff/list_changed_files to diff_and_files

Tests that exercise claim_review and evidence() paths need their git
mocks to return a tuple from diff_and_files instead of separate diff
and list_changed_files return values, matching the new combined accessor
introduced by the evidence-assembly timeout fix.

* [a1f52d13] test: configure diff_and_files mock in lifecycle parity test _make_deps

The spec-parity test test_claim_review_matches_spec[qa-awaiting_qa] failed
with ValueError at qa.py:330 because _make_deps set git=AsyncMock() without
configuring diff_and_files.return_value, so the await resolved to a MagicMock
instead of a (diff_summary, files_changed) 2-tuple. Adds the same
return_value = ("", []) configuration that commit 014a0d03 applied to the
other 4 test files.

* [a1f52d13] docs(evidence-assembly-timeout): correct GET /diff guard scope and _qa_git_and_conventions timing field

---------

Co-authored-by: Backend Developer 2 <be-dev-2@roboco.tech>
Co-authored-by: Backend Documenter <be-doc@roboco.tech>
This commit is contained in:
roboco-app[bot]
2026-08-02 03:22:25 +00:00
committed by GitHub
co-authored by Backend Developer 2 Backend Documenter
parent 89254f796c
commit ce89a04d26
12 changed files with 6314 additions and 974 deletions
@@ -41,10 +41,12 @@ def _over_cap_project() -> MagicMock:
def _make_deps(task_svc: AsyncMock, **overrides: Any) -> ChoreographerDeps:
git = AsyncMock()
git.diff_and_files.return_value = ("", [])
base: dict[str, Any] = {
"task": task_svc,
"work_session": AsyncMock(),
"git": AsyncMock(),
"git": git,
"a2a": AsyncMock(),
"journal": AsyncMock(),
"audit": AsyncMock(),
@@ -37,7 +37,7 @@ def _make_deps(**overrides: AsyncMock) -> ContentActionsDeps:
else:
git = AsyncMock()
git.commit.return_value = {"sha": "abc12345"}
git.diff.return_value = ""
git.diff_and_files.return_value = ("", [])
a2a = overrides.get("a2a", AsyncMock())
journal = overrides.get("journal", AsyncMock())
@@ -270,7 +270,7 @@ async def test_evidence_blocks_when_not_assignee() -> None:
task_svc = AsyncMock()
task_svc.get.return_value = task_obj
git_svc = AsyncMock()
git_svc.diff.return_value = ""
git_svc.diff_and_files.return_value = ("", [])
workspace_svc = AsyncMock()
deps = _make_deps(task=task_svc, git=git_svc, workspace=workspace_svc)
ca = ContentActions(deps)
@@ -279,7 +279,7 @@ async def test_evidence_blocks_when_not_assignee() -> None:
body = env.as_dict()
assert body["error"] == "not_authorized"
workspace_svc.fetch_branch_for_inspection.assert_not_awaited()
git_svc.diff.assert_not_awaited()
git_svc.diff_and_files.assert_not_awaited()
# ---------------------------------------------------------------------------
@@ -410,7 +410,7 @@ async def test_evidence_unassigned_task_allows_inspection() -> None:
task_svc = AsyncMock()
task_svc.get.return_value = task_obj
git_svc = AsyncMock()
git_svc.diff.return_value = "diff content"
git_svc.diff_and_files.return_value = ("diff content", [])
workspace_svc = AsyncMock()
deps = _make_deps(task=task_svc, git=git_svc, workspace=workspace_svc)
ca = ContentActions(deps)
@@ -446,8 +446,7 @@ async def test_evidence_allows_dependency_inspection() -> None:
task_svc.get.return_value = target
task_svc.list_assigned_for_agent.return_value = [callers_task]
git_svc = AsyncMock()
git_svc.diff.return_value = ""
git_svc.list_changed_files.return_value = []
git_svc.diff_and_files.return_value = ("", [])
workspace_svc = AsyncMock()
deps = _make_deps(task=task_svc, git=git_svc, workspace=workspace_svc)
ca = ContentActions(deps)
@@ -7,7 +7,7 @@ Bug:
commit's delta, even when GitHub showed a multi-commit change set.
Fix:
Pull files via ``git.list_changed_files(branch_name=...)`` (no base
Pull files via ``git.diff_and_files(branch_name=...)`` (no base
→ full diff vs parent branch). Pull diff with ``base=None`` so the
full PR diff comes through. Both use git as the authoritative source
instead of the legacy ``work_session.files_modified`` field, which
@@ -65,8 +65,10 @@ async def test_evidence_populates_files_changed_from_git() -> None:
task_svc = AsyncMock()
task_svc.get.return_value = _task_with_pr(task_id, commits=["abc", "def"])
git_svc = AsyncMock()
git_svc.diff.return_value = "diff --git a/README.md b/README.md\n+added line\n"
git_svc.list_changed_files.return_value = ["README.md", "docs/guide.md"]
git_svc.diff_and_files.return_value = (
"diff --git a/README.md b/README.md\n+added line\n",
["README.md", "docs/guide.md"],
)
workspace_svc = AsyncMock()
evidence_repo = AsyncMock()
evidence_repo.journal_highlights_for_task.return_value = []
@@ -80,12 +82,12 @@ async def test_evidence_populates_files_changed_from_git() -> None:
assert body["error"] is None
assert body["evidence"]["files_changed"] == ["README.md", "docs/guide.md"]
assert "diff --git" in body["evidence"]["pr_diff_summary"]
git_svc.list_changed_files.assert_awaited_once()
git_svc.diff_and_files.assert_awaited_once()
@pytest.mark.asyncio
async def test_evidence_uses_full_pr_diff_not_head_minus_one() -> None:
"""git.diff must be called with base=None (full PR diff vs parent),
"""diff_and_files must be called with base=None (full PR diff vs parent),
not base='HEAD~1' (only the last commit)."""
agent_id = uuid4()
task_id = uuid4()
@@ -94,8 +96,7 @@ async def test_evidence_uses_full_pr_diff_not_head_minus_one() -> None:
# task.commits was non-empty, masking earlier commits' changes.
task_svc.get.return_value = _task_with_pr(task_id, commits=["sha1", "sha2", "sha3"])
git_svc = AsyncMock()
git_svc.diff.return_value = "full diff"
git_svc.list_changed_files.return_value = []
git_svc.diff_and_files.return_value = ("full diff", [])
workspace_svc = AsyncMock()
evidence_repo = AsyncMock()
evidence_repo.journal_highlights_for_task.return_value = []
@@ -105,13 +106,13 @@ async def test_evidence_uses_full_pr_diff_not_head_minus_one() -> None:
)
await ca.evidence(agent_id=agent_id, task_id=task_id)
git_svc.diff.assert_awaited_once()
call_kwargs = git_svc.diff.await_args.kwargs
git_svc.diff_and_files.assert_awaited_once()
call_kwargs = git_svc.diff_and_files.await_args.kwargs
# Pre-fix bug: kwargs['base'] would be 'HEAD~1' for any multi-commit
# branch. Post-fix: base is omitted (or explicitly None).
base = call_kwargs.get("base")
assert base in (None, ""), (
f"git.diff must use full-PR diff (base=None), got base={base!r}"
f"diff_and_files must use full-PR diff (base=None), got base={base!r}"
)
assert call_kwargs.get("branch_name") == "feature/backend/abc12345--def67890"
@@ -125,8 +126,7 @@ async def test_evidence_populates_journal_highlights() -> None:
task_svc = AsyncMock()
task_svc.get.return_value = _task_with_pr(task_id, commits=["abc"])
git_svc = AsyncMock()
git_svc.diff.return_value = ""
git_svc.list_changed_files.return_value = []
git_svc.diff_and_files.return_value = ("", [])
workspace_svc = AsyncMock()
evidence_repo = AsyncMock()
highlights = [
@@ -179,5 +179,4 @@ async def test_evidence_no_branch_skips_git_calls() -> None:
assert body["error"] is None
assert body["evidence"]["files_changed"] == []
assert body["evidence"]["pr_diff_summary"] == ""
git_svc.diff.assert_not_awaited()
git_svc.list_changed_files.assert_not_awaited()
git_svc.diff_and_files.assert_not_awaited()
@@ -107,8 +107,7 @@ async def test_claim_review_evidence_carries_prior_findings(
task_svc.list_paused_for_agent.return_value = []
task_svc.qa_claim.return_value = t_claimed
git_svc = AsyncMock()
git_svc.diff.return_value = ""
git_svc.list_changed_files.return_value = []
git_svc.diff_and_files.return_value = ("", [])
deps = _make_deps(task=task_svc, git=git_svc)
c = Choreographer(deps)