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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+256 -4
View File
@@ -5,6 +5,7 @@ import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
from cognitive_agent.application.codex_assistant import CodexAssistantService
from cognitive_agent.application.domain_runtime import ConfiguredDomainRuntime
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
@@ -791,6 +792,12 @@ operations:
},
},
)
saved_public_mode = next(
mode
for mode in self.service.mode_profiles.public_modes("denza_n7")
if mode["id"] == "nap"
)
self.assertEqual(saved_public_mode["behavior"], "cyclic")
with patch.object(
self.service,
@@ -824,7 +831,7 @@ operations:
}
self.assertIn("roof.set_shade_motion", replayed_capabilities)
def test_build_named_mode_executes_and_persists_in_one_turn(self) -> None:
def test_build_named_mode_returns_draft_before_explicit_save(self) -> None:
mode_root = Path(self.temporary_directory.name) / "modes"
self.service.mode_profiles = ModeProfileRegistry(mode_root)
model_output = {
@@ -864,9 +871,41 @@ operations:
)
self.assertEqual(built["status"], "succeeded")
self.assertEqual(built["mode_profile"]["name"], "K歌模式")
self.assertIsNone(built["mode_profile"])
self.assertEqual(
built["orchestration_route"], "mode_profile_create_preview"
)
draft = built["mode_profile_draft"]
self.assertEqual(draft["name"], "K歌模式")
self.assertEqual(draft["operation_count"], 2)
self.assertEqual(set(draft["domains"]), {"ambient_light", "climate"})
self.assertEqual(built["mode_creation"]["source_status"], "succeeded")
self.assertEqual(built["mode_creation"]["operation_count"], 2)
self.assertNotIn(
"K歌模式",
{
mode["name"]
for mode in self.service.runtime_info()["modes"]
},
)
self.assertEqual(
self.service.mode_profiles.match_text(
"进入K歌模式", vehicle_id="denza_n7"
),
[],
)
saved = self.service.create_mode_profile_card(
draft["name"],
draft["instruction"],
draft["source_request_id"],
draft["vehicle_id"],
)
self.assertEqual(saved["status"], "succeeded")
self.assertEqual(saved["mode_profile"]["name"], "K歌模式")
self.assertEqual(saved["mode_profile"]["operation_count"], 2)
self.assertTrue(saved["mode_profile"]["command_flow"]["plans"])
self.assertIn(
"K歌模式",
{
@@ -878,7 +917,7 @@ operations:
"进入K歌模式", vehicle_id="denza_n7"
)
self.assertEqual([profile.name for profile in matched], ["K歌模式"])
self.assertTrue(Path(built["mode_profile"]["source"]).is_file())
self.assertTrue(Path(matched[0].source).is_file())
def test_build_named_mode_is_always_a_dry_run_configuration_workflow(self) -> None:
mode_root = Path(self.temporary_directory.name) / "hardware-modes"
@@ -916,7 +955,208 @@ operations:
self.assertEqual(built["status"], "succeeded")
self.assertEqual(built["execution_mode"], "dry_run")
self.assertFalse(built["hardware_execution_allowed"])
self.assertEqual(built["mode_profile"]["name"], "K歌模式")
self.assertIsNone(built["mode_profile"])
self.assertEqual(built["mode_profile_draft"]["name"], "K歌模式")
self.assertEqual(
built["orchestration_route"], "mode_profile_create_preview"
)
self.assertNotIn(
"K歌模式",
{
mode["name"]
for mode in self.service.runtime_info()["modes"]
},
)
def test_build_named_mode_uses_the_user_supplied_scene_requirements(self) -> None:
mode_root = Path(self.temporary_directory.name) / "described-modes"
self.service.mode_profiles = ModeProfileRegistry(mode_root)
model_output = {
"detected_domains": ["climate"],
"requests": [
{
"domain": "climate",
"capability": "climate.set_temperature",
"target_scope": "vehicle",
"operation": "set",
"parameters": [{"name": "temperature_c", "value": 24}],
"reason": "保持助眠温度。",
}
],
"ambiguities": [],
"decision_summary": ["已按用户描述构建助眠场景。"],
"confidence": 0.95,
}
with patch.object(
self.service.openai,
"interpret",
return_value=model_output,
) as interpret:
built = self.service.process(
"创建宝宝睡眠模式:营造安静的助眠氛围",
"openai",
"conv-build-described-mode",
vehicle_id="denza_n7",
)
self.assertEqual(built["status"], "succeeded")
self.assertIsNone(built["mode_profile"])
self.assertEqual(built["mode_profile_draft"]["name"], "宝宝睡眠模式")
self.assertEqual(
built["mode_profile_draft"]["instruction"], "营造安静的助眠氛围"
)
self.assertEqual(
built["orchestration_route"], "mode_profile_create_preview"
)
self.assertEqual(interpret.call_args.args[0], "营造安静的助眠氛围")
def test_movie_mode_preview_and_confirmed_save_keep_all_nine_actions(
self,
) -> None:
user_root = Path(self.temporary_directory.name) / "movie-mode-user"
self.service.mode_profiles = ModeProfileRegistry(
REPOSITORY_ROOT / "config" / "modes",
user_root=user_root,
)
utterance = (
"构建一个观影模式,遮阳帘关闭,车窗关闭,"
"副驾座椅调整为零重力模式,灯光全部关闭"
)
built = self.service.preview_native_can(
utterance,
provider="local",
conversation_id="conv-build-movie-mode",
vehicle_id="denza_n7",
)
self.assertEqual(built["status"], "succeeded")
self.assertEqual(
built["orchestration_route"], "mode_profile_create_preview"
)
self.assertEqual(built["unresolved_clauses"], [])
self.assertIsNone(built["mode_profile"])
draft = built["mode_profile_draft"]
self.assertEqual(draft["name"], "观影模式")
self.assertEqual(draft["operation_count"], 9)
self.assertEqual(
set(draft["domains"]),
{"seat", "roof", "window", "interior_light", "ambient_light"},
)
self.assertNotIn(
"观影模式",
{mode["name"] for mode in self.service.runtime_info()["modes"]},
)
intent_actions = built["intent_ir"]["spec"]["actions"]
capabilities = [action["capability"] for action in intent_actions]
self.assertEqual(len(capabilities), 9)
self.assertEqual(
set(capabilities),
{
"seat.set_longitudinal_position",
"seat.set_cushion_tilt",
"seat.set_backrest_recline",
"seat.set_legrest_height",
"roof.set_shade_motion",
"window.set_motion",
"interior_light.set_indoor_power",
"interior_light.set_reading_power",
"ambient_light.set_power",
},
)
window_action = next(
action
for action in intent_actions
if action["capability"] == "window.set_motion"
)
self.assertEqual(window_action["target"]["zone"], "front_left")
saved = self.service.create_mode_profile_card(
draft["name"],
draft["instruction"],
draft["source_request_id"],
draft["vehicle_id"],
)
self.assertEqual(saved["status"], "succeeded")
profile = saved["mode_profile"]
self.assertEqual(profile["name"], "观影模式")
self.assertEqual(profile["origin"], "user")
self.assertEqual(profile["operation_count"], 9)
self.assertEqual(
sum(
len(config["spec"]["operations"])
for config in profile["command_flow"]["function_configs"].values()
),
9,
)
self.assertEqual(
set(profile["command_flow"]["plans"]),
set(profile["command_flow"]["function_configs"]),
)
persisted = self.service.mode_profiles.get_profile(
profile["id"], "denza_n7"
)
self.assertTrue(Path(persisted.source).is_file())
executed = self.service.execute_mode_profile_card(
profile["id"],
conversation_id="conv-execute-movie-mode",
execution_mode="dry_run",
vehicle_id="denza_n7",
)
self.assertEqual(executed["status"], "succeeded")
self.assertEqual(executed["orchestration_route"], "saved_mode_command_flow")
self.assertEqual(len(executed["execution_report"]["spec"]["results"]), 9)
def test_movie_mode_assistant_route_uses_real_builder_without_codex(self) -> None:
user_root = Path(self.temporary_directory.name) / "assistant-movie-mode-user"
self.service.mode_profiles = ModeProfileRegistry(
REPOSITORY_ROOT / "config" / "modes",
user_root=user_root,
)
utterance = (
"构建一个观影模式,遮阳帘关闭,车窗关闭,"
"副驾座椅调整为零重力模式,灯光全部关闭"
)
codex_client = MagicMock()
codex_client.available = False
assistant = CodexAssistantService(codex_client)
events: list[dict[str, object]] = []
def execute_control(instruction: str, progress: object) -> dict[str, object]:
callback = progress if callable(progress) else None
return self.service.preview_native_can(
instruction,
provider="local",
conversation_id="conv-assistant-movie-mode",
execution_mode="hardware",
vehicle_id="denza_n7",
on_commands_validated=callback,
)
result = assistant.stream(
utterance,
conversation_id="conv-assistant-movie-mode",
principal="integration-test",
history=[],
emit=lambda event: events.append(event) or True,
execute_control=execute_control,
)
self.assertEqual(result["route"], "vehicle_control")
self.assertTrue(result["success"])
self.assertEqual(result["result"]["execution_mode"], "dry_run")
self.assertEqual(result["result"]["mode_profile_draft"]["name"], "观影模式")
self.assertEqual(result["result"]["mode_profile_draft"]["operation_count"], 9)
self.assertEqual(len(result["result"]["actions"]), 9)
self.assertEqual(events[0]["engine"], "n7web-mode-builder")
self.assertEqual(events[-1]["route"], "vehicle_control")
self.assertIn("观影模式", str(events[-1]["answer"]))
self.assertFalse(any(user_root.glob("*.yaml")))
codex_client.start_turn.assert_not_called()
def test_create_mode_language_does_not_match_ordinary_mode_execution(self) -> None:
self.assertEqual(
@@ -927,6 +1167,18 @@ operations:
self.service._parse_mode_profile_create_name("新增冥想空间模式"),
"冥想空间模式",
)
self.assertEqual(
self.service._parse_mode_profile_create_name(
"创建宝宝睡眠模式:副驾半躺、空调24度、关闭阅读灯"
),
"宝宝睡眠模式",
)
self.assertEqual(
self.service._parse_mode_profile_create_name(
"请帮我创建一个 宝宝睡眠模式:副驾半躺"
),
"宝宝睡眠模式",
)
self.assertIsNone(
self.service._parse_mode_profile_create_name("进入K歌模式")
)
@@ -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
@@ -0,0 +1,427 @@
from __future__ import annotations
import json
import os
import queue
import subprocess
import sys
import threading
from collections.abc import Callable, Mapping
from http.server import ThreadingHTTPServer
from pathlib import Path
from typing import Any, Self
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
from cognitive_agent.application.codex_assistant import ActiveCodexTurn, CodexAssistantService
from cognitive_agent.application.seat_service import SeatDemoService
from cognitive_agent.auth import AuthConfig, AuthRuntime
from cognitive_agent.server import SeatDemoHandler, VehicleDisplaySync
ROOT = Path(__file__).resolve().parents[2]
ORIGINAL_INSTRUCTION = "打开遮阳帘"
WRONG_REQUEST_TOKEN = "mcp-loopback-wrong-token-456"
_EOF = object()
SHADE_PLAN: dict[str, object] = {
"detected_domains": ["roof"],
"requests": [
{
"domain": "roof",
"capability": "roof.set_shade_motion",
"target_scope": "vehicle",
"operation": "set",
"duration_ms": None,
"parameters": [{"name": "motion", "value": "open"}],
"reason": "用户明确要求打开遮阳帘。",
}
],
"ambiguities": [],
"decision_summary": ["打开遮阳帘。"],
"confidence": 0.99,
}
class _StdioMcpClient:
"""Drive the real MCP child process without platform-specific pipe polling."""
def __init__(self, callback_url: str, callback_token: str) -> None:
environment = os.environ.copy()
python_paths = [
ROOT / "src",
ROOT / "domains" / "_template" / "src",
ROOT / "domains" / "ambient_light" / "src",
ROOT / "domains" / "climate" / "src",
ROOT / "domains" / "roof" / "src",
ROOT / "domains" / "seat" / "src",
]
inherited_python_path = environment.get("PYTHONPATH", "").strip()
environment["PYTHONPATH"] = os.pathsep.join(
[
*(str(path) for path in python_paths),
*([inherited_python_path] if inherited_python_path else []),
]
)
environment["COGNITIVE_AGENT_CODEX_VEHICLE_CALLBACK_URL"] = callback_url
environment["COGNITIVE_AGENT_CODEX_VEHICLE_CALLBACK_TOKEN"] = callback_token
environment["COGNITIVE_AGENT_CODEX_VEHICLE_CALLBACK_TIMEOUT_SECONDS"] = "5"
# MCP is a UTF-8 JSON-lines protocol. Force the same pipe encoding on
# Windows (whose inherited console encoding may otherwise be GBK).
environment["PYTHONUTF8"] = "1"
environment["PYTHONIOENCODING"] = "utf-8"
self._responses: queue.Queue[object] = queue.Queue()
self._stderr: list[str] = []
self._next_id = 0
self.process = subprocess.Popen(
[
sys.executable,
"-u",
"-m",
"cognitive_agent.infrastructure.codex.vehicle_mcp_server",
],
cwd=ROOT,
env=environment,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="strict",
bufsize=1,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
self._stdout_thread = threading.Thread(target=self._read_stdout, daemon=True)
self._stderr_thread = threading.Thread(target=self._read_stderr, daemon=True)
self._stdout_thread.start()
self._stderr_thread.start()
def _read_stdout(self) -> None:
assert self.process.stdout is not None
try:
for line in self.process.stdout:
self._responses.put(line)
finally:
self._responses.put(_EOF)
def _read_stderr(self) -> None:
assert self.process.stderr is not None
self._stderr.extend(self.process.stderr)
def request(self, method: str, params: Mapping[str, object]) -> dict[str, Any]:
self._next_id += 1
request_id = self._next_id
self._write(
{
"jsonrpc": "2.0",
"id": request_id,
"method": method,
"params": dict(params),
}
)
try:
raw = self._responses.get(timeout=10)
except queue.Empty as exc:
raise AssertionError(
f"MCP response timed out; stderr={''.join(self._stderr)!r}"
) from exc
if raw is _EOF:
raise AssertionError(
f"MCP process exited before responding; stderr={''.join(self._stderr)!r}"
)
response = json.loads(str(raw))
assert response["jsonrpc"] == "2.0"
assert response["id"] == request_id
return response
def notify(self, method: str, params: Mapping[str, object]) -> None:
self._write({"jsonrpc": "2.0", "method": method, "params": dict(params)})
def _write(self, message: Mapping[str, object]) -> None:
assert self.process.stdin is not None
assert self.process.poll() is None, f"MCP child exited: {''.join(self._stderr)}"
self.process.stdin.write(
json.dumps(message, ensure_ascii=False, separators=(",", ":")) + "\n"
)
self.process.stdin.flush()
def close(self) -> None:
if self.process.stdin is not None and not self.process.stdin.closed:
self.process.stdin.close()
try:
return_code = self.process.wait(timeout=5)
except subprocess.TimeoutExpired:
self.process.terminate()
try:
return_code = self.process.wait(timeout=3)
except subprocess.TimeoutExpired:
self.process.kill()
return_code = self.process.wait(timeout=3)
self._stdout_thread.join(timeout=2)
self._stderr_thread.join(timeout=2)
if self.process.stdout is not None:
self.process.stdout.close()
if self.process.stderr is not None:
self.process.stderr.close()
assert return_code == 0, (
f"MCP child exited with {return_code}; stderr={''.join(self._stderr)!r}"
)
def __enter__(self) -> Self:
return self
def __exit__(self, *_args: object) -> None:
self.close()
def _complete_turn(turn: ActiveCodexTurn) -> None:
item_id = f"answer-{turn.turn_id}"
answer = "已完成遮阳帘模拟编排。"
turn.events.put(
{
"method": "item/completed",
"params": {
"item": {
"type": "agentMessage",
"id": item_id,
"text": answer,
"phase": "final_answer",
}
},
}
)
turn.events.put(
{
"method": "turn/completed",
"params": {
"turn": {
"id": turn.turn_id,
"status": "completed",
"items": [],
"error": None,
}
},
}
)
class _McpDrivingCodexClient:
"""Minimal model-side driver; all vehicle work still crosses the real MCP process."""
available = True
def __init__(self) -> None:
self.rpc: _StdioMcpClient | None = None
self.started_with: tuple[str, str] | None = None
self.capability_response: dict[str, Any] | None = None
self.wrong_token_response: dict[str, Any] | None = None
self.execution_response: dict[str, Any] | None = None
self.active_request_token = ""
@property
def status(self) -> str:
return "ready"
def start(self, callback_url: str, callback_token: str) -> None:
self.started_with = (callback_url, callback_token)
def close(self) -> None:
return None
def start_thread(self, *, developer_instructions: str, ephemeral: bool = True) -> str:
assert "get_n7web_capabilities" in developer_instructions
assert "execute_cockpit_request" in developer_instructions
assert ephemeral is True
return "thread-real-mcp-loopback"
def start_turn(
self,
thread_id: str,
text: str,
request_token: str,
*,
application_context: Mapping[str, object] | None = None,
) -> ActiveCodexTurn:
assert self.rpc is not None
assert thread_id == "thread-real-mcp-loopback"
assert text == ORIGINAL_INSTRUCTION
assert request_token != WRONG_REQUEST_TOKEN
assert len(request_token) >= 8
assert application_context
self.active_request_token = request_token
turn = ActiveCodexTurn(
thread_id=thread_id,
turn_id="turn-real-mcp-loopback",
events=queue.Queue(),
)
self.capability_response = self.rpc.request(
"tools/call",
{
"name": "get_n7web_capabilities",
"arguments": {"request_token": request_token, "query": "遮阳帘"},
},
)
self.wrong_token_response = self.rpc.request(
"tools/call",
{
"name": "execute_cockpit_request",
"arguments": {
"request_token": WRONG_REQUEST_TOKEN,
"plan": SHADE_PLAN,
},
},
)
self.execution_response = self.rpc.request(
"tools/call",
{
"name": "execute_cockpit_request",
# No instruction crosses MCP. The application must recover the
# exact original text from this turn-bound opaque token.
"arguments": {"request_token": request_token, "plan": SHADE_PLAN},
},
)
_complete_turn(turn)
return turn
def finish_turn(self, _turn: ActiveCodexTurn) -> None:
return None
def interrupt(self, _turn: ActiveCodexTurn) -> None:
return None
def unsubscribe_thread(self, _thread_id: str) -> None:
return None
def _auth_disabled() -> AuthRuntime:
return AuthRuntime(AuthConfig(False, "", "", b"", 3600, 5, 300, 120, False))
def test_real_mcp_stdio_process_reaches_bound_loopback_dry_run(tmp_path: Path) -> None:
service = SeatDemoService(ROOT)
service.artifact_root = tmp_path / "runs"
catalog = AssistantCapabilityCatalog(
service,
peripheral_provider=list,
nfc_provider=lambda: {"reader": {"state": "ready"}, "card": None},
)
client = _McpDrivingCodexClient()
assistant = CodexAssistantService(client)
handler = type("RealMcpLoopbackHandler", (SeatDemoHandler,), {})
handler.service = service
handler.assistant = assistant
handler.auth = _auth_disabled()
handler.vehicle_display_sync = VehicleDisplaySync()
handler.assistant_capability_catalog = catalog
handler.log_message = lambda *_args: None
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
server_thread.start()
callback_url = (
f"http://127.0.0.1:{server.server_port}/api/internal/codex/vehicle-control"
)
raw_results: list[dict[str, object]] = []
observed_instructions: list[str] = []
def execute_control(
instruction: str,
progress: Callable[[dict[str, object]], None],
assistant_plan: Mapping[str, object],
) -> dict[str, object]:
observed_instructions.append(instruction)
result = service.preview_native_can(
instruction,
provider="local",
conversation_id="conv-real-mcp-vehicle",
execution_mode="dry_run",
on_commands_validated=progress,
assistant_plan=dict(assistant_plan),
)
raw_results.append(result)
return result
try:
assistant.start(callback_url)
assert client.started_with == (callback_url, assistant.internal_token)
with _StdioMcpClient(callback_url, assistant.internal_token) as rpc:
client.rpc = rpc
initialized = rpc.request(
"initialize",
{
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {"name": "n7web-integration-test", "version": "1.0"},
},
)
assert initialized["result"]["protocolVersion"] == "2025-06-18"
assert initialized["result"]["serverInfo"]["name"] == "n7web-cockpit"
rpc.notify("notifications/initialized", {})
listed = rpc.request("tools/list", {})
tools = listed["result"]["tools"]
assert [tool["name"] for tool in tools] == [
"get_n7web_capabilities",
"execute_cockpit_request",
]
execute_schema = tools[1]["inputSchema"]["properties"]
assert set(execute_schema) == {"request_token", "plan"}
assert "instruction" not in execute_schema
snapshot = catalog.snapshot()
result = assistant.stream(
ORIGINAL_INSTRUCTION,
conversation_id="conv-real-mcp-assistant",
principal="real-mcp-integration-test",
history=[],
emit=lambda _event: True,
execute_control=execute_control,
capability_snapshot=snapshot,
capability_context=catalog.compact_context(snapshot),
control_terms=catalog.control_terms(snapshot),
query_capabilities=catalog.query,
)
assert result["route"] == "agent"
assert client.capability_response is not None
capability_result = client.capability_response["result"]
assert capability_result["isError"] is False
capability_catalog = capability_result["structuredContent"]["catalog"]
assert capability_catalog["matched"] is True
assert "roof.set_shade_motion" in {
item["id"] for item in capability_catalog["capabilities"]
}
assert client.wrong_token_response is not None
rejected = client.wrong_token_response["result"]
assert rejected["isError"] is True
assert rejected["structuredContent"]["error"]["code"] == "n7web_tool_failed"
assert "HTTP 409" in rejected["structuredContent"]["error"]["message"]
assert client.execution_response is not None
execution = client.execution_response["result"]
assert execution["isError"] is False
structured = execution["structuredContent"]
assert structured["status"] == "succeeded"
assert structured["execution_mode"] == "dry_run"
assert structured["verified"] is False
assert structured["verification_status"] == "simulated"
assert structured["utterance"] == ORIGINAL_INSTRUCTION
assert {action["capability"] for action in structured["actions"]} == {
"roof.set_shade_motion"
}
assert json.loads(execution["content"][0]["text"]) == structured
assert observed_instructions == [ORIGINAL_INSTRUCTION]
assert len(raw_results) == 1
assert raw_results[0]["utterance"] == ORIGINAL_INSTRUCTION
assert raw_results[0]["execution_mode"] == "dry_run"
assert raw_results[0]["status"] == "succeeded"
assert rpc.process.poll() == 0
finally:
assistant.close()
server.shutdown()
server.server_close()
server_thread.join(timeout=3)
service.close()
assert not server_thread.is_alive()