Files

143 lines
4.3 KiB
Python

"""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 hashlib
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,
payload: bytes | None = None,
) -> None:
payload = payload if payload is not None else b"\x01\x02" * 160
self.journal.append(
"rtp",
"packet",
call_id=call_id,
direction=direction,
sequence=sequence,
payload_bytes=len(payload),
payload_sha256=hashlib.sha256(payload).hexdigest(),
non_silent=any(payload),
)
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,
text: str | None = None,
) -> None:
self.journal.append(
"ai",
"turn",
call_id=call_id,
turn_index=turn_index,
speaker=speaker,
playback_state=playback_state,
text=text or f"mock-{speaker}-turn-{turn_index}",
)
self.journal.append(
"ai",
"playback",
call_id=call_id,
turn_index=turn_index,
state="sent",
)
self.journal.append(
"ai",
"playback",
call_id=call_id,
turn_index=turn_index,
state=playback_state
if playback_state in {"confirmed", "playback_confirmed", "unknown"}
else "unknown",
)