fix(agent): propagate resource collection failures (#40697)

This commit is contained in:
盐粒 Yanli
2026-08-13 06:16:12 +00:00
committed by GitHub
parent 6e80f42f38
commit 8df220c2b3
7 changed files with 186 additions and 115 deletions
@@ -129,27 +129,23 @@ def test_build_apply_fails_fast_without_source_binding() -> None:
)
def test_home_snapshot_collection_database_failure_is_best_effort(monkeypatch: pytest.MonkeyPatch) -> None:
def test_home_snapshot_collection_database_failure_propagates(monkeypatch: pytest.MonkeyPatch) -> None:
context = MagicMock()
session = context.__enter__.return_value
session.scalar.side_effect = RuntimeError("database unavailable")
log_exception = MagicMock()
error = RuntimeError("database unavailable")
session.scalar.side_effect = error
monkeypatch.setattr(
"services.agent.home_snapshot_service.session_factory.create_session",
lambda: context,
)
monkeypatch.setattr("services.agent.home_snapshot_service.logger.exception", log_exception)
AgentHomeSnapshotService.collect_retired_home_snapshot(
tenant_id="tenant-1",
home_snapshot_id="home-1",
)
with pytest.raises(RuntimeError) as exc_info:
AgentHomeSnapshotService.collect_retired_home_snapshot(
tenant_id="tenant-1",
home_snapshot_id="home-1",
)
session.scalar.assert_called_once()
log_exception.assert_called_once_with(
"Failed to collect retired Agent Home Snapshot",
extra={"tenant_id": "tenant-1", "home_snapshot_id": "home-1"},
)
assert exc_info.value is error
@pytest.mark.parametrize(
@@ -157,7 +153,7 @@ def test_home_snapshot_collection_database_failure_is_best_effort(monkeypatch: p
[(AgentHomeSnapshot, AgentConfigDraft, AgentConfigSnapshot)],
indirect=True,
)
def test_home_snapshot_collection_final_delete_failure_is_best_effort(
def test_home_snapshot_collection_backend_failure_propagates_and_preserves_retired_snapshot(
monkeypatch: pytest.MonkeyPatch, sqlite_session: Session
) -> None:
snapshot = AgentHomeSnapshot(
@@ -169,7 +165,45 @@ def test_home_snapshot_collection_final_delete_failure_is_best_effort(
)
sqlite_session.add(snapshot)
sqlite_session.commit()
commit = MagicMock(side_effect=RuntimeError("database unavailable"))
error = RuntimeError("Agent backend unavailable")
delete = MagicMock(side_effect=error)
monkeypatch.setattr(
"services.agent.home_snapshot_service.session_factory.create_session",
lambda: nullcontext(sqlite_session),
)
monkeypatch.setattr(AgentHomeSnapshotService, "delete", delete)
with pytest.raises(RuntimeError) as exc_info:
AgentHomeSnapshotService.collect_retired_home_snapshot(
tenant_id="tenant-1",
home_snapshot_id=snapshot.id,
)
assert exc_info.value is error
stored_snapshot = sqlite_session.get(AgentHomeSnapshot, snapshot.id)
assert stored_snapshot is not None
assert stored_snapshot.status is AgentWorkingResourceStatus.RETIRED
@pytest.mark.parametrize(
"sqlite_session",
[(AgentHomeSnapshot, AgentConfigDraft, AgentConfigSnapshot)],
indirect=True,
)
def test_home_snapshot_collection_final_delete_failure_propagates(
monkeypatch: pytest.MonkeyPatch, sqlite_session: Session
) -> None:
snapshot = AgentHomeSnapshot(
id="home-1",
tenant_id="tenant-1",
agent_id="agent-1",
snapshot_ref="snapshot-ref-1",
status=AgentWorkingResourceStatus.RETIRED,
)
sqlite_session.add(snapshot)
sqlite_session.commit()
error = RuntimeError("database unavailable")
commit = MagicMock(side_effect=error)
delete = MagicMock()
monkeypatch.setattr(
"services.agent.home_snapshot_service.session_factory.create_session",
@@ -178,9 +212,11 @@ def test_home_snapshot_collection_final_delete_failure_is_best_effort(
monkeypatch.setattr(sqlite_session, "commit", commit)
monkeypatch.setattr(AgentHomeSnapshotService, "delete", delete)
AgentHomeSnapshotService.collect_retired_home_snapshot(
tenant_id="tenant-1",
home_snapshot_id="home-1",
)
with pytest.raises(RuntimeError) as exc_info:
AgentHomeSnapshotService.collect_retired_home_snapshot(
tenant_id="tenant-1",
home_snapshot_id="home-1",
)
assert exc_info.value is error
delete.assert_called_once_with(snapshot_ref="snapshot-ref-1")
@@ -431,45 +431,91 @@ def test_collect_workspace_destroys_workspace_then_remaining_bindings(
assert sqlite_session.get(AgentWorkspaceBinding, remaining.id) is None
def test_binding_collection_database_failure_is_best_effort(monkeypatch: pytest.MonkeyPatch) -> None:
def test_binding_collection_database_failure_propagates(monkeypatch: pytest.MonkeyPatch) -> None:
context = MagicMock()
session = context.__enter__.return_value
session.scalar.side_effect = RuntimeError("database unavailable")
log_exception = MagicMock()
error = RuntimeError("database unavailable")
session.scalar.side_effect = error
monkeypatch.setattr("services.agent.workspace_service.session_factory.create_session", lambda: context)
monkeypatch.setattr("services.agent.workspace_service.logger.exception", log_exception)
AgentWorkspaceService.collect_retired_binding(tenant_id="tenant-1", binding_id="binding-1")
with pytest.raises(RuntimeError) as exc_info:
AgentWorkspaceService.collect_retired_binding(tenant_id="tenant-1", binding_id="binding-1")
session.scalar.assert_called_once()
log_exception.assert_called_once_with(
"Failed to collect retired Agent Workspace Binding",
extra={"tenant_id": "tenant-1", "binding_id": "binding-1"},
)
assert exc_info.value is error
@pytest.mark.parametrize("sqlite_session", [(AgentWorkspace, AgentWorkspaceBinding)], indirect=True)
def test_workspace_collection_final_delete_failure_is_best_effort(
def test_binding_collection_backend_failure_propagates_and_preserves_retired_binding(
monkeypatch: pytest.MonkeyPatch, sqlite_session: Session
) -> None:
workspace = _workspace()
binding = _binding(status=AgentWorkingResourceStatus.RETIRED)
sqlite_session.add_all([workspace, binding])
sqlite_session.commit()
error = RuntimeError("Agent backend unavailable")
client = MagicMock()
client.destroy_execution_binding_sync.side_effect = error
monkeypatch.setattr(
"services.agent.workspace_service.session_factory.create_session", lambda: nullcontext(sqlite_session)
)
monkeypatch.setattr(AgentWorkspaceService, "_client", lambda: nullcontext(client))
with pytest.raises(RuntimeError) as exc_info:
AgentWorkspaceService.collect_retired_binding(tenant_id="tenant-1", binding_id=binding.id)
assert exc_info.value is error
stored_binding = sqlite_session.get(AgentWorkspaceBinding, binding.id)
assert stored_binding is not None
assert stored_binding.status is AgentWorkingResourceStatus.RETIRED
@pytest.mark.parametrize("sqlite_session", [(AgentWorkspace, AgentWorkspaceBinding)], indirect=True)
def test_workspace_collection_backend_failure_propagates_and_preserves_retired_resources(
monkeypatch: pytest.MonkeyPatch, sqlite_session: Session
) -> None:
workspace = _workspace(status=AgentWorkingResourceStatus.RETIRED)
anchor = _binding(status=AgentWorkingResourceStatus.RETIRED)
sqlite_session.add_all([workspace, anchor])
sqlite_session.commit()
commit = MagicMock(side_effect=RuntimeError("database unavailable"))
error = RuntimeError("Agent backend unavailable")
client = MagicMock()
client.destroy_execution_binding_sync.side_effect = error
monkeypatch.setattr(
"services.agent.workspace_service.session_factory.create_session", lambda: nullcontext(sqlite_session)
)
monkeypatch.setattr(AgentWorkspaceService, "_client", lambda: nullcontext(client))
with pytest.raises(RuntimeError) as exc_info:
AgentWorkspaceService.collect_retired_workspace(tenant_id="tenant-1", workspace_id=workspace.id)
assert exc_info.value is error
stored_workspace = sqlite_session.get(AgentWorkspace, workspace.id)
stored_anchor = sqlite_session.get(AgentWorkspaceBinding, anchor.id)
assert stored_workspace is not None
assert stored_workspace.status is AgentWorkingResourceStatus.RETIRED
assert stored_anchor is not None
assert stored_anchor.status is AgentWorkingResourceStatus.RETIRED
@pytest.mark.parametrize("sqlite_session", [(AgentWorkspace, AgentWorkspaceBinding)], indirect=True)
def test_workspace_collection_final_delete_failure_propagates(
monkeypatch: pytest.MonkeyPatch, sqlite_session: Session
) -> None:
workspace = _workspace(status=AgentWorkingResourceStatus.RETIRED)
anchor = _binding(status=AgentWorkingResourceStatus.RETIRED)
sqlite_session.add_all([workspace, anchor])
sqlite_session.commit()
error = RuntimeError("database unavailable")
commit = MagicMock(side_effect=error)
client = MagicMock()
log_exception = MagicMock()
monkeypatch.setattr(
"services.agent.workspace_service.session_factory.create_session", lambda: nullcontext(sqlite_session)
)
monkeypatch.setattr(sqlite_session, "commit", commit)
monkeypatch.setattr(AgentWorkspaceService, "_client", lambda: nullcontext(client))
monkeypatch.setattr("services.agent.workspace_service.logger.exception", log_exception)
AgentWorkspaceService.collect_retired_workspace(tenant_id="tenant-1", workspace_id="workspace-1")
with pytest.raises(RuntimeError) as exc_info:
AgentWorkspaceService.collect_retired_workspace(tenant_id="tenant-1", workspace_id="workspace-1")
assert exc_info.value is error
client.destroy_execution_binding_sync.assert_called_once()
log_exception.assert_called_once_with(
"Failed to collect retired Agent Workspace",
extra={"tenant_id": "tenant-1", "workspace_id": "workspace-1"},
)
@@ -39,14 +39,13 @@ def test_enqueue_deduplicates_ids_and_skips_empty_input(monkeypatch: pytest.Monk
)
def test_collection_continues_in_workspace_binding_snapshot_order(monkeypatch: pytest.MonkeyPatch) -> None:
def test_collection_runs_in_workspace_binding_snapshot_order(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[str] = []
def collect_workspace(**_kwargs: object) -> None:
calls.append("workspace")
raise RuntimeError("workspace failed")
monkeypatch.setattr(AgentWorkspaceService, "collect_retired_workspace", collect_workspace)
monkeypatch.setattr(
AgentWorkspaceService,
"collect_retired_workspace",
lambda **_kwargs: calls.append("workspace"),
)
monkeypatch.setattr(
AgentWorkspaceService,
"collect_retired_binding",
@@ -68,6 +67,48 @@ def test_collection_continues_in_workspace_binding_snapshot_order(monkeypatch: p
assert calls == ["workspace", "binding", "home"]
def test_collection_failure_propagates_and_stops_task(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[str] = []
error = RuntimeError("workspace failed")
log_exception = MagicMock()
def collect_workspace(**_kwargs: object) -> None:
calls.append("workspace")
raise error
monkeypatch.setattr(AgentWorkspaceService, "collect_retired_workspace", collect_workspace)
monkeypatch.setattr(
AgentWorkspaceService,
"collect_retired_binding",
lambda **_kwargs: calls.append("binding"),
)
monkeypatch.setattr(
AgentHomeSnapshotService,
"collect_retired_home_snapshot",
lambda **_kwargs: calls.append("home"),
)
monkeypatch.setattr("tasks.collect_agent_resources_task.logger.exception", log_exception)
with pytest.raises(RuntimeError) as exc_info:
collect_agent_resources.run(
tenant_id="tenant-1",
workspace_ids=["workspace-1"],
binding_ids=["binding-1"],
home_snapshot_ids=["home-1"],
)
assert exc_info.value is error
assert calls == ["workspace"]
log_exception.assert_called_once_with(
"Failed to collect retired Agent resource",
extra={
"tenant_id": "tenant-1",
"resource_type": "workspace",
"resource_id": "workspace-1",
},
)
def test_enqueue_failure_is_best_effort(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(
collect_agent_resources,