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>
This commit is contained in:
117
tests/test_definition_of_done.py
Normal file
117
tests/test_definition_of_done.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""§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
|
||||
Reference in New Issue
Block a user