143 lines
4.3 KiB
Python
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",
|
|
)
|