Files
gogo-telefon/tests/test_definition_of_done.py
Senad Uka fd45ff84dc M6 + M0: hardening, deploy docs, PoC scripts
- Pipeline-down fallback: dialplan plays gogo-fallback after a failed
  AudioSocket (§15) + script to synthesize the Bosnian prompt via TTS
- Voice heartbeat file → /health/voice endpoint + live status in admin panel
- Retention job also purges stale call registrations; chat sessions metered;
  100%-usage super-admin alert email (GOGO_ADMIN_ALERT_EMAIL)
- .env.example, docs/DEPLOY.md (compose stack, GPU node, backups pg_dump,
  update procedure, security notes)
- §16 Definition of Done as an automated test: softphone-style call → fake
  partner availability → request pushed → webhook confirm → console SMS
- M0 PoC scripts: STT accuracy harness (CER/WER + spoken-digit check; dry-run
  verified with espeak-ng samples + faster-whisper small on CPU — phone number
  extracted exactly), TTS bake-off (Azure vs ElevenLabs, latency+cost), and
  end-to-end latency smoke test speaking real AudioSocket to the live pipeline

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 11:27:05 +02:00

118 lines
4.4 KiB
Python

"""§16 Definition of Done, software phase:
'a demo where a softphone call books a request end-to-end (agent conversation →
availability from fake partner API → request pushed → webhook outcome →
console-SMS logged) … all without any hardware present.'
(The chat-widget + mock-tenant half lives in tests/test_chat_widget.py.)
"""
from sqlalchemy import select
from gogo.agent.llm import LLMResponse, ScriptedLLM, ToolUse, set_llm
from gogo.domain import BookingStatus
from gogo.models import BookingRequest, Call, CallRegistration, Service
from gogo.voice.call import CallLimits, CallSession
from tests.fake_partner import staff_resolve
from tests.test_voice import UTTERANCE, FakeCallerTransport, frames, voice_fakes # noqa: F401
SLOT = {"start": "2026-07-15T17:00:00+02:00", "end": "2026-07-15T17:45:00+02:00"}
async def test_definition_of_done_voice_partner_flow(
session, partner_tenant, wire_fake_partner, gogo_client, emails, sms, voice_fakes # noqa: F811
):
stt, tts = voice_fakes
wire_fake_partner.slots["SRV-12"] = [SLOT]
svc = (
await session.execute(select(Service).where(Service.tenant_id == partner_tenant.id))
).scalar_one()
# the "softphone caller" asks for a manicure; the agent checks availability
# against the partner API and pushes the booking request into it
stt.texts = ["Htjela bih manikir u srijedu, Amra Hodžić, broj s kojeg zovem."]
set_llm(
ScriptedLLM(
[
LLMResponse(
text="",
tool_uses=[
ToolUse(
id="t1",
name="check_availability",
input={
"service_id": str(svc.id),
"date_from": "2026-07-15",
"date_to": "2026-07-15",
},
)
],
stop_reason="tool_use",
),
LLMResponse(
text="",
tool_uses=[
ToolUse(
id="t2",
name="submit_booking_request",
input={
"service_id": str(svc.id),
"service_name_raw": "manikir",
"client_name": "Amra Hodžić",
"client_phone": "+38765123456",
"slots": [SLOT],
"summary": "Manikir, srijeda u 17.",
},
)
],
stop_reason="tool_use",
),
LLMResponse(
text="Prosljeđujem zahtjev salonu — kontaktiraće vas. Hvala i prijatno!"
),
]
)
)
reg = CallRegistration(tenant_id=partner_tenant.id, caller_msisdn="+38765123456")
session.add(reg)
await session.commit()
reg_id = reg.id
transport = FakeCallerTransport(UTTERANCE)
await CallSession(
transport, reg_id, pace=0, limits=CallLimits(inactivity_s=5, max_call_s=60)
).run()
set_llm(None)
# 1) availability came from the fake partner, request was PUSHED into it
assert "REQ-0001" in wire_fake_partner.requests
pushed = wire_fake_partner.requests["REQ-0001"]
assert pushed["client"]["name"] == "Amra Hodžić"
assert pushed["service_id"] == "SRV-12"
assert pushed["source"] == "voice"
# partner tenants get no owner email by default (§8.2)
assert emails == []
# 2) salon staff confirm in their own software → webhook → Gogo closes + SMS
resp = await staff_resolve(
gogo_client, str(partner_tenant.id), "REQ-0001", "confirmed", confirmed_slot=SLOT
)
assert resp.status_code == 200
session.expire_all()
req = (await session.execute(select(BookingRequest))).scalar_one()
assert req.status == BookingStatus.confirmed.value
# 3) console-SMS logged to the client
assert len(sms) == 1
to, body = sms[0]
assert to == "+38765123456"
assert "Potvrđen termin" in body and "srijeda" in body
# 4) the call itself is fully accounted for
call = (await session.execute(select(Call))).scalar_one()
assert call.outcome == "request_created"
assert call.transcript and call.recording_path