fix(sre): close bounded runtime contract gaps
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 4m41s
CD Pipeline / build-and-deploy (push) Successful in 15m36s
CD Pipeline / post-deploy-checks (push) Successful in 4m19s
All checks were successful
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 4m41s
CD Pipeline / build-and-deploy (push) Successful in 15m36s
CD Pipeline / post-deploy-checks (push) Successful in 4m19s
This commit is contained in:
@@ -5,6 +5,7 @@ import base64
|
||||
import hashlib
|
||||
import os
|
||||
import struct
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
os.environ.setdefault("DATABASE_URL", "postgresql+asyncpg://test:test@localhost/test")
|
||||
@@ -70,6 +71,36 @@ def app_client() -> TestClient:
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_agent99_client_retries_one_202_with_exact_db_only_receipt() -> None:
|
||||
source = (
|
||||
Path(__file__).resolve().parents[3] / "agent99-control-plane.ps1"
|
||||
).read_text(encoding="utf-8-sig")
|
||||
send_section = source.split("function Send-AgentTelegram {", 1)[1].split(
|
||||
"function Record-AgentEvent {",
|
||||
1,
|
||||
)[0]
|
||||
|
||||
assert send_section.count("Invoke-AgentCanonicalLifecycleIngress ") == 2
|
||||
assert 'if ([int]$ingress.statusCode -eq 202)' in send_section
|
||||
assert '"agent99_telegram_lifecycle_reconcile_v1"' in send_section
|
||||
assert '$payload["reconcile_receipt"] = $reconcileReceipt' in send_section
|
||||
assert "Start-Sleep -Seconds ([int]$ingress.retryAfterSeconds)" in send_section
|
||||
assert "[int]$ingress.retryAfterSeconds -le 2" in send_section
|
||||
assert "canonicalIngressAttemptCount = 2" in send_section
|
||||
assert "reconcileProviderSendPerformed" in send_section
|
||||
assert "provider_message_id" in send_section
|
||||
assert "destination_binding" in send_section
|
||||
assert "X-Agent99-Lifecycle-Token" not in send_section
|
||||
selftest_section = source.split(
|
||||
"function Invoke-AgentTelegramDeliverySelfTest {",
|
||||
1,
|
||||
)[1].split("function Invoke-AgentSensorGateSelfTest {", 1)[0]
|
||||
assert 'id = $testId' in selftest_section
|
||||
assert selftest_section.count(
|
||||
'Send-AgentTelegram "warning" "telegram_delivery_test"'
|
||||
) == 1
|
||||
|
||||
|
||||
def test_lifecycle_payload_rejects_unbounded_content() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
Agent99TelegramLifecycleRequest.model_validate(
|
||||
@@ -90,6 +121,22 @@ def test_lifecycle_payload_rejects_unbounded_content() -> None:
|
||||
"visual": {**visual_payload(), "sha256": "0" * 64},
|
||||
}
|
||||
)
|
||||
with pytest.raises(ValidationError, match="reconcile_identity_mismatch"):
|
||||
Agent99TelegramLifecycleRequest.model_validate(
|
||||
{
|
||||
**payload(),
|
||||
"reconcile_receipt": {
|
||||
"schema_version": (
|
||||
"agent99_telegram_lifecycle_reconcile_v1"
|
||||
),
|
||||
"delivery_id": payload()["delivery_id"],
|
||||
"incident_id": "INC-WRONG",
|
||||
"state_key": payload()["state_key"],
|
||||
"provider_message_id": "8123",
|
||||
"destination_binding": "a" * 64,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def test_visual_caption_stays_within_telegram_limit_without_broken_html() -> None:
|
||||
@@ -208,6 +255,91 @@ def test_lifecycle_endpoint_returns_same_delivery_receipt(monkeypatch) -> None:
|
||||
assert response.json()["provider_message_id"] == "8123"
|
||||
|
||||
|
||||
def test_lifecycle_endpoint_202_retry_is_db_only_and_returns_200(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(settings, "AGENT99_SRE_ALERT_RELAY_TOKEN", "expected")
|
||||
destination_binding = "a" * 64
|
||||
calls = {"deliver": 0, "reconcile": 0}
|
||||
|
||||
async def pending_delivery(request):
|
||||
calls["deliver"] += 1
|
||||
assert request.reconcile_receipt is None
|
||||
return {
|
||||
"schema_version": "agent99_telegram_lifecycle_receipt_v1",
|
||||
"ok": False,
|
||||
"delivery_id": request.delivery_id,
|
||||
"incident_id": request.incident_id,
|
||||
"delivery_status": "pending_unknown",
|
||||
"provider_message_id": "8124",
|
||||
"destination_binding": destination_binding,
|
||||
"provider_send_performed": True,
|
||||
"reconcile_receipt": {
|
||||
"schema_version": "agent99_telegram_lifecycle_reconcile_v1",
|
||||
"delivery_id": request.delivery_id,
|
||||
"incident_id": request.incident_id,
|
||||
"state_key": request.state_key,
|
||||
"provider_message_id": "8124",
|
||||
"destination_binding": destination_binding,
|
||||
},
|
||||
}
|
||||
|
||||
async def durable_reconcile(request):
|
||||
calls["reconcile"] += 1
|
||||
assert request.reconcile_receipt is not None
|
||||
assert request.reconcile_receipt.provider_message_id == "8124"
|
||||
assert (
|
||||
request.reconcile_receipt.destination_binding
|
||||
== destination_binding
|
||||
)
|
||||
return {
|
||||
"schema_version": "agent99_telegram_lifecycle_receipt_v1",
|
||||
"ok": True,
|
||||
"delivery_id": request.delivery_id,
|
||||
"incident_id": request.incident_id,
|
||||
"delivery_status": "sent_reused",
|
||||
"provider_message_id": "8124",
|
||||
"destination_binding": destination_binding,
|
||||
"durable_outbound_acknowledged": True,
|
||||
"destination_binding_verified": True,
|
||||
"provider_send_performed": False,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
agents_api,
|
||||
"deliver_agent99_telegram_lifecycle",
|
||||
pending_delivery,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
agents_api,
|
||||
"reconcile_agent99_telegram_lifecycle",
|
||||
durable_reconcile,
|
||||
)
|
||||
client = app_client()
|
||||
first = client.post(
|
||||
"/api/v1/agents/agent99/telegram-lifecycle",
|
||||
headers={"X-Agent99-Lifecycle-Token": "expected"},
|
||||
json=payload(),
|
||||
)
|
||||
|
||||
assert first.status_code == 202
|
||||
assert first.headers["retry-after"] == "2"
|
||||
retry_payload = {
|
||||
**payload(),
|
||||
"reconcile_receipt": first.json()["reconcile_receipt"],
|
||||
}
|
||||
second = client.post(
|
||||
"/api/v1/agents/agent99/telegram-lifecycle",
|
||||
headers={"X-Agent99-Lifecycle-Token": "expected"},
|
||||
json=retry_payload,
|
||||
)
|
||||
|
||||
assert second.status_code == 200
|
||||
assert second.json()["delivery_status"] == "sent_reused"
|
||||
assert second.json()["provider_send_performed"] is False
|
||||
assert calls == {"deliver": 1, "reconcile": 1}
|
||||
|
||||
|
||||
def test_lifecycle_outbox_identity_is_stable_and_separate() -> None:
|
||||
base = {
|
||||
"callback_reply": {
|
||||
@@ -249,15 +381,42 @@ def test_lifecycle_outbox_identity_is_stable_and_separate() -> None:
|
||||
) != gateway_service._controlled_apply_result_delivery_run_id(controlled_identity)
|
||||
|
||||
|
||||
def test_lifecycle_outbox_persists_destination_and_visual_bindings() -> None:
|
||||
destination_binding = "a" * 64
|
||||
|
||||
envelope = gateway_service._merge_outbound_source_envelope_extra(
|
||||
{"adapter": "legacy_telegram_gateway"},
|
||||
{
|
||||
"canonical_route_receipt": {
|
||||
"destination_binding": destination_binding,
|
||||
},
|
||||
"visual_delivery": {
|
||||
"requested": True,
|
||||
"raw_visual_stored": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert envelope["canonical_route_receipt"] == {
|
||||
"destination_binding": destination_binding,
|
||||
}
|
||||
assert envelope["visual_delivery"] == {
|
||||
"requested": True,
|
||||
"raw_visual_stored": False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifecycle_durable_readback_binds_exact_identity_without_egress(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
gateway = object.__new__(gateway_service.TelegramGateway)
|
||||
destination_binding = "a" * 64
|
||||
reconcile = AsyncMock(
|
||||
return_value={
|
||||
"status": "sent",
|
||||
"provider_message_id": "8124",
|
||||
"destination_binding": destination_binding,
|
||||
"visual_requested": True,
|
||||
}
|
||||
)
|
||||
@@ -272,12 +431,18 @@ async def test_lifecycle_durable_readback_binds_exact_identity_without_egress(
|
||||
incident_id=payload()["incident_id"],
|
||||
state_key=payload()["state_key"],
|
||||
expected_provider_message_id="8124",
|
||||
expected_destination_binding=destination_binding,
|
||||
)
|
||||
|
||||
assert result["ok"] is True
|
||||
assert result["_awooop_delivery_status"] == "sent_reused"
|
||||
assert result["_awooop_provider_send_performed"] is False
|
||||
assert result["_awooop_durable_readback_performed"] is True
|
||||
assert result["_awooop_delivery_context"] == {
|
||||
"destination_binding": destination_binding,
|
||||
"destination_binding_verified": True,
|
||||
"durable_exact_row_readback": True,
|
||||
}
|
||||
reconcile.assert_awaited_once_with(
|
||||
identity={
|
||||
"project_id": "awoooi",
|
||||
@@ -287,9 +452,116 @@ async def test_lifecycle_durable_readback_binds_exact_identity_without_egress(
|
||||
"delivery_kind": "agent99_lifecycle",
|
||||
},
|
||||
expected_provider_message_id="8124",
|
||||
expected_destination_binding=destination_binding,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifecycle_reconcile_destination_mismatch_never_finalizes(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
gateway = object.__new__(gateway_service.TelegramGateway)
|
||||
readback = AsyncMock(
|
||||
return_value={
|
||||
"status": "pending_unknown",
|
||||
"run_id": "00000000-0000-0000-0000-000000000201",
|
||||
"message_id": "00000000-0000-0000-0000-000000000202",
|
||||
"destination_binding": "a" * 64,
|
||||
}
|
||||
)
|
||||
finalize = AsyncMock(return_value=True)
|
||||
monkeypatch.setattr(
|
||||
gateway,
|
||||
"_read_controlled_apply_result_outbound",
|
||||
readback,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
gateway,
|
||||
"_finalize_controlled_apply_result_outbound",
|
||||
finalize,
|
||||
)
|
||||
|
||||
result = await gateway._reconcile_controlled_apply_result_outbound(
|
||||
identity={
|
||||
"project_id": "awoooi",
|
||||
"automation_run_id": payload()["delivery_id"],
|
||||
"incident_id": payload()["incident_id"],
|
||||
"apply_op_id": payload()["state_key"],
|
||||
"delivery_kind": "agent99_lifecycle",
|
||||
},
|
||||
expected_provider_message_id="8124",
|
||||
expected_destination_binding="b" * 64,
|
||||
)
|
||||
|
||||
assert result["status"] == "destination_binding_mismatch"
|
||||
finalize.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifecycle_reconcile_service_never_enters_provider_sender(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
destination_binding = "a" * 64
|
||||
calls: list[dict] = []
|
||||
|
||||
class Gateway:
|
||||
async def send_agent99_lifecycle_receipt(self, **_kwargs):
|
||||
raise AssertionError("provider sender must not run during reconcile")
|
||||
|
||||
async def reconcile_agent99_lifecycle_receipt(self, **kwargs):
|
||||
calls.append(kwargs)
|
||||
return {
|
||||
"ok": True,
|
||||
"result": {"message_id": "8124"},
|
||||
"_awooop_outbound_mirror_acknowledged": True,
|
||||
"_awooop_delivery_status": "sent_reused",
|
||||
"_awooop_provider_send_performed": False,
|
||||
"_awooop_delivery_context": {
|
||||
"destination_binding": destination_binding,
|
||||
"destination_binding_verified": True,
|
||||
"durable_exact_row_readback": True,
|
||||
},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(
|
||||
lifecycle_service,
|
||||
"get_telegram_gateway",
|
||||
lambda: Gateway(),
|
||||
)
|
||||
request = Agent99TelegramLifecycleRequest.model_validate(
|
||||
{
|
||||
**payload(),
|
||||
"reconcile_receipt": {
|
||||
"schema_version": "agent99_telegram_lifecycle_reconcile_v1",
|
||||
"delivery_id": payload()["delivery_id"],
|
||||
"incident_id": payload()["incident_id"],
|
||||
"state_key": payload()["state_key"],
|
||||
"provider_message_id": "8124",
|
||||
"destination_binding": destination_binding,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
receipt = await lifecycle_service.reconcile_agent99_telegram_lifecycle(
|
||||
request
|
||||
)
|
||||
|
||||
assert receipt["ok"] is True
|
||||
assert receipt["delivery_status"] == "sent_reused"
|
||||
assert receipt["provider_send_performed"] is False
|
||||
assert receipt["destination_binding_verified"] is True
|
||||
assert calls == [
|
||||
{
|
||||
"delivery_id": payload()["delivery_id"],
|
||||
"incident_id": payload()["incident_id"],
|
||||
"state_key": payload()["state_key"],
|
||||
"project_id": "awoooi",
|
||||
"expected_provider_message_id": "8124",
|
||||
"expected_destination_binding": destination_binding,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lifecycle_service_uses_durable_canonical_sender(monkeypatch) -> None:
|
||||
calls: list[dict] = []
|
||||
@@ -343,6 +615,7 @@ async def test_lifecycle_reconciles_pending_send_without_duplicate_provider_mess
|
||||
"_awooop_visual_delivery_verified": True,
|
||||
"_awooop_delivery_context": {
|
||||
"destination_binding_verified": True,
|
||||
"destination_binding": "a" * 64,
|
||||
},
|
||||
}
|
||||
reconciled_response = {
|
||||
@@ -395,6 +668,7 @@ async def test_lifecycle_reconciles_pending_send_without_duplicate_provider_mess
|
||||
"state_key": payload()["state_key"],
|
||||
"project_id": "awoooi",
|
||||
"expected_provider_message_id": "9126",
|
||||
"expected_destination_binding": "a" * 64,
|
||||
}
|
||||
|
||||
|
||||
@@ -449,6 +723,7 @@ async def test_lifecycle_follower_only_reads_inflight_durable_delivery(
|
||||
assert len(send_calls) == 1
|
||||
assert len(reconcile_calls) == 1
|
||||
assert reconcile_calls[0]["expected_provider_message_id"] is None
|
||||
assert reconcile_calls[0]["expected_destination_binding"] is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -470,6 +745,7 @@ async def test_lifecycle_reconciliation_fails_closed_after_bounded_readbacks(
|
||||
"_awooop_provider_send_performed": True,
|
||||
"_awooop_delivery_context": {
|
||||
"destination_binding_verified": True,
|
||||
"destination_binding": "a" * 64,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -511,6 +787,10 @@ async def test_lifecycle_reconciliation_fails_closed_after_bounded_readbacks(
|
||||
call["expected_provider_message_id"] == "9127"
|
||||
for call in reconcile_calls
|
||||
)
|
||||
assert all(
|
||||
call["expected_destination_binding"] == "a" * 64
|
||||
for call in reconcile_calls
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user