feat: unify debug runtime timelines

This commit is contained in:
Xin Wang
2026-08-13 09:30:43 +08:00
parent 7c18cea16d
commit eb111a3890
21 changed files with 1380 additions and 533 deletions
+18
View File
@@ -521,6 +521,24 @@ class PromptBrainTests(unittest.IsolatedAsyncioTestCase):
self.assertIn("点击“确认”", confirmation_messages[0]["content"])
self.assertIn("弹窗标题:重要提示", confirmation_messages[0]["content"])
self.assertIn("弹窗内容:请确认已阅读。", confirmation_messages[0]["content"])
message_traces = [
frame.message
for frame in queued
if isinstance(frame, OutputTransportMessageUrgentFrame)
and frame.message.get("type") == "workflow-event"
and frame.message.get("event")
in {"message_started", "message_completed"}
]
self.assertEqual(
[event["event"] for event in message_traces],
["message_started", "message_completed"],
)
self.assertEqual(message_traces[0]["messageTitle"], "重要提示")
self.assertEqual(
message_traces[0]["messageContent"],
"请确认已阅读。",
)
self.assertEqual(message_traces[1]["action"], "confirmed")
self.assertEqual(
sum(isinstance(frame, LLMRunFrame) for frame in queued),
1,
+17 -1
View File
@@ -11,12 +11,27 @@ from pipecat.frames.frames import (
)
from services.pipecat.call_lifecycle import (
CallEndCoordinator,
call_end_trace_event,
playback_marker_for,
)
from services.pipecat.processors import CallEndingUserMuteStrategy
class CallEndCoordinatorTest(unittest.IsolatedAsyncioTestCase):
def test_shared_trace_event_uses_runtime_record_contract(self):
event = call_end_trace_event(
"pending",
reason="用户已完成咨询",
interruptible=True,
voice_interruptible=True,
)
self.assertEqual(event["type"], "workflow-event")
self.assertEqual(event["event"], "conversation_end_requested")
self.assertEqual(event["reason"], "用户已完成咨询")
self.assertTrue(event["interruptible"])
self.assertTrue(event["voiceInterruptible"])
async def asyncSetUp(self):
self.reasons: list[str] = []
self.states: list[tuple[str, bool]] = []
@@ -87,7 +102,7 @@ class CallEndCoordinatorTest(unittest.IsolatedAsyncioTestCase):
self.assertTrue(self.coordinator.accepts_input)
self.assertTrue(self.coordinator.cancel_pending_end())
await asyncio.sleep(0)
self.assertEqual(self.states[-1], ("cancelled", False))
self.assertEqual(self.states[-1], ("cancelled", True))
self.assertFalse(self.coordinator.ending)
self.assertTrue(self.coordinator.accepts_input)
self.assertTrue(self.coordinator.speaking)
@@ -95,6 +110,7 @@ class CallEndCoordinatorTest(unittest.IsolatedAsyncioTestCase):
await self.coordinator.observe(BotStoppedSpeakingFrame())
self.assertFalse(self.coordinator.speaking)
self.assertEqual(self.reasons, [])
self.assertEqual(self.coordinator.reason, "user_goodbye")
async def test_committed_end_rejects_cancellation(self):
self.coordinator.begin("required_shutdown")
@@ -144,6 +144,22 @@ class ConversationRecorderTest(unittest.IsolatedAsyncioTestCase):
self.assertEqual(saved["sessionId"], "conv_test")
self.assertEqual(saved["sequence"], 1)
async def test_internal_message_stage_transport_trace_is_not_persisted_twice(self):
recorder = ConversationRecorder("conv_test")
event = {
"type": "workflow-event",
"eventId": "wfe_internal",
"event": "client_tool_completed",
"functionName": "show_message",
"internalStage": True,
}
with patch("services.conversation_history.SessionLocal") as session_local:
await recorder.record_transport_message(event)
session_local.assert_not_called()
self.assertEqual(recorder._trace_sequence, 0)
async def test_image_is_persisted_as_message_with_artifact(self):
conversation = SimpleNamespace(message_count=0)
+91 -1
View File
@@ -10,10 +10,13 @@ from services.runtime_variables import DynamicVariableStore, prepare_dynamic_con
from services.test_runs.mock_tools import MockToolExecutor, TextClientToolPort
from services.test_runs.text_runner import (
_TextCaptureProcessor,
_opening_speech_from_message,
_resolve_startup_greeting,
_runtime_record_from_message,
_wait_for_startup,
_wait_for_quiescence,
)
from test_schemas import BatchRunCreate
from test_schemas import BatchRunCreate, BatchStartupResult
class FakeLLM:
@@ -126,6 +129,25 @@ class DebugAutoTestContractTests(unittest.TestCase):
"你好,测试用户",
)
def test_startup_result_exposes_real_runtime_records(self):
result = BatchStartupResult.model_validate(
{
"completed": True,
"runtimeRecords": [
{
"type": "workflow-event",
"eventId": "wfe_opening",
"event": "message_started",
}
],
}
)
self.assertEqual(
result.runtime_records[0]["event"],
"message_started",
)
def test_text_client_port_records_automatic_message_confirmation(self):
port = TextClientToolPort()
@@ -142,6 +164,51 @@ class DebugAutoTestContractTests(unittest.TestCase):
[{"title": "重要提示", "message": "请确认"}],
)
def test_confirmation_owned_speech_is_kept_as_startup_greeting(self):
self.assertEqual(
_resolve_startup_greeting(
"",
["您好,请先阅读弹窗。"],
),
"您好,请先阅读弹窗。",
)
self.assertEqual(
_resolve_startup_greeting(
"普通开场白",
["其他固定播报"],
),
"普通开场白",
)
self.assertEqual(
_resolve_startup_greeting(
"",
[],
),
"",
)
def test_only_prompt_opening_transcript_is_identified_as_greeting(self):
self.assertEqual(
_opening_speech_from_message(
{
"type": "transcript",
"content": "您好,请先阅读弹窗。",
"source": "prompt-opening-speech",
}
),
"您好,请先阅读弹窗。",
)
self.assertEqual(
_opening_speech_from_message(
{
"type": "transcript",
"content": "普通 Workflow 播报",
"source": "workflow-message-speech",
}
),
"",
)
class DebugAutoTestRuntimeTests(unittest.IsolatedAsyncioTestCase):
async def test_system_end_tool_keeps_real_lifecycle_in_text_tests(self):
@@ -180,6 +247,29 @@ class DebugAutoTestRuntimeTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(params.result["action"], "ending_call")
self.assertEqual(executor_holder["executor"].unmocked_calls, [])
async def test_text_capture_keeps_runtime_records_for_auto_test_ui(self):
event = {
"type": "workflow-event",
"eventId": "wfe_end",
"event": "conversation_end_requested",
"reason": "测试完成",
}
self.assertEqual(_runtime_record_from_message(event), event)
self.assertIsNone(
_runtime_record_from_message({"type": "call-end-state"})
)
self.assertIsNone(
_runtime_record_from_message(
{
"type": "workflow-event",
"eventId": "wfe_internal",
"event": "client_tool_completed",
"internalStage": True,
}
)
)
async def test_startup_waits_for_queued_entry_reply(self):
capture = _TextCaptureProcessor(LLMContext(messages=[]))