Files
Cognitive_Agent/tests/integration/test_speech_control_preview_matrix.py
T

267 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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