feat: integrate learned peripherals into mode creation
Expose safe NFC-bound infrared actions to semantic scene composition, require flashing-light actions for romantic and Mid-Autumn modes, and preserve mixed CAN/IR mode flows. Fix NFC device reassignment and retry extension/toy MQTT publication when an active card's binding catalog changes.
This commit is contained in:
@@ -2,13 +2,21 @@ from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
|
||||
from cognitive_agent.application.codex_assistant import CodexAssistantService
|
||||
from cognitive_agent.application.mode_profile import ModeProfileRegistry
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
||||
from cognitive_agent.application.self_extension import (
|
||||
SelfExtensionService,
|
||||
SimulationGateway,
|
||||
)
|
||||
from cognitive_agent.infrastructure.codex.app_server import CodexAppServerClient
|
||||
from cognitive_agent.server import SeatDemoHandler, VehicleDisplaySync
|
||||
|
||||
FAKE_SERVER = REPOSITORY_ROOT / "tests" / "fixtures" / "fake_codex_app_server.py"
|
||||
MCP_SERVER = (
|
||||
@@ -17,8 +25,18 @@ MCP_SERVER = (
|
||||
CALLBACK_URL = "http://127.0.0.1:8765/api/internal/codex/vehicle-control"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("message", "expected_name"),
|
||||
(
|
||||
("构建一个星夜模式", "星夜模式"),
|
||||
("创建学习模式", "学习模式"),
|
||||
("创建中秋欢快模式", "中秋欢快模式"),
|
||||
),
|
||||
)
|
||||
def test_structured_scene_turn_reaches_the_complete_local_dry_run_pipeline(
|
||||
tmp_path: Path,
|
||||
message: str,
|
||||
expected_name: str,
|
||||
) -> None:
|
||||
service = SeatDemoService(REPOSITORY_ROOT)
|
||||
service.artifact_root = tmp_path / "runs"
|
||||
@@ -65,7 +83,7 @@ def test_structured_scene_turn_reaches_the_complete_local_dry_run_pipeline(
|
||||
side_effect=AssertionError("scene preview must never reserve hardware"),
|
||||
):
|
||||
response = assistant.stream(
|
||||
"构建一个星夜模式",
|
||||
message,
|
||||
conversation_id="conv-scene-e2e-pipeline",
|
||||
principal="scene-e2e-user",
|
||||
history=[],
|
||||
@@ -87,7 +105,7 @@ def test_structured_scene_turn_reaches_the_complete_local_dry_run_pipeline(
|
||||
assert result["execution_mode"] == "dry_run"
|
||||
assert result["hardware_execution_allowed"] is False
|
||||
assert result["orchestration_route"] == "mode_profile_create_preview"
|
||||
assert draft["name"] == "星夜模式"
|
||||
assert draft["name"] == expected_name
|
||||
assert draft["generation_source"] == "codex_scene_composition"
|
||||
assert draft["operation_count"] == 2
|
||||
capabilities = {
|
||||
@@ -100,3 +118,122 @@ def test_structured_scene_turn_reaches_the_complete_local_dry_run_pipeline(
|
||||
assert not list((tmp_path / "user-modes").rglob("*.yaml"))
|
||||
assert any(event.get("type") == "action.completed" for event in events)
|
||||
assert all("request_token" not in str(event) for event in events)
|
||||
|
||||
|
||||
def test_mid_autumn_cloud_scene_requires_authorized_flashing_light(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service = SeatDemoService(REPOSITORY_ROOT)
|
||||
service.artifact_root = tmp_path / "runs"
|
||||
service.mode_profiles = ModeProfileRegistry(
|
||||
REPOSITORY_ROOT / "config" / "modes",
|
||||
user_root=tmp_path / "user-modes",
|
||||
)
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=tmp_path / "infrared.sqlite3",
|
||||
)
|
||||
extension.sync_peripheral_catalog(
|
||||
[
|
||||
{
|
||||
"id": "ir-slot:flashing-light:2",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "flashing-light",
|
||||
"device_name": "闪烁灯",
|
||||
"object_names": ["闪烁灯", "闪光灯"],
|
||||
"card_uid": "E113B203",
|
||||
"action": "打开",
|
||||
"action_names": ["打开", "开启", "启动"],
|
||||
"risk": "low",
|
||||
"slot": 2,
|
||||
},
|
||||
{
|
||||
"id": "ir-slot:flashing-light:3",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "flashing-light",
|
||||
"device_name": "闪烁灯",
|
||||
"object_names": ["闪烁灯", "闪光灯"],
|
||||
"card_uid": "E113B203",
|
||||
"action": "切换为红色",
|
||||
"action_names": ["切换为红色", "设置为红色"],
|
||||
"risk": "low",
|
||||
"slot": 3,
|
||||
},
|
||||
]
|
||||
)
|
||||
handler = object.__new__(SeatDemoHandler)
|
||||
handler.service = service
|
||||
handler.extension = extension
|
||||
handler.nfc = SimpleNamespace(snapshot=lambda: {"card": None})
|
||||
handler.vehicle_display_sync = VehicleDisplaySync()
|
||||
catalog = AssistantCapabilityCatalog(
|
||||
service,
|
||||
peripheral_provider=extension.list_peripheral_routing_capabilities,
|
||||
)
|
||||
snapshot = catalog.snapshot()
|
||||
client = CodexAppServerClient(
|
||||
cwd=tmp_path / "codex-workspace",
|
||||
codex_command=[sys.executable, "-u", str(FAKE_SERVER)],
|
||||
model="gpt-test-fast",
|
||||
reasoning_effort="low",
|
||||
request_timeout=2,
|
||||
startup_timeout=2,
|
||||
environment={"OPENAI_BASE_URL": "https://api.openai.com/v1"},
|
||||
mcp_server_path=MCP_SERVER,
|
||||
)
|
||||
assistant = CodexAssistantService(client)
|
||||
|
||||
def execute_control(
|
||||
instruction: str,
|
||||
progress: object,
|
||||
assistant_plan: object,
|
||||
) -> dict[str, object]:
|
||||
assert isinstance(assistant_plan, dict)
|
||||
result = handler._execute_request_with_peripherals(
|
||||
"/api/requests/preview",
|
||||
{
|
||||
"utterance": instruction,
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-mid-autumn-flashing-light",
|
||||
"execution_mode": "hardware",
|
||||
"vehicle_id": "denza_n7",
|
||||
"simulation_fault": "normal",
|
||||
"hardware_access_bypass": True,
|
||||
"control_transport": "wired_can",
|
||||
"assistant_plan": assistant_plan,
|
||||
},
|
||||
on_commands_validated=progress,
|
||||
)
|
||||
assert result is not None
|
||||
return result
|
||||
|
||||
try:
|
||||
assistant.start(CALLBACK_URL)
|
||||
with patch.object(
|
||||
extension,
|
||||
"execute_gateway",
|
||||
side_effect=AssertionError("mode creation must not emit infrared"),
|
||||
):
|
||||
response = assistant.stream(
|
||||
"创建中秋欢快模式",
|
||||
conversation_id="conv-mid-autumn-flashing-light",
|
||||
principal="mid-autumn-e2e-user",
|
||||
history=[],
|
||||
emit=lambda _event: True,
|
||||
execute_control=execute_control,
|
||||
capability_snapshot=snapshot,
|
||||
capability_context=catalog.compact_context(snapshot),
|
||||
scene_capability_context=catalog.scene_composition_context(snapshot),
|
||||
control_terms=catalog.control_terms(snapshot),
|
||||
)
|
||||
finally:
|
||||
assistant.close()
|
||||
|
||||
result = response["result"]
|
||||
assert response["success"] is True
|
||||
assert result["status"] == "succeeded"
|
||||
assert result["execution_mode"] == "dry_run"
|
||||
assert [item["slot"] for item in result["infrared_actions"]] == [2, 3]
|
||||
assert result["mode_profile_draft"]["domains"][-1] == "self_extension"
|
||||
|
||||
Reference in New Issue
Block a user