feat: 完善 N7 智能座舱交互与控制链路

This commit is contained in:
vee
2026-09-07 15:34:59 +08:00
parent 32d179143f
commit c96970f068
100 changed files with 30088 additions and 1174 deletions
@@ -0,0 +1,266 @@
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