658 lines
22 KiB
Python
658 lines
22 KiB
Python
from __future__ import annotations
|
|
|
|
# ruff: noqa: E402
|
|
import os
|
|
from hashlib import sha256
|
|
from types import SimpleNamespace
|
|
from uuid import UUID
|
|
|
|
os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
|
|
|
|
import pytest
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
from pydantic import ValidationError
|
|
|
|
from src.api.v1 import agents as agents_api
|
|
from src.core.config import settings
|
|
from src.models.agent99_completion import Agent99CompletionCallbackRequest
|
|
from src.services import agent99_completion_callback as callback_service
|
|
from src.services.agent99_controlled_dispatch_ledger import (
|
|
build_agent99_dispatch_identity,
|
|
)
|
|
|
|
|
|
def payload() -> dict:
|
|
return {
|
|
"schema_version": "agent99_completion_callback_v1",
|
|
"callback_id": "agent99-sre-alert-20260711-resolved",
|
|
"project_id": "awoooi",
|
|
"run_id": "sre-alert-20260711",
|
|
"trace_id": "host_recovery:cold-start-gate:no-host",
|
|
"work_item_id": "agent99-incident:INC-20260711-001",
|
|
"alert_id": "INC-20260711-001",
|
|
"correlation_key": "host_recovery:cold-start-gate:no-host",
|
|
"source": "agent99-sre-alert-inbox",
|
|
"mode": "Recover",
|
|
"outcome_state": "resolved",
|
|
"controlled_apply": True,
|
|
"transport_ok": True,
|
|
"verifier_name": "recover_post_condition_v1",
|
|
"verifier_passed": True,
|
|
"source_event_resolved": True,
|
|
"source_event_resolution_policy": "mode_verifier_can_resolve",
|
|
"duration_seconds": 9.4,
|
|
"evidence_ref": "agent99-Recover-20260711-202342.json",
|
|
"alert_kind": "host_recovery",
|
|
"alert_service": "cold-start-gate",
|
|
"alert_host": "192.168.0.110",
|
|
"telegram": {
|
|
"sent": True,
|
|
"suppressed": False,
|
|
"message_format": "incident_card_zh_tw_v4",
|
|
"visual_sent": True,
|
|
"visual_file": "agent99-card-recovery.png",
|
|
},
|
|
"problem": {"occurrences": 2, "verified_resolved": 2},
|
|
"occurred_at": "2026-07-11T20:23:51+08:00",
|
|
}
|
|
|
|
|
|
def app_client() -> TestClient:
|
|
app = FastAPI()
|
|
app.include_router(agents_api.router, prefix="/api/v1")
|
|
return TestClient(app)
|
|
|
|
|
|
def test_completion_callback_fails_closed_without_server_token(monkeypatch) -> None:
|
|
monkeypatch.setattr(settings, "AGENT99_SRE_ALERT_RELAY_TOKEN", "")
|
|
|
|
response = app_client().post(
|
|
"/api/v1/agents/agent99/completion-callback",
|
|
json=payload(),
|
|
)
|
|
|
|
assert response.status_code == 503
|
|
assert response.json()["detail"] == "agent99_completion_auth_not_configured"
|
|
|
|
|
|
def test_completion_payload_rejects_secret_fields_and_full_paths() -> None:
|
|
with pytest.raises(ValidationError):
|
|
Agent99CompletionCallbackRequest.model_validate(
|
|
{**payload(), "bot_token": "must-not-enter-callback"}
|
|
)
|
|
with pytest.raises(ValidationError):
|
|
Agent99CompletionCallbackRequest.model_validate(
|
|
{**payload(), "evidence_ref": r"C:\Wooo\Agent99\evidence\raw.json"}
|
|
)
|
|
|
|
|
|
def test_completion_operation_incident_id_preserves_canonical_ids() -> None:
|
|
assert callback_service._operation_incident_id(None) is None
|
|
assert (
|
|
callback_service._operation_incident_id("INC-20260714-A1B2C3")
|
|
== "INC-20260714-A1B2C3"
|
|
)
|
|
|
|
|
|
def test_completion_operation_incident_id_compacts_external_ids() -> None:
|
|
external_id = "awoooi-agent99-ae36a454-c9eb-5f2d-9e35-be852b3eac9b"
|
|
expected = f"INC-AG99-{sha256(external_id.encode('utf-8')).hexdigest().upper()[:21]}"
|
|
|
|
compact = callback_service._operation_incident_id(external_id)
|
|
|
|
assert compact == expected
|
|
assert len(compact) == 30
|
|
|
|
|
|
def test_completion_callback_rejects_wrong_token(monkeypatch) -> None:
|
|
monkeypatch.setattr(settings, "AGENT99_SRE_ALERT_RELAY_TOKEN", "expected")
|
|
|
|
response = app_client().post(
|
|
"/api/v1/agents/agent99/completion-callback",
|
|
headers={"X-Agent99-Completion-Token": "wrong"},
|
|
json=payload(),
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
assert response.json()["detail"] == "agent99_completion_auth_failed"
|
|
|
|
|
|
def test_completion_callback_requires_durable_readback(monkeypatch) -> None:
|
|
monkeypatch.setattr(settings, "AGENT99_SRE_ALERT_RELAY_TOKEN", "expected")
|
|
|
|
async def fake_record(_payload):
|
|
return {"ok": False, "durable_readback": False}
|
|
|
|
monkeypatch.setattr(agents_api, "record_agent99_completion_callback", fake_record)
|
|
response = app_client().post(
|
|
"/api/v1/agents/agent99/completion-callback",
|
|
headers={"X-Agent99-Completion-Token": "expected"},
|
|
json=payload(),
|
|
)
|
|
|
|
assert response.status_code == 503
|
|
assert response.json()["detail"] == "agent99_completion_durable_readback_failed"
|
|
|
|
|
|
def test_completion_callback_keeps_unverified_closure_pending(monkeypatch) -> None:
|
|
monkeypatch.setattr(settings, "AGENT99_SRE_ALERT_RELAY_TOKEN", "expected")
|
|
|
|
async def fake_record(_request):
|
|
return {
|
|
"ok": False,
|
|
"durable_readback": True,
|
|
"status": "closure_authority_pending",
|
|
}
|
|
|
|
monkeypatch.setattr(agents_api, "record_agent99_completion_callback", fake_record)
|
|
response = app_client().post(
|
|
"/api/v1/agents/agent99/completion-callback",
|
|
headers={"X-Agent99-Completion-Token": "expected"},
|
|
json=payload(),
|
|
)
|
|
|
|
assert response.status_code == 503
|
|
assert response.json()["detail"] == "closure_authority_pending"
|
|
|
|
|
|
def test_completion_callback_returns_same_trace_receipt(monkeypatch) -> None:
|
|
monkeypatch.setattr(settings, "AGENT99_SRE_ALERT_RELAY_TOKEN", "expected")
|
|
|
|
async def fake_record(request):
|
|
return {
|
|
"schema_version": "agent99_completion_callback_receipt_v1",
|
|
"ok": True,
|
|
"durable_readback": True,
|
|
"callback_id": request.callback_id,
|
|
"run_id": request.run_id,
|
|
"trace_id": request.trace_id,
|
|
"awooop_run_id": "5e5b0080-e8c9-53a7-872b-0fc1ce80f11f",
|
|
}
|
|
|
|
monkeypatch.setattr(agents_api, "record_agent99_completion_callback", fake_record)
|
|
response = app_client().post(
|
|
"/api/v1/agents/agent99/completion-callback",
|
|
headers={"X-Agent99-Completion-Token": "expected"},
|
|
json=payload(),
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["durable_readback"] is True
|
|
assert response.json()["callback_id"] == payload()["callback_id"]
|
|
assert response.json()["trace_id"] == payload()["trace_id"]
|
|
|
|
|
|
class FakeOperationRepository:
|
|
def __init__(self) -> None:
|
|
self.events: list[tuple[str, dict]] = []
|
|
|
|
async def append(self, event_type: str, **kwargs):
|
|
self.events.append((event_type, kwargs))
|
|
return SimpleNamespace(id=f"operation-{len(self.events)}")
|
|
|
|
|
|
def authorize_terminal_callback(monkeypatch) -> None:
|
|
async def fake_authority(_payload):
|
|
return {
|
|
"required": True,
|
|
"verified": True,
|
|
"blocker": "none",
|
|
"typed_domain": "control_plane_recovery",
|
|
"route_id": "agent99:host_recovery:Recover",
|
|
}
|
|
|
|
monkeypatch.setattr(
|
|
callback_service,
|
|
"_load_callback_closure_authority",
|
|
fake_authority,
|
|
)
|
|
|
|
|
|
def typed_dispatch_callback_contract() -> tuple[
|
|
Agent99CompletionCallbackRequest,
|
|
dict,
|
|
]:
|
|
identity = build_agent99_dispatch_identity(
|
|
project_id="awoooi",
|
|
incident_id="INC-20260711-001",
|
|
source_fingerprint="agent99-callback-source",
|
|
route_id="agent99_cold_start_recovery",
|
|
work_item_id="agent99-incident:INC-20260711-001",
|
|
)
|
|
verifier_name = "recover_post_condition_v1"
|
|
request = Agent99CompletionCallbackRequest.model_validate({
|
|
**payload(),
|
|
"run_id": str(identity.run_id),
|
|
"trace_id": identity.trace_id,
|
|
"work_item_id": identity.work_item_id,
|
|
"verifier_name": verifier_name,
|
|
})
|
|
receipt = {
|
|
"schema_version": "agent99_controlled_dispatch_receipt_v1",
|
|
"status": "verifier_passed_learning_writeback_pending",
|
|
"identity": identity.public_dict(),
|
|
"run_id": str(identity.run_id),
|
|
"trace_id": identity.trace_id,
|
|
"run_state": "waiting_tool",
|
|
"dispatch_promoted": True,
|
|
"controlled_apply_authorized": True,
|
|
"post_verifier_passed": True,
|
|
"dispatch_scope": {
|
|
"schema_version": "agent99_dispatch_scope_v1",
|
|
"kind": "host_recovery",
|
|
"suggested_mode": "Recover",
|
|
"target_resource": "cold-start-gate",
|
|
"controlled_apply_requested": True,
|
|
"canonical_asset_id": "windows-vmware:host_99",
|
|
"typed_domain": "control_plane_recovery",
|
|
"executor": "Agent99",
|
|
"verifier": verifier_name,
|
|
"route_id": identity.route_id,
|
|
},
|
|
"verifier": {
|
|
"schema_version": "agent99_independent_verifier_receipt_v1",
|
|
"status": "success",
|
|
"run_id": str(identity.run_id),
|
|
"trace_id": identity.trace_id,
|
|
"work_item_id": identity.work_item_id,
|
|
"outcome_state": "resolved",
|
|
"transport_ok": True,
|
|
"verifier_passed": True,
|
|
"source_event_resolved": True,
|
|
},
|
|
}
|
|
return request, receipt
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_callback_closure_authority_requires_exact_typed_ledger(
|
|
monkeypatch,
|
|
) -> None:
|
|
request, dispatch_receipt = typed_dispatch_callback_contract()
|
|
|
|
async def fake_read(**_kwargs):
|
|
return dispatch_receipt
|
|
|
|
monkeypatch.setattr(
|
|
callback_service,
|
|
"read_agent99_dispatch_receipt_for_run",
|
|
fake_read,
|
|
)
|
|
|
|
authority = await callback_service._load_callback_closure_authority(request)
|
|
|
|
assert authority == {
|
|
"required": True,
|
|
"verified": True,
|
|
"blocker": "none",
|
|
"typed_domain": "control_plane_recovery",
|
|
"route_id": "agent99_cold_start_recovery",
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_callback_closure_authority_rejects_cross_domain_scope(
|
|
monkeypatch,
|
|
) -> None:
|
|
request, dispatch_receipt = typed_dispatch_callback_contract()
|
|
dispatch_receipt["dispatch_scope"]["typed_domain"] = "docker_compose"
|
|
|
|
async def fake_read(**_kwargs):
|
|
return dispatch_receipt
|
|
|
|
monkeypatch.setattr(
|
|
callback_service,
|
|
"read_agent99_dispatch_receipt_for_run",
|
|
fake_read,
|
|
)
|
|
|
|
authority = await callback_service._load_callback_closure_authority(request)
|
|
|
|
assert authority["verified"] is False
|
|
assert authority["blocker"] == "typed_dispatch_scope_mismatch"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
"field",
|
|
["transport_ok", "verifier_passed", "source_event_resolved"],
|
|
)
|
|
async def test_callback_closure_authority_rejects_false_verifier_projection(
|
|
monkeypatch,
|
|
field: str,
|
|
) -> None:
|
|
request, dispatch_receipt = typed_dispatch_callback_contract()
|
|
request = request.model_copy(update={field: False})
|
|
|
|
async def fake_read(**_kwargs):
|
|
return dispatch_receipt
|
|
|
|
monkeypatch.setattr(
|
|
callback_service,
|
|
"read_agent99_dispatch_receipt_for_run",
|
|
fake_read,
|
|
)
|
|
|
|
authority = await callback_service._load_callback_closure_authority(request)
|
|
|
|
assert authority["verified"] is False
|
|
assert authority["blocker"] == "callback_verifier_projection_mismatch"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_completion_service_does_not_resolve_from_caller_booleans(
|
|
monkeypatch,
|
|
) -> None:
|
|
request = Agent99CompletionCallbackRequest.model_validate(payload())
|
|
event_id = UUID("3c9e8ee7-fb21-5420-afae-ef928f75a58c")
|
|
repository = FakeOperationRepository()
|
|
readbacks = iter([
|
|
None,
|
|
{
|
|
"event_id": str(event_id),
|
|
"run_id": "5e5b0080-e8c9-53a7-872b-0fc1ce80f11f",
|
|
"operation_count": 1,
|
|
"execution_count": 1,
|
|
"resolved_count": 0,
|
|
},
|
|
])
|
|
|
|
async def fake_authority(_payload):
|
|
return {
|
|
"required": True,
|
|
"verified": False,
|
|
"blocker": "dispatch_run_not_found",
|
|
"typed_domain": "",
|
|
"route_id": "",
|
|
}
|
|
|
|
async def fake_readback(**_kwargs):
|
|
return next(readbacks)
|
|
|
|
async def fake_record_event(**kwargs):
|
|
assert kwargs["stage"] == "verifying"
|
|
assert kwargs["severity"] == "warning"
|
|
return event_id
|
|
|
|
monkeypatch.setattr(
|
|
callback_service,
|
|
"_load_callback_closure_authority",
|
|
fake_authority,
|
|
)
|
|
monkeypatch.setattr(callback_service, "_load_callback_readback", fake_readback)
|
|
monkeypatch.setattr(callback_service, "record_external_alert_event", fake_record_event)
|
|
monkeypatch.setattr(
|
|
callback_service,
|
|
"get_alert_operation_log_repository",
|
|
lambda: repository,
|
|
)
|
|
|
|
receipt = await callback_service.record_agent99_completion_callback(request)
|
|
|
|
assert receipt["ok"] is False
|
|
assert receipt["durable_readback"] is True
|
|
assert receipt["outcome_state"] == "verifying"
|
|
assert receipt["closure_allowed"] is False
|
|
assert receipt["closure_blocker"] == "dispatch_run_not_found"
|
|
assert [item[0] for item in repository.events] == ["EXECUTION_COMPLETED"]
|
|
assert repository.events[0][1]["success"] is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_completion_service_retry_adds_only_resolved_receipt(
|
|
monkeypatch,
|
|
) -> None:
|
|
request = Agent99CompletionCallbackRequest.model_validate(payload())
|
|
verifying_event_id = UUID("3c9e8ee7-fb21-5420-afae-ef928f75a58c")
|
|
resolved_event_id = UUID("5e5b0080-e8c9-53a7-872b-0fc1ce80f11f")
|
|
awooop_run_id = "8b877ed3-037f-5463-954b-f4b77a77d786"
|
|
repository = FakeOperationRepository()
|
|
authority_ready = {"value": False}
|
|
readbacks = iter([
|
|
None,
|
|
{
|
|
"event_id": str(verifying_event_id),
|
|
"run_id": awooop_run_id,
|
|
"operation_count": 1,
|
|
"execution_count": 1,
|
|
"resolved_count": 0,
|
|
},
|
|
{
|
|
"event_id": None,
|
|
"run_id": None,
|
|
"operation_count": 1,
|
|
"execution_count": 1,
|
|
"resolved_count": 0,
|
|
},
|
|
{
|
|
"event_id": str(resolved_event_id),
|
|
"run_id": awooop_run_id,
|
|
"operation_count": 2,
|
|
"execution_count": 1,
|
|
"resolved_count": 1,
|
|
},
|
|
])
|
|
event_calls: list[dict] = []
|
|
|
|
async def fake_authority(_payload):
|
|
verified = authority_ready["value"]
|
|
return {
|
|
"required": True,
|
|
"verified": verified,
|
|
"blocker": "none" if verified else "independent_verifier_missing",
|
|
"typed_domain": "control_plane_recovery" if verified else "",
|
|
"route_id": "agent99_cold_start_recovery" if verified else "",
|
|
}
|
|
|
|
async def fake_readback(**_kwargs):
|
|
return next(readbacks)
|
|
|
|
async def fake_record_event(**kwargs):
|
|
event_calls.append(kwargs)
|
|
return (
|
|
resolved_event_id
|
|
if kwargs["stage"] == "resolved"
|
|
else verifying_event_id
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
callback_service,
|
|
"_load_callback_closure_authority",
|
|
fake_authority,
|
|
)
|
|
monkeypatch.setattr(callback_service, "_load_callback_readback", fake_readback)
|
|
monkeypatch.setattr(callback_service, "record_external_alert_event", fake_record_event)
|
|
monkeypatch.setattr(
|
|
callback_service,
|
|
"get_alert_operation_log_repository",
|
|
lambda: repository,
|
|
)
|
|
|
|
pending = await callback_service.record_agent99_completion_callback(request)
|
|
authority_ready["value"] = True
|
|
resolved = await callback_service.record_agent99_completion_callback(request)
|
|
|
|
assert pending["ok"] is False
|
|
assert resolved["ok"] is True
|
|
assert resolved["duplicate"] is True
|
|
assert [item[0] for item in repository.events] == [
|
|
"EXECUTION_COMPLETED",
|
|
"RESOLVED",
|
|
]
|
|
assert [call["stage"] for call in event_calls] == ["verifying", "resolved"]
|
|
assert [call["is_duplicate"] for call in event_calls] == [False, True]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_completion_service_writes_event_run_and_operation_receipts(monkeypatch) -> None:
|
|
authorize_terminal_callback(monkeypatch)
|
|
request = Agent99CompletionCallbackRequest.model_validate(payload())
|
|
event_id = UUID("3c9e8ee7-fb21-5420-afae-ef928f75a58c")
|
|
awooop_run_id = "5e5b0080-e8c9-53a7-872b-0fc1ce80f11f"
|
|
repository = FakeOperationRepository()
|
|
readbacks = iter(
|
|
[
|
|
None,
|
|
{
|
|
"event_id": str(event_id),
|
|
"run_id": awooop_run_id,
|
|
"operation_count": 2,
|
|
"execution_count": 1,
|
|
"resolved_count": 1,
|
|
},
|
|
]
|
|
)
|
|
event_call: dict = {}
|
|
|
|
async def fake_readback(**_kwargs):
|
|
return next(readbacks)
|
|
|
|
async def fake_record_event(**kwargs):
|
|
event_call.update(kwargs)
|
|
return event_id
|
|
|
|
monkeypatch.setattr(callback_service, "_load_callback_readback", fake_readback)
|
|
monkeypatch.setattr(callback_service, "record_external_alert_event", fake_record_event)
|
|
monkeypatch.setattr(
|
|
callback_service,
|
|
"get_alert_operation_log_repository",
|
|
lambda: repository,
|
|
)
|
|
|
|
receipt = await callback_service.record_agent99_completion_callback(request)
|
|
|
|
assert receipt["durable_readback"] is True
|
|
assert receipt["awooop_run_id"] == awooop_run_id
|
|
assert event_call["provider"] == "agent99"
|
|
assert event_call["stage"] == "resolved"
|
|
assert [item[0] for item in repository.events] == [
|
|
"EXECUTION_COMPLETED",
|
|
"RESOLVED",
|
|
]
|
|
assert repository.events[0][1]["context"]["stores_secret"] is False
|
|
assert repository.events[0][1]["context"]["raw_log_stored"] is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_completion_service_compacts_long_alert_id_for_operation_log(
|
|
monkeypatch,
|
|
) -> None:
|
|
authorize_terminal_callback(monkeypatch)
|
|
external_alert_id = "awoooi-agent99-ae36a454-c9eb-5f2d-9e35-be852b3eac9b"
|
|
request = Agent99CompletionCallbackRequest.model_validate(
|
|
{**payload(), "alert_id": external_alert_id}
|
|
)
|
|
event_id = UUID("3c9e8ee7-fb21-5420-afae-ef928f75a58c")
|
|
repository = FakeOperationRepository()
|
|
readbacks = iter(
|
|
[
|
|
None,
|
|
{
|
|
"event_id": str(event_id),
|
|
"run_id": "5e5b0080-e8c9-53a7-872b-0fc1ce80f11f",
|
|
"operation_count": 2,
|
|
"execution_count": 1,
|
|
"resolved_count": 1,
|
|
},
|
|
]
|
|
)
|
|
|
|
async def fake_readback(**_kwargs):
|
|
return next(readbacks)
|
|
|
|
async def fake_record_event(**_kwargs):
|
|
return event_id
|
|
|
|
monkeypatch.setattr(callback_service, "_load_callback_readback", fake_readback)
|
|
monkeypatch.setattr(callback_service, "record_external_alert_event", fake_record_event)
|
|
monkeypatch.setattr(
|
|
callback_service,
|
|
"get_alert_operation_log_repository",
|
|
lambda: repository,
|
|
)
|
|
|
|
receipt = await callback_service.record_agent99_completion_callback(request)
|
|
|
|
compact_id = callback_service._operation_incident_id(external_alert_id)
|
|
assert receipt["durable_readback"] is True
|
|
assert compact_id is not None
|
|
assert len(compact_id) == 30
|
|
assert {event[1]["incident_id"] for event in repository.events} == {compact_id}
|
|
assert repository.events[0][1]["context"]["alert_id"] == external_alert_id
|
|
assert repository.events[0][1]["context"]["operation_incident_id"] == compact_id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_completion_service_is_idempotent(monkeypatch) -> None:
|
|
authorize_terminal_callback(monkeypatch)
|
|
request = Agent99CompletionCallbackRequest.model_validate(payload())
|
|
event_id = UUID("3c9e8ee7-fb21-5420-afae-ef928f75a58c")
|
|
existing = {
|
|
"event_id": str(event_id),
|
|
"run_id": "5e5b0080-e8c9-53a7-872b-0fc1ce80f11f",
|
|
"operation_count": 2,
|
|
"execution_count": 1,
|
|
"resolved_count": 1,
|
|
}
|
|
repository = FakeOperationRepository()
|
|
event_call: dict = {}
|
|
|
|
async def fake_readback(**_kwargs):
|
|
return existing
|
|
|
|
async def fake_record_event(**kwargs):
|
|
event_call.update(kwargs)
|
|
return event_id
|
|
|
|
monkeypatch.setattr(callback_service, "_load_callback_readback", fake_readback)
|
|
monkeypatch.setattr(callback_service, "record_external_alert_event", fake_record_event)
|
|
monkeypatch.setattr(
|
|
callback_service,
|
|
"get_alert_operation_log_repository",
|
|
lambda: repository,
|
|
)
|
|
|
|
receipt = await callback_service.record_agent99_completion_callback(request)
|
|
|
|
assert receipt["status"] == "duplicate_verified"
|
|
assert receipt["duplicate"] is True
|
|
assert event_call["is_duplicate"] is True
|
|
assert repository.events == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_completion_service_repairs_partial_terminal_receipt(monkeypatch) -> None:
|
|
authorize_terminal_callback(monkeypatch)
|
|
request = Agent99CompletionCallbackRequest.model_validate(payload())
|
|
event_id = UUID("3c9e8ee7-fb21-5420-afae-ef928f75a58c")
|
|
before = {
|
|
"event_id": str(event_id),
|
|
"run_id": "5e5b0080-e8c9-53a7-872b-0fc1ce80f11f",
|
|
"operation_count": 1,
|
|
"execution_count": 1,
|
|
"resolved_count": 0,
|
|
}
|
|
after = {**before, "operation_count": 2, "resolved_count": 1}
|
|
readbacks = iter([before, after])
|
|
repository = FakeOperationRepository()
|
|
|
|
async def fake_readback(**_kwargs):
|
|
return next(readbacks)
|
|
|
|
async def fake_record_event(**_kwargs):
|
|
return event_id
|
|
|
|
monkeypatch.setattr(callback_service, "_load_callback_readback", fake_readback)
|
|
monkeypatch.setattr(callback_service, "record_external_alert_event", fake_record_event)
|
|
monkeypatch.setattr(
|
|
callback_service,
|
|
"get_alert_operation_log_repository",
|
|
lambda: repository,
|
|
)
|
|
|
|
receipt = await callback_service.record_agent99_completion_callback(request)
|
|
|
|
assert receipt["durable_readback"] is True
|
|
assert [item[0] for item in repository.events] == ["RESOLVED"]
|