from __future__ import annotations import json from collections.abc import Callable from pathlib import Path from unittest.mock import patch import pytest from cognitive_agent.application.codex_assistant import ( CodexAssistantService, _authorize_vehicle_request, ) from cognitive_agent.application.native_can_runtime import NativeCanExecutor from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService from cognitive_agent.infrastructure.speech.understanding import ( UnderstandingTranscriptProcessor, ) from tests.unit.test_codex_assistant_service import CONTROL_TERMS, FakeCodexClient @pytest.fixture(scope="module") def speech_processor() -> UnderstandingTranscriptProcessor: return UnderstandingTranscriptProcessor(REPOSITORY_ROOT) @pytest.mark.parametrize( ("spoken_duration", "duration_ms"), ( ("3s", 3_000), ("3秒", 3_000), ("三秒", 3_000), ("1.5s", 1_500), ("1.5秒", 1_500), ), ) @pytest.mark.parametrize("backrest_alias", ("座椅靠背", "椅背")) def test_spoken_preview_keeps_explicit_timed_backrest_semantics( speech_processor: UnderstandingTranscriptProcessor, spoken_duration: str, duration_ms: int, backrest_alias: str, ) -> None: raw = ( f"主驾{backrest_alias}向后移动{spoken_duration}," "先不要下发,给出解析的控制指令" ) result = speech_processor.process(raw) assert len(result.commands) == 1 command = result.commands[0] assert command.capability == "seat.set_driver_axis_native" assert command.target_scope == "driver" assert command.operation == "backward" assert command.parameters == { "axis": "backrest", "direction": "backward", "duration_ms": duration_ms, } seconds = f"{duration_ms / 1_000:g}秒" assert seconds in result.clean_text assert seconds in result.assistant_text assert f"{duration_ms / 1_000:g}度" not in result.assistant_text assert "不要下发" in result.assistant_text assert "解析的控制指令" in result.assistant_text assert result.execution_eligible is False def test_reported_three_second_preview_is_preserved_exactly_and_fails_closed( speech_processor: UnderstandingTranscriptProcessor, ) -> None: raw = "主驾座椅靠背向后移动3s,先不要下发,给出解析的控制指令" result = speech_processor.process(raw) assert result.clean_text == "将主驾座椅靠背向后移动3秒。" assert result.assistant_text == ( "将主驾座椅靠背向后移动3秒,先不要下发,给出解析的控制指令。" ) command = result.commands[0] assert command.resolution_status == "unsupported" assert any("高于工程上限2000" in issue for issue in command.validation_issues) assert result.semantic_status == "recognized_unsupported" assert result.uncertain is True assert result.execution_eligible is False def test_reported_three_second_preview_cannot_enter_the_service_execution_bridge( tmp_path: Path, speech_processor: UnderstandingTranscriptProcessor, ) -> None: transcript = speech_processor.process( "主驾座椅靠背向后移动3s,先不要下发,给出解析的控制指令" ) understanding = transcript.to_dict() understanding["utterance_id"] = "reported-timed-preview-safety-gate" service = SeatDemoService(REPOSITORY_ROOT) service.artifact_root = tmp_path / "artifacts" with patch.object( NativeCanExecutor, "execute", side_effect=AssertionError("preview-only speech reached physical CAN"), ) as physical_execute, pytest.raises(ValueError, match="not execution eligible"): service.process( transcript.clean_text, provider="local", conversation_id="conv-reported-timed-preview-gate", execution_mode="dry_run", vehicle_id="denza_n7", _speech_understanding=understanding, ) assert physical_execute.call_count == 0 @pytest.mark.parametrize( "preview_request", ( "先不要下发,给出解析的控制指令", "只解析不要执行", "仅预览", "不要执行", "不下发", "不要发送到车辆", "只给出控制指令", "仅返回解析结果", ), ) def test_preview_only_phrases_cannot_authorize_vehicle_execution( speech_processor: UnderstandingTranscriptProcessor, preview_request: str, ) -> None: result = speech_processor.process( f"主驾座椅靠背向后移动2秒,{preview_request}" ) authorization = _authorize_vehicle_request(result.assistant_text, CONTROL_TERMS) assert result.commands[0].parameters["duration_ms"] == 2_000 assert result.execution_eligible is False assert authorization.instruction is None assert authorization.reason == "denied_by_user" @pytest.mark.parametrize( "preview_request", ( "先不要下发,给出解析的控制指令", "只解析不要执行", "仅预览", "不要执行", "不下发", "不要发送到车辆", ), ) def test_spoken_preview_routes_to_answer_only_and_never_calls_control_service( speech_processor: UnderstandingTranscriptProcessor, preview_request: str, ) -> None: transcript = speech_processor.process( f"主驾座椅靠背向后移动2秒,{preview_request}" ) client = FakeCodexClient() assistant = CodexAssistantService(client) events: list[dict[str, object]] = [] control_calls: list[str] = [] def unexpected_control( instruction: str, _progress: Callable[[dict[str, object]], None], ) -> dict[str, object]: control_calls.append(instruction) pytest.fail("a preview-only speech request reached vehicle execution") result = assistant.stream( transcript.assistant_text, conversation_id="conv-speech-preview-answer-only", principal="speech-preview-matrix", history=[], emit=lambda event: events.append(event) or True, execute_control=unexpected_control, control_terms=CONTROL_TERMS, ) assert result["route"] == "agent" assert control_calls == [] assert not any(str(event.get("type", "")).startswith("action.") for event in events) assert events[0]["route"] == "agent" assert events[-1]["type"] == "answer.completed" @pytest.mark.parametrize( ("raw", "duration_ms"), ( ("主驾座椅靠背向后移动2s", 2_000), ("主驾座椅靠背向后移动2秒", 2_000), ("主驾椅背向后移动二秒", 2_000), ("主驾椅背向后移动1.5s", 1_500), ), ) def test_ordinary_timed_speech_reaches_only_the_dry_run_service_bridge( tmp_path: Path, speech_processor: UnderstandingTranscriptProcessor, raw: str, duration_ms: int, ) -> None: transcript = speech_processor.process(raw) understanding = transcript.to_dict() understanding["utterance_id"] = "timed-speech-preview-matrix" service = SeatDemoService(REPOSITORY_ROOT) service.artifact_root = tmp_path / "artifacts" assert transcript.execution_eligible is True assert transcript.commands[0].parameters["duration_ms"] == duration_ms with patch.object( NativeCanExecutor, "execute", side_effect=AssertionError("dry-run speech reached the physical CAN executor"), ) as physical_execute: result = service.process( transcript.clean_text, provider="local", conversation_id="conv-timed-speech-preview-matrix", execution_mode="dry_run", vehicle_id="denza_n7", _speech_understanding=understanding, ) assert physical_execute.call_count == 0 assert result["status"] == "succeeded" assert result["execution_mode"] == "dry_run" assert result["hardware_execution_allowed"] is False assert result["provider_used"] == "speech_semantic" actions = result["intent_ir"]["spec"]["actions"] assert len(actions) == 1 assert actions[0]["capability"] == "seat.set_driver_axis_native" assert actions[0]["parameters"] == { "axis": "backrest", "direction": "backward", "duration_ms": duration_ms, } command = result["command_plans"]["native_seat"]["spec"]["commands"][0] special_parameters = json.loads(command["signals"]["special_parameters"]) assert special_parameters["duration_ms"] == duration_ms @pytest.mark.parametrize( "raw", ( "不要让主驾座椅靠背向后移动2秒,告诉我这样有什么影响", "如果主驾座椅靠背向后移动2秒会怎样", "请解释‘主驾座椅靠背向后移动2秒’是什么意思", "我们聊聊主驾座椅靠背向后移动2秒,但不要执行", ), ) def test_negation_hypothetical_and_chat_boundaries_never_become_executable( speech_processor: UnderstandingTranscriptProcessor, raw: str, ) -> None: transcript = speech_processor.process(raw) authorization = _authorize_vehicle_request(transcript.assistant_text, CONTROL_TERMS) assert transcript.execution_eligible is False assert authorization.instruction is None