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,496 @@
from __future__ import annotations
import json
import queue
from collections.abc import Callable, Mapping
import pytest
from cognitive_agent.application.codex_assistant import (
ActiveCodexTurn,
CodexAssistantService,
)
from cognitive_agent.application.seat_service import SeatDemoService
from cognitive_agent.infrastructure.codex import vehicle_mcp_server
def _complete_turn(turn: ActiveCodexTurn, answer: str = "已安全处理。") -> None:
item_id = f"answer-{turn.turn_id}"
turn.events.put(
{
"method": "item/started",
"params": {
"item": {
"id": item_id,
"type": "agentMessage",
"phase": "final_answer",
}
},
}
)
turn.events.put(
{
"method": "item/agentMessage/delta",
"params": {"itemId": item_id, "delta": answer},
}
)
turn.events.put(
{
"method": "item/completed",
"params": {
"item": {
"id": item_id,
"type": "agentMessage",
"phase": "final_answer",
"text": answer,
}
},
}
)
turn.events.put(
{
"method": "turn/completed",
"params": {"turn": {"id": turn.turn_id, "status": "completed"}},
}
)
class _SecurityProbeClient:
available = True
status = "ready"
def __init__(self) -> None:
self.on_turn: Callable[[str, ActiveCodexTurn], None] | None = None
self.request_token = ""
def start(self, _callback_url: str, _callback_token: str) -> None:
return None
def close(self) -> None:
return None
def start_thread(
self,
*,
developer_instructions: str,
ephemeral: bool = True,
) -> str:
assert developer_instructions
assert ephemeral is True
return "thread-security-probe"
def start_turn(
self,
thread_id: str,
text: str,
request_token: str,
*,
application_context: Mapping[str, object] | None = None,
) -> ActiveCodexTurn:
del text, application_context
self.request_token = request_token
turn = ActiveCodexTurn(thread_id, "turn-security-probe", queue.Queue())
if self.on_turn is not None:
self.on_turn(request_token, turn)
_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 _stream(
assistant: CodexAssistantService,
message: str,
*,
execute_control: Callable[..., dict[str, object]] | None = None,
) -> tuple[dict[str, object], list[dict[str, object]]]:
events: list[dict[str, object]] = []
result = assistant.stream(
message,
conversation_id="conv-security-boundary",
principal="security-probe",
history=[],
emit=lambda event: events.append(event),
execute_control=execute_control
or (lambda *_args: pytest.fail("unexpected control execution")),
capability_snapshot={},
capability_context={},
control_terms=("空调",),
query_capabilities=lambda _snapshot, _query: {},
)
return result, events
def _plan(parameter_value: object) -> dict[str, object]:
return {
"detected_domains": ["roof"],
"requests": [
{
"domain": "roof",
"capability": "roof.set_transmittance",
"target_scope": "vehicle",
"operation": "set",
"duration_ms": None,
"parameters": [{"name": "transmittance_pct", "value": parameter_value}],
"reason": "用户明确指定透光率。",
}
],
"ambiguities": [],
"decision_summary": ["设置天幕透光率。"],
"confidence": 0.99,
}
def _seat_motion_plan(
capability: str,
operation: str,
parameters: Mapping[str, object],
*,
duration_ms: int | None = None,
) -> dict[str, object]:
return {
"detected_domains": ["seat"],
"requests": [
{
"domain": "seat",
"capability": capability,
"target_scope": "passenger",
"operation": operation,
"duration_ms": duration_ms,
"parameters": [
{"name": name, "value": value} for name, value in parameters.items()
],
"reason": "仅规范化用户明确指定的座椅运动。",
}
],
"ambiguities": [],
"decision_summary": ["执行用户明确指定的座椅运动。"],
"confidence": 0.99,
}
def _prime_axis(
service: SeatDemoService,
conversation_id: str,
axis: str,
position_pct: int,
) -> None:
service.conversations.apply_successful_actions(
conversation_id,
"test setup",
[
{
"action_type": "position",
"target": "passenger",
"axis": axis,
"position_pct": position_pct,
}
],
["test setup"],
)
@pytest.mark.parametrize("non_finite", [float("nan"), float("inf"), float("-inf")])
def test_mcp_rejects_non_finite_plan_parameter_before_callback(
non_finite: float,
) -> None:
callback_called = False
def callback(
_tool: str,
_arguments: Mapping[str, object],
) -> Mapping[str, object]:
nonlocal callback_called
callback_called = True
return {"status": "succeeded"}
response = vehicle_mcp_server.handle_message(
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "execute_cockpit_request",
"arguments": {
"request_token": "request-token-123",
"plan": _plan(non_finite),
},
},
},
callback,
)
assert response is not None
assert response["error"] == {
"code": -32602,
"message": "Invalid assistant plan",
}
assert callback_called is False
def test_request_token_is_not_exposed_across_mcp_progress_events() -> None:
client = _SecurityProbeClient()
assistant = CodexAssistantService(client)
def expose_split_token(request_token: str, turn: ActiveCodexTurn) -> None:
split = len(request_token) // 2
for part in (request_token[:split], request_token[split:]):
turn.events.put(
{
"method": "item/mcpToolCall/progress",
"params": {"message": part},
}
)
client.on_turn = expose_split_token
_result, events = _stream(assistant, "解释一下空调的工作原理")
serialized = json.dumps(events, ensure_ascii=False)
split = len(client.request_token) // 2
assert client.request_token[:split] not in serialized
assert client.request_token[split:] not in serialized
def test_model_controlled_summary_cannot_echo_request_token_to_browser() -> None:
client = _SecurityProbeClient()
assistant = CodexAssistantService(client)
def execute_tool(request_token: str, _turn: ActiveCodexTurn) -> None:
assistant.execute_cockpit_request(request_token)
def execute_control(
instruction: str,
_progress: Callable[[dict[str, object]], None],
) -> dict[str, object]:
assert instruction == "打开空调"
return {
"status": "succeeded",
"execution_mode": "dry_run",
"decision_summary": [f"internal={client.request_token}"],
"intent_ir": {"spec": {"actions": []}},
}
client.on_turn = execute_tool
_result, events = _stream(
assistant,
"打开空调",
execute_control=execute_control,
)
assert client.request_token not in json.dumps(events, ensure_ascii=False)
def test_safe_vehicle_result_filters_sensitive_nested_action_parameters() -> None:
safe = CodexAssistantService._safe_vehicle_result(
{
"status": "succeeded",
"execution_mode": "dry_run",
"intent_ir": {
"spec": {
"actions": [
{
"domain": "climate",
"capability": "climate.set_power",
"target": {"zone": "vehicle"},
"parameters": {
"state": "on",
"raw_can": "41 01 02 03 04 05 06 07",
"approval_token": "approval-secret",
"internal_token": "internal-secret",
},
}
]
}
},
}
)
assert safe["actions"][0]["parameters"]["state"] == "on"
serialized = json.dumps(safe, ensure_ascii=False)
assert "raw_can" not in serialized
assert "approval-secret" not in serialized
assert "internal-secret" not in serialized
@pytest.mark.parametrize(
(
"text",
"capability",
"axis",
"correct_operation",
"reversed_operation",
"parameters",
"duration_ms",
"initial_position",
),
[
(
"副驾座椅向前移动20毫米",
"seat.set_longitudinal_position",
"longitudinal",
"decrease",
"increase",
{"distance_mm": 20},
None,
50,
),
(
"副驾座椅向后移动20毫米",
"seat.set_longitudinal_position",
"longitudinal",
"increase",
"decrease",
{"distance_mm": 20},
None,
50,
),
(
"把副驾坐垫前缘抬高2秒",
"seat.set_cushion_tilt",
"cushion_tilt",
"decrease",
"increase",
{},
2_000,
50,
),
(
"把副驾坐垫前缘降低2秒",
"seat.set_cushion_tilt",
"cushion_tilt",
"increase",
"decrease",
{},
2_000,
50,
),
(
"副驾靠背向后放倒10度",
"seat.set_backrest_recline",
"backrest_recline",
"increase",
"decrease",
{"angle_delta_deg": 10},
None,
50,
),
(
"副驾靠背向前调直10度",
"seat.set_backrest_recline",
"backrest_recline",
"decrease",
"increase",
{"angle_delta_deg": 10},
None,
50,
),
(
"请使副驾腿托角度向上调整10度",
"seat.set_legrest_height",
"legrest_height",
"increase",
"decrease",
{"angle_delta_deg": 10},
None,
50,
),
(
"请使副驾腿托角度向下调整10度",
"seat.set_legrest_height",
"legrest_height",
"decrease",
"increase",
{"angle_delta_deg": 10},
None,
50,
),
(
"请使副驾腿托伸长到65%",
"seat.set_legrest_extension",
"legrest_extension",
"increase",
"decrease",
{"position_pct": 65},
None,
50,
),
(
"请使副驾腿托缩回到35%",
"seat.set_legrest_extension",
"legrest_extension",
"decrease",
"increase",
{"position_pct": 35},
None,
50,
),
],
)
def test_each_seat_axis_binds_physical_words_to_its_normalized_direction(
tmp_path,
text: str,
capability: str,
axis: str,
correct_operation: str,
reversed_operation: str,
parameters: Mapping[str, object],
duration_ms: int | None,
initial_position: int,
) -> None:
service = SeatDemoService()
service.artifact_root = tmp_path
slug = capability.rsplit(".", 1)[-1].replace("_", "-")
correct_conversation = f"conv-direction-correct-{slug}"
reversed_conversation = f"conv-direction-reversed-{slug}"
_prime_axis(service, correct_conversation, axis, initial_position)
_prime_axis(service, reversed_conversation, axis, initial_position)
try:
reversed_result = service.preview_native_can(
text,
provider="local",
execution_mode="dry_run",
conversation_id=reversed_conversation,
assistant_plan=_seat_motion_plan(
capability,
reversed_operation,
parameters,
duration_ms=duration_ms,
),
)
correct_result = service.preview_native_can(
text,
provider="local",
execution_mode="dry_run",
conversation_id=correct_conversation,
assistant_plan=_seat_motion_plan(
capability,
correct_operation,
parameters,
duration_ms=duration_ms,
),
)
finally:
service.close()
assert reversed_result["status"] == "no_action"
assert any("安全授权已过滤" in summary for summary in reversed_result["decision_summary"])
assert correct_result["status"] == "succeeded"
assert correct_result["provider_used"] == "local_rules+codex_app_server"
actions = correct_result["intent_ir"]["spec"]["actions"]
assert len(actions) == 1
action = actions[0]
assert action["capability"] == capability
assert action["resolution"]["direction"] == correct_operation
final_position = action["parameters"]["position_pct"]
if correct_operation == "increase":
assert final_position > initial_position
else:
assert final_position < initial_position