feat(agent-call): add contract-driven mock executor
This commit is contained in:
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user