feat(agent-call): implement remediation plan
This commit is contained in:
+84
-8
@@ -7,12 +7,15 @@ exists. Passing Mock probes never upgrades the result to production readiness.
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from contextlib import redirect_stdout
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from types import TracebackType
|
||||
from typing import Any, Self
|
||||
|
||||
from agent_call.core import (
|
||||
AgentCallService,
|
||||
@@ -221,11 +224,14 @@ def probe_v04() -> dict[str, Any]:
|
||||
|
||||
|
||||
class ConflictContext:
|
||||
def __enter__(self) -> ConflictContext:
|
||||
def __enter__(self) -> Self:
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: Any
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc: BaseException | None,
|
||||
tb: TracebackType | None,
|
||||
) -> bool:
|
||||
if exc_type is not ConflictError:
|
||||
raise AssertionError("expected revision conflict")
|
||||
@@ -256,10 +262,37 @@ def probe_v06_v07() -> dict[str, Any]:
|
||||
"disconnect": "ari_disconnect",
|
||||
}
|
||||
outcomes: dict[str, str] = {}
|
||||
uncertain_before_reconcile: str | None = None
|
||||
for label, scenario in scenarios.items():
|
||||
body = command(f"cmd_{label}")
|
||||
svc.set_failure(body["command_id"], scenario)
|
||||
snapshot = run_success(svc, body)
|
||||
svc.publish_execute(body)
|
||||
if scenario == "ari_disconnect":
|
||||
call_id = None
|
||||
before = None
|
||||
for _ in range(500):
|
||||
svc.process_once()
|
||||
current = svc.get_command("tenant-demo", body["command_id"])
|
||||
call_id = current.get("call_id")
|
||||
if call_id:
|
||||
before = svc.get_call("tenant-demo", call_id)
|
||||
if before["call_state"] == "reconciling":
|
||||
break
|
||||
time.sleep(0.01)
|
||||
if not call_id or before is None:
|
||||
raise AssertionError("disconnect probe did not create a call")
|
||||
uncertain_before_reconcile = before["call_state"]
|
||||
if uncertain_before_reconcile != "reconciling":
|
||||
raise AssertionError(
|
||||
"disconnect probe did not retain unknown state"
|
||||
)
|
||||
svc.reconcile_call(
|
||||
call_id, ended=True, outcome="failed", reason="ARI_RECONCILED"
|
||||
)
|
||||
snapshot = svc.get_command("tenant-demo", body["command_id"])
|
||||
else:
|
||||
svc.wait_for_idle()
|
||||
snapshot = svc.get_command("tenant-demo", body["command_id"])
|
||||
call = svc.get_call("tenant-demo", snapshot["call_id"])
|
||||
outcomes[label] = call["outcome"]
|
||||
if len(call["attempts"]) != 1:
|
||||
@@ -280,6 +313,7 @@ def probe_v06_v07() -> dict[str, Any]:
|
||||
"asterisk_real": "not_run",
|
||||
"outcomes": outcomes,
|
||||
"fallback_redials": 0,
|
||||
"disconnect_state_before_reconcile": uncertain_before_reconcile,
|
||||
}
|
||||
|
||||
|
||||
@@ -374,9 +408,37 @@ def probe_v11_v12() -> dict[str, Any]:
|
||||
body = command("cmd_backup")
|
||||
run_success(svc, body)
|
||||
backup = Path(directory) / "backup.sqlite3"
|
||||
copy_database(str(Path(directory) / "executor.sqlite3"), str(backup))
|
||||
with redirect_stdout(io.StringIO()):
|
||||
copy_database(str(Path(directory) / "executor.sqlite3"), str(backup))
|
||||
if not backup.is_file():
|
||||
raise AssertionError("backup file was not created")
|
||||
restored = AgentCallService(
|
||||
db_path=backup,
|
||||
object_dir=Path(directory) / "restored-objects",
|
||||
broker=InMemoryBroker(),
|
||||
)
|
||||
try:
|
||||
restored_command = restored.get_command("tenant-demo", body["command_id"])
|
||||
restored_call = restored.get_call(
|
||||
"tenant-demo", restored_command["call_id"]
|
||||
)
|
||||
if (
|
||||
restored_command["status"] != "completed"
|
||||
or restored_call["recordings"][0]["status"] != "verified"
|
||||
):
|
||||
raise AssertionError("restored database lost the completed call facts")
|
||||
restored_counts = {
|
||||
"commands": restored.store.one("SELECT COUNT(*) AS n FROM commands")[
|
||||
"n"
|
||||
],
|
||||
"events": restored.store.one("SELECT COUNT(*) AS n FROM events")["n"],
|
||||
"inbox": restored.store.one("SELECT COUNT(*) AS n FROM inbox")["n"],
|
||||
"recordings": restored.store.one(
|
||||
"SELECT COUNT(*) AS n FROM recordings"
|
||||
)["n"],
|
||||
}
|
||||
finally:
|
||||
restored.stop()
|
||||
metrics = svc.metrics()
|
||||
if "agent_call_outbox_pending 0" not in metrics or 'mode="mock"' not in metrics:
|
||||
raise AssertionError("monitoring metrics lack healthy mock state")
|
||||
@@ -392,7 +454,9 @@ def probe_v11_v12() -> dict[str, Any]:
|
||||
return {
|
||||
"backup": "verified",
|
||||
"metrics": "healthy",
|
||||
"rollback_preserves_db": True,
|
||||
"rollback_preserves_db": "not_run",
|
||||
"restored_db_facts": True,
|
||||
"restored_counts": restored_counts,
|
||||
"real_mode_mock_rejection": True,
|
||||
}
|
||||
|
||||
@@ -419,10 +483,15 @@ def probe_v13() -> dict[str, Any]:
|
||||
if (
|
||||
evidence.get("status") != "observed"
|
||||
or evidence.get("production_claim", True)
|
||||
or not evidence.get("continuous_media_evidence", False)
|
||||
or load.get("minimum_answered_calls", 0) < 1000
|
||||
or load.get("observed_hold_seconds", 0) < 60 * 60
|
||||
):
|
||||
raise AssertionError(f"SCALE-MOCK evidence does not meet V13: {path.name}")
|
||||
return {
|
||||
"status": "not_run",
|
||||
"evidence_file": str(path.relative_to(ROOT)),
|
||||
"reason": "historical state-only evidence cannot satisfy continuous media V13",
|
||||
}
|
||||
return {
|
||||
"status": "observed",
|
||||
"evidence_file": str(path.relative_to(ROOT)),
|
||||
@@ -466,7 +535,14 @@ def run() -> dict[str, Any]:
|
||||
"duration_ms": round((time.monotonic() - started) * 1000, 3),
|
||||
"evidence": evidence,
|
||||
}
|
||||
except Exception as exc:
|
||||
except (
|
||||
AssertionError,
|
||||
KeyError,
|
||||
OSError,
|
||||
RuntimeError,
|
||||
ServiceError,
|
||||
ValueError,
|
||||
) as exc:
|
||||
results["probes"][name] = {
|
||||
"status": "FAIL",
|
||||
"duration_ms": round((time.monotonic() - started) * 1000, 3),
|
||||
|
||||
Reference in New Issue
Block a user