feat(agent-call): add contract-driven mock executor

This commit is contained in:
2026-09-12 19:29:49 +08:00
parent 6cd28c033e
commit d4ac130a47
47 changed files with 5936 additions and 30 deletions
+105
View File
@@ -0,0 +1,105 @@
"""Protocol-shaped deterministic mocks used by the mock profile.
They record protocol facts instead of pretending that an HTTP callback is a SIP,
ARI, RTP, AI-stream, or OSS integration.
"""
from __future__ import annotations
import json
import threading
from pathlib import Path
from typing import Any
class ProtocolJournal:
def __init__(self, path: str | Path) -> None:
self.path = Path(path)
self.path.parent.mkdir(parents=True, exist_ok=True)
self._lock = threading.Lock()
def append(self, protocol: str, event: str, **facts: Any) -> None:
entry = {"protocol": protocol, "event": event, **facts}
try:
encoded = json.dumps(
entry, ensure_ascii=False, sort_keys=True, separators=(",", ":")
)
with self._lock, self.path.open("a", encoding="utf-8") as stream:
stream.write(encoded + "\n")
except (OSError, TypeError, ValueError) as exc:
raise RuntimeError("protocol journal write failed") from exc
def read(self) -> list[dict[str, Any]]:
try:
lines = self.path.read_text(encoding="utf-8").splitlines()
return [json.loads(line) for line in lines if line]
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
raise RuntimeError("protocol journal read failed") from exc
class MockSIPPeer:
def __init__(self, journal: ProtocolJournal) -> None:
self.journal = journal
def invite(self, call_id: str, callee: str, target_uri: str, caller: str) -> None:
self.journal.append(
"sip",
"invite",
call_id=call_id,
callee=callee,
target_uri=target_uri,
caller=caller,
)
def ringing(self, call_id: str) -> None:
self.journal.append("sip", "ringing", call_id=call_id)
def answered(self, call_id: str) -> None:
self.journal.append("sip", "answered", call_id=call_id)
def bye(self, call_id: str, reason: str) -> None:
self.journal.append("sip", "bye", call_id=call_id, reason=reason)
class MockARI:
def __init__(self, journal: ProtocolJournal) -> None:
self.journal = journal
def channel_created(self, call_id: str, cell_id: str) -> None:
self.journal.append("ari", "channel_created", call_id=call_id, cell_id=cell_id)
def bridge_ready(self, call_id: str) -> None:
self.journal.append("ari", "bridge_ready", call_id=call_id)
def media_closed(self, call_id: str) -> None:
self.journal.append("ari", "media_closed", call_id=call_id)
def cleanup(self, call_id: str) -> None:
self.journal.append("ari", "cleanup", call_id=call_id)
class MockRTP:
def __init__(self, journal: ProtocolJournal) -> None:
self.journal = journal
def packet(self, call_id: str, direction: str, sequence: int) -> None:
self.journal.append(
"rtp", "packet", call_id=call_id, direction=direction, sequence=sequence
)
class MockAI:
def __init__(self, journal: ProtocolJournal) -> None:
self.journal = journal
def turn(
self, call_id: str, turn_index: int, speaker: str, playback_state: str
) -> None:
self.journal.append(
"ai",
"turn",
call_id=call_id,
turn_index=turn_index,
speaker=speaker,
playback_state=playback_state,
)