mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* [62845be1] test(gateway): prove claim_review/evidence timeout fix — dedup, timeout guards Add the test coverage the acceptance criteria require but the existing implementation lacked: a git.py-level test proving diff_and_files resolves the shared workspace/token/head/base state exactly once, and bounded-timeout tests exercising the actual gateway_timeout trip for evidence(), claim_review, and the /api/git/diff route. Also fixes pre-existing ruff-format drift and a mypy no-any-return in the same fix cluster (content_actions.py, git.py, qa.py, routes/git.py) so make gate passes clean. * [62845be1] fix(gateway): add missing evidence_assembly_timeout_seconds/conventions_validator_timeout_seconds settings and Envelope.gateway_timeout classmethod The claim_review/evidence()/roboco_git_diff bounded-timeout fix referenced settings.evidence_assembly_timeout_seconds, settings.conventions_validator_timeout_seconds, and Envelope.gateway_timeout() but none of the three were ever defined, so every code path that hit the timeout guard raised AttributeError instead of returning the structured envelope. Added both Settings fields (90s/45s defaults, well under flow_verb_timeout_seconds) and the Envelope.gateway_timeout classmethod matching the wire shape api/middleware.py's outer 504 already uses. * [62845be1] docs(services): document claim_review/evidence timeout fix — dedup, parallelization, bounded timeouts --------- Co-authored-by: Backend Developer 1 <be-dev-1@roboco.tech> Co-authored-by: Backend Documenter <be-doc@roboco.tech>
102 lines
4.1 KiB
Python
102 lines
4.1 KiB
Python
"""Task #62845be1: ``diff_and_files`` resolves shared state ONCE.
|
|
|
|
``diff()`` and ``list_changed_files()`` each independently re-resolve the
|
|
workspace, auth token, head ref, and diff base before running their own
|
|
``git diff`` subprocess — duplicated work when a caller (evidence assembly)
|
|
needs both. ``diff_and_files`` resolves that shared state a single time,
|
|
then runs the two ``git diff`` subprocesses concurrently.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from roboco.services.git import GitService
|
|
|
|
_BR = "feature/backend/root1234--cellpm56--dev78901"
|
|
|
|
|
|
def _git_service() -> Any:
|
|
# A real constructor (not __new__) so ``self.log`` is bound —
|
|
# ``diff_and_files`` logs its own resolve/diff timing.
|
|
return GitService(MagicMock())
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_and_files_resolves_shared_state_once() -> None:
|
|
svc = _git_service()
|
|
svc._workspace_for_branch = AsyncMock(return_value=Path("/tmp/qa-ws"))
|
|
svc._token_for_branch = AsyncMock(return_value="tok")
|
|
svc._resolve_head_ref = AsyncMock(return_value=f"origin/{_BR}")
|
|
svc._resolve_diff_base = AsyncMock(return_value="origin/master")
|
|
captured: list[list[str]] = []
|
|
|
|
async def fake_run(_ws: Any, args: list[str], **_kw: Any) -> Any:
|
|
captured.append(args)
|
|
if args[:2] == ["diff", "--name-only"]:
|
|
return type(
|
|
"R", (), {"returncode": 0, "stdout": "README.md\nsrc/app.py\n"}
|
|
)()
|
|
return type("R", (), {"returncode": 0, "stdout": "diff body"})()
|
|
|
|
with patch.object(svc, "_run_git", new=fake_run):
|
|
diff, files = await svc.diff_and_files(branch_name=_BR)
|
|
|
|
assert diff == "diff body"
|
|
assert files == ["README.md", "src/app.py"]
|
|
# The shared resolution work runs exactly ONCE, not once per sub-call.
|
|
svc._workspace_for_branch.assert_awaited_once()
|
|
svc._token_for_branch.assert_awaited_once()
|
|
svc._resolve_head_ref.assert_awaited_once()
|
|
svc._resolve_diff_base.assert_awaited_once()
|
|
# Both the `diff` and `diff --name-only` subprocesses still ran, off the
|
|
# same resolved base...head pair.
|
|
assert any(c == ["diff", f"origin/master...origin/{_BR}"] for c in captured)
|
|
assert any(
|
|
c == ["diff", "--name-only", f"origin/master...origin/{_BR}"] for c in captured
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_and_files_honors_explicit_base_and_preferred_parent() -> None:
|
|
svc = _git_service()
|
|
svc._workspace_for_branch = AsyncMock(return_value=Path("/tmp/dev-ws"))
|
|
svc._token_for_branch = AsyncMock(return_value="tok")
|
|
svc._resolve_head_ref = AsyncMock(return_value=_BR)
|
|
svc._resolve_diff_base = AsyncMock(return_value="origin/master")
|
|
|
|
async def fake_run(_ws: Any, _args: list[str], **_kw: Any) -> Any:
|
|
return type("R", (), {"returncode": 0, "stdout": ""})()
|
|
|
|
with patch.object(svc, "_run_git", new=fake_run):
|
|
await svc.diff_and_files(
|
|
branch_name=_BR, base="HEAD~1", preferred_parent="feature/backend/other"
|
|
)
|
|
|
|
# An explicit base skips _resolve_diff_base entirely.
|
|
svc._resolve_diff_base.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_diff_and_files_matches_diff_and_list_changed_files_output() -> None:
|
|
"""Combined accessor returns the same data the two separate calls would."""
|
|
svc = _git_service()
|
|
svc._workspace_for_branch = AsyncMock(return_value=Path("/tmp/ws"))
|
|
svc._token_for_branch = AsyncMock(return_value="tok")
|
|
svc._resolve_head_ref = AsyncMock(return_value=f"origin/{_BR}")
|
|
svc._resolve_diff_base = AsyncMock(return_value="origin/master")
|
|
|
|
async def fake_run(_ws: Any, args: list[str], **_kw: Any) -> Any:
|
|
if args[:2] == ["diff", "--name-only"]:
|
|
return type("R", (), {"returncode": 0, "stdout": "a.py\nb.py\n"})()
|
|
return type("R", (), {"returncode": 0, "stdout": "full diff body"})()
|
|
|
|
with patch.object(svc, "_run_git", new=fake_run):
|
|
diff, files = await svc.diff_and_files(branch_name=_BR)
|
|
|
|
assert diff == "full diff body"
|
|
assert files == ["a.py", "b.py"]
|