diff --git a/agent/middleware/workflow_push_guard.py b/agent/middleware/workflow_push_guard.py index 40abfe50..67b5069f 100644 --- a/agent/middleware/workflow_push_guard.py +++ b/agent/middleware/workflow_push_guard.py @@ -340,13 +340,7 @@ def _workflow_change_for_push(backend: Any, parsed: ParsedGitPush) -> WorkflowPu if not diff.ok or not diff.output: return None - content_parts: list[str] = [] - for path in files: - blob = _run_git(backend, root, f"show {shlex.quote(head)}:{shlex.quote(path)}") - if not blob.ok: - return None - content_parts.append(blob.output) - content_hash = _fingerprint({"contents": content_parts}) + content_hash = _fingerprint({"diff": diff.output}) remote = _run_git(backend, root, "config --get remote.origin.url") repo = _normalize_remote(_first_line(remote.output)) if remote.ok else "" diff --git a/tests/test_workflow_push_guard.py b/tests/test_workflow_push_guard.py index c1c44211..63362ec8 100644 --- a/tests/test_workflow_push_guard.py +++ b/tests/test_workflow_push_guard.py @@ -407,6 +407,48 @@ async def test_rebased_workflow_push_keeps_fingerprint_for_same_diff( assert payload3["fingerprint"] != payload["fingerprint"] +async def test_deleted_workflow_file_requires_approval( + monkeypatch: pytest.MonkeyPatch, +) -> None: + backend = _Backend( + workflow_files=".github/workflows/ci.yml", + diff_output="diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml\ndeleted file\n", + ) + guard.SANDBOX_BACKENDS["thread-1"] = backend + + async def fake_approved(thread_id: str, fingerprint: str) -> bool: + return False + + async def fake_find_approval(*args: Any, **kwargs: Any) -> dict[str, Any] | None: + return None + + async def fake_pending(thread_id: str, **kwargs: Any) -> tuple[dict[str, Any], bool]: + return {"fingerprint": kwargs["fingerprint"], "status": "pending", "notified": False}, True + + async def fake_post(*args: Any, **kwargs: Any) -> tuple[str, None]: + return "1700000000.000300", None + + async def fake_notified(*args: Any, **kwargs: Any) -> None: + return None + + monkeypatch.setattr(guard, "workflow_push_approved", fake_approved) + monkeypatch.setattr(guard, "find_workflow_push_approval", fake_find_approval) + monkeypatch.setattr(guard, "ensure_workflow_push_pending", fake_pending) + monkeypatch.setattr(guard, "post_slack_thread_reply_with_ts", fake_post) + monkeypatch.setattr(guard, "mark_workflow_push_notified", fake_notified) + + async def handler(_request: Any) -> ToolMessage: + return ToolMessage(content="pushed", tool_call_id="call-1") + + result = await guard.WorkflowPushGuardMiddleware().awrap_tool_call(_Request(), handler) + + assert isinstance(result, ToolMessage) + assert result.status == "error" + payload = json.loads(str(result.content)) + assert payload["workflow_approval_status"] == "approval_required" + assert payload["files"] == [".github/workflows/ci.yml"] + + async def test_approved_workflow_push_aborts_when_elevation_fails( monkeypatch: pytest.MonkeyPatch, ) -> None: