fix: compose named cockpit modes from capabilities
This commit is contained in:
@@ -178,6 +178,7 @@ class FakeVehicleService:
|
||||
def __init__(self) -> None:
|
||||
self.calls: list[dict[str, object]] = []
|
||||
self.mode_save_calls: list[dict[str, object]] = []
|
||||
self.mode_trial_calls: list[dict[str, object]] = []
|
||||
self.fail = False
|
||||
|
||||
def preview_native_can(self, **kwargs: object) -> dict[str, object]:
|
||||
@@ -286,6 +287,7 @@ class FakeVehicleService:
|
||||
"conversation_id": kwargs["conversation_id"],
|
||||
"status": "succeeded",
|
||||
"execution_mode": "dry_run",
|
||||
"hardware_execution_allowed": False,
|
||||
"supplement_required": False,
|
||||
"orchestration_route": "mode_profile_create_preview",
|
||||
"mode_profile_draft": {
|
||||
@@ -361,6 +363,31 @@ class FakeVehicleService:
|
||||
"source_request_id": source_request_id,
|
||||
}
|
||||
|
||||
def trial_mode_profile_draft(
|
||||
self,
|
||||
name: str,
|
||||
instruction: str,
|
||||
source_request_id: str,
|
||||
vehicle_id: str | None = None,
|
||||
) -> dict[str, object]:
|
||||
self.mode_trial_calls.append(
|
||||
{
|
||||
"name": name,
|
||||
"instruction": instruction,
|
||||
"source_request_id": source_request_id,
|
||||
"vehicle_id": vehicle_id,
|
||||
}
|
||||
)
|
||||
return {
|
||||
"request_id": source_request_id,
|
||||
"status": "succeeded",
|
||||
"execution_mode": "dry_run",
|
||||
"hardware_execution_allowed": False,
|
||||
"orchestration_route": "mode_profile_draft_trial",
|
||||
"intent_ir": {"spec": {"actions": []}},
|
||||
"function_configs": {},
|
||||
}
|
||||
|
||||
|
||||
def _auth_disabled() -> AuthRuntime:
|
||||
return AuthRuntime(AuthConfig(False, "", "", b"", 3600, 5, 300, 120, False))
|
||||
@@ -402,6 +429,7 @@ class TestCodexAssistantWebApi:
|
||||
def setup_method(self) -> None:
|
||||
AssistantApiHandler.service.calls.clear()
|
||||
AssistantApiHandler.service.mode_save_calls.clear()
|
||||
AssistantApiHandler.service.mode_trial_calls.clear()
|
||||
AssistantApiHandler.service.fail = False
|
||||
FAKE_CODEX.available = True
|
||||
FAKE_CODEX.turn_calls.clear()
|
||||
@@ -636,6 +664,32 @@ class TestCodexAssistantWebApi:
|
||||
assert "预览" in events[-1]["answer"]
|
||||
assert "已添加" not in events[-1]["answer"]
|
||||
|
||||
def test_mode_draft_trial_reuses_the_server_owned_preview(self) -> None:
|
||||
status, content_type, payload = self._request_json(
|
||||
"/api/mode-profile-drafts/trial",
|
||||
{
|
||||
"name": "午休模式",
|
||||
"instruction": "进入午休模式",
|
||||
"source_request_id": "req-api-mode-trial",
|
||||
"vehicle_id": "denza_n7",
|
||||
},
|
||||
)
|
||||
|
||||
assert status == 200
|
||||
assert content_type == "application/json"
|
||||
assert payload["status"] == "succeeded"
|
||||
assert payload["execution_mode"] == "dry_run"
|
||||
assert payload["hardware_execution_allowed"] is False
|
||||
assert payload["orchestration_route"] == "mode_profile_draft_trial"
|
||||
assert AssistantApiHandler.service.mode_trial_calls == [
|
||||
{
|
||||
"name": "午休模式",
|
||||
"instruction": "进入午休模式",
|
||||
"source_request_id": "req-api-mode-trial",
|
||||
"vehicle_id": "denza_n7",
|
||||
}
|
||||
]
|
||||
|
||||
def test_exact_movie_mode_request_streams_a_nine_action_preview(self) -> None:
|
||||
events = self.post_assistant(
|
||||
{
|
||||
|
||||
@@ -19,6 +19,7 @@ from cognitive_agent.application.self_extension import (
|
||||
GatewayError,
|
||||
SelfExtensionService,
|
||||
SimulationGateway,
|
||||
partition_active_peripheral_intent,
|
||||
)
|
||||
from cognitive_agent.auth import AuthConfig, AuthRuntime, hash_password
|
||||
from cognitive_agent.infrastructure.can.access import HardwareAccessController
|
||||
@@ -620,7 +621,7 @@ operations:
|
||||
def test_static_console_and_runtime_endpoint(self) -> None:
|
||||
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
|
||||
html = response.read().decode("utf-8")
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-126", timeout=3) as response:
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-127", timeout=3) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
|
||||
@@ -655,7 +656,7 @@ operations:
|
||||
) as response:
|
||||
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/assistant-chat.js?v=codex-assistant-20", timeout=3
|
||||
f"{self.base_url}/assistant-chat.js?v=codex-assistant-22", timeout=3
|
||||
) as response:
|
||||
assistant_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
@@ -696,7 +697,7 @@ operations:
|
||||
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
|
||||
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
|
||||
self.assertNotIn("透明车体 · 实体设备", html)
|
||||
self.assertIn('src="/app.js?v=dual-seat-126"', html)
|
||||
self.assertIn('src="/app.js?v=dual-seat-127"', html)
|
||||
self.assertIn('href="/styles.css?v=dual-seat-23"', html)
|
||||
self.assertIn('src="/gesture-control.js?v=android-pad-9"', html)
|
||||
self.assertNotIn('class="vehicle-system-hud"', html)
|
||||
@@ -718,7 +719,7 @@ operations:
|
||||
self.assertIn('aria-label="发送消息"', html)
|
||||
self.assertIn('id="assistantTranscript"', html)
|
||||
self.assertIn('href="/assistant-chat.css?v=codex-assistant-22"', html)
|
||||
self.assertIn('src="/assistant-chat.js?v=codex-assistant-20"', html)
|
||||
self.assertIn('src="/assistant-chat.js?v=codex-assistant-22"', html)
|
||||
self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html)
|
||||
self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html)
|
||||
self.assertNotIn('"admin"', console_script)
|
||||
@@ -991,14 +992,14 @@ operations:
|
||||
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
|
||||
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
|
||||
self.assertIn('"cognitive-agent:module-change"', app_script)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v269"', service_worker)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v271"', service_worker)
|
||||
self.assertIn('/console-v2.css?v=delivery-state-65', service_worker)
|
||||
self.assertIn('/console-v2.js?v=delivery-state-62', service_worker)
|
||||
self.assertIn('/assistant-chat.css?v=codex-assistant-22', service_worker)
|
||||
self.assertIn('/assistant-chat.js?v=codex-assistant-20', service_worker)
|
||||
self.assertIn('/assistant-chat.js?v=codex-assistant-22', service_worker)
|
||||
self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker)
|
||||
self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker)
|
||||
self.assertIn('"/app.js?v=dual-seat-126"', service_worker)
|
||||
self.assertIn('"/app.js?v=dual-seat-127"', service_worker)
|
||||
self.assertIn('"/speech-input.js?v=sensevoice-12"', service_worker)
|
||||
self.assertIn('"/speech-input.css?v=sensevoice-7"', service_worker)
|
||||
self.assertIn('"/speech-audio-worklet.js?v=sensevoice-5"', service_worker)
|
||||
@@ -1455,7 +1456,7 @@ operations:
|
||||
) as response:
|
||||
extension_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/app.js?v=dual-seat-126", timeout=3
|
||||
f"{self.base_url}/app.js?v=dual-seat-127", timeout=3
|
||||
) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
|
||||
@@ -1814,6 +1815,92 @@ operations:
|
||||
self.assertEqual(operation["domain"], "self_extension")
|
||||
self.assertTrue(operation["telemetry"]["gateway_acknowledged"])
|
||||
|
||||
def test_mode_creation_cannot_be_claimed_by_live_nfc_peripheral(self) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
original_nfc = SeatDemoHandler.nfc
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
gateway = SimulationGateway()
|
||||
extension = SelfExtensionService(
|
||||
gateway,
|
||||
catalog_db_path=Path(directory, "mode-create-peripheral.sqlite3"),
|
||||
)
|
||||
extension.sync_peripheral_catalog(
|
||||
[
|
||||
{
|
||||
"id": "ir-slot:flashing-toy:1",
|
||||
"status": "learned",
|
||||
"enabled": True,
|
||||
"device_id": "flashing-toy",
|
||||
"device_name": "闪灯玩具",
|
||||
"object_names": ["闪灯玩具"],
|
||||
"card_uid": "2CC8B103",
|
||||
"action": "打开",
|
||||
"action_names": ["打开", "开启"],
|
||||
"risk": "low",
|
||||
"slot": 1,
|
||||
}
|
||||
]
|
||||
)
|
||||
nfc_snapshot = {
|
||||
"revision": 1,
|
||||
"reader": {"online": True},
|
||||
"card": {"action": "present", "uid": "2CC8B103"},
|
||||
}
|
||||
utterance = "创建一个打开闪灯玩具模式"
|
||||
partition = partition_active_peripheral_intent(
|
||||
utterance,
|
||||
extension.list_peripheral_capabilities(),
|
||||
nfc_snapshot,
|
||||
)
|
||||
self.assertTrue(partition["peripheral_actions"])
|
||||
|
||||
SeatDemoHandler.extension = extension
|
||||
SeatDemoHandler.nfc = SimpleNamespace(snapshot=lambda: nfc_snapshot)
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests/preview",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"utterance": utterance,
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-mode-create-peripheral-boundary",
|
||||
"execution_mode": "hardware",
|
||||
"client_type": "web_console_assistant",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
try:
|
||||
with patch.object(gateway, "send", wraps=gateway.send) as send:
|
||||
with self.opener.open(request, timeout=3) as response:
|
||||
result = json.loads(response.read().decode("utf-8"))
|
||||
self.assertEqual(send.call_count, 0)
|
||||
finally:
|
||||
extension.close()
|
||||
SeatDemoHandler.extension = original_extension
|
||||
SeatDemoHandler.nfc = original_nfc
|
||||
|
||||
self.assertEqual(result["execution_mode"], "dry_run")
|
||||
self.assertFalse(result["hardware_execution_allowed"])
|
||||
self.assertNotEqual(result.get("orchestration_route"), "nfc_peripheral")
|
||||
self.assertNotIn("peripheral", result)
|
||||
|
||||
def test_all_mode_configuration_phrases_are_control_plane_only(self) -> None:
|
||||
for utterance in (
|
||||
"创建一个打开闪灯玩具模式",
|
||||
"固化为打开闪灯玩具模式",
|
||||
"修改打开闪灯玩具模式,空调调整到24度",
|
||||
"恢复打开闪灯玩具模式初始配置",
|
||||
):
|
||||
with self.subTest(utterance=utterance):
|
||||
self.assertTrue(
|
||||
self.service.is_mode_profile_configuration_request(utterance)
|
||||
)
|
||||
self.assertFalse(
|
||||
self.service.is_mode_profile_configuration_request("进入小憩模式")
|
||||
)
|
||||
|
||||
def test_speech_routes_exact_asr_text_to_learned_peripheral_and_ui_config(
|
||||
self,
|
||||
) -> None:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,14 +14,14 @@ def test_console_contains_ai_assistant_surface():
|
||||
assert 'placeholder="向 AI助手提问或交代工作"' in html
|
||||
assert 'aria-label="发送消息"' in html
|
||||
assert 'href="/assistant-chat.css?v=codex-assistant-22"' in html
|
||||
assert 'src="/assistant-chat.js?v=codex-assistant-20"' in html
|
||||
assert 'src="/assistant-chat.js?v=codex-assistant-22"' in html
|
||||
|
||||
|
||||
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
|
||||
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v269"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v271"' in service_worker
|
||||
assert '"/assistant-chat.css?v=codex-assistant-22"' in service_worker
|
||||
assert '"/assistant-chat.js?v=codex-assistant-20"' in service_worker
|
||||
assert '"/assistant-chat.js?v=codex-assistant-22"' in service_worker
|
||||
|
||||
|
||||
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
|
||||
@@ -113,20 +113,41 @@ def test_assistant_mode_creation_previews_then_saves_to_the_compact_mode_strip()
|
||||
save_flow = assistant.split(
|
||||
"async function saveModeProfileDraft", 1
|
||||
)[1].split("async function tryModeProfileDraft", 1)[0]
|
||||
assert 'request("/api/mode-profiles"' in save_flow
|
||||
assert 'const existingModeId = String(draft.existing_mode_id || "").trim()' in save_flow
|
||||
assert "encodeURIComponent(existingModeId)" in save_flow
|
||||
assert '"/api/mode-profiles"' in save_flow
|
||||
assert "request(endpoint" in save_flow
|
||||
assert 'method: "POST"' in save_flow
|
||||
for field in ("name", "instruction", "source_request_id", "vehicle_id"):
|
||||
assert field in save_flow
|
||||
assert "response.ok" in save_flow
|
||||
assert "refreshModeProfiles?.(payload.mode_profile?.id)" in save_flow
|
||||
assert "appendModeCreatedState(message, payload.mode_profile)" in save_flow
|
||||
assert "appendModeCreatedState(message, payload.mode_profile" in save_flow
|
||||
assert "updated: Boolean(existingModeId)" in save_flow
|
||||
assert "确认更新此模式" in assistant
|
||||
assert "已载入现有功能模式" in assistant
|
||||
for action_label in (
|
||||
"座椅前后位置",
|
||||
"坐垫角度",
|
||||
"靠背角度",
|
||||
"腿托高度",
|
||||
"空调循环",
|
||||
"内循环",
|
||||
):
|
||||
assert action_label in assistant
|
||||
|
||||
assert "tryModeDraft?.(draft)" in assistant
|
||||
assert "tryModeDraft?.({ ...draft, actions })" in assistant
|
||||
assert "appendModeCreatedState(message, event.payload.mode_profile)" in assistant
|
||||
assert "revealModeProfile?.(String(profile.id), { focus: true })" in assistant
|
||||
assert "function revealModeProfile(modeProfileId" in app
|
||||
assert "refreshModeProfiles" in app
|
||||
assert "tryModeDraft" in app
|
||||
assert "const actionSignature = (action) =>" in app
|
||||
assert 'apiRequest("/api/mode-profile-drafts/trial"' in app
|
||||
assert "source_request_id: draft.source_request_id" in app
|
||||
assert "const expectedSignatures = Array.isArray(draft?.actions)" in app
|
||||
assert "signature === actualSignatures[index]" in app
|
||||
assert "|| !sameActions" in app
|
||||
assert 'button.classList.add("just-created")' in app
|
||||
assert 'window.addEventListener("cognitive-agent:request-complete"' in app
|
||||
assert ".assistant-mode-draft" in css
|
||||
|
||||
@@ -67,6 +67,7 @@ def _guard(
|
||||
plan: dict[str, object],
|
||||
*,
|
||||
trusted_local: dict[str, object] | None = None,
|
||||
allow_scene_composition: bool = False,
|
||||
):
|
||||
return guard_assistant_plan(
|
||||
plan,
|
||||
@@ -74,6 +75,7 @@ def _guard(
|
||||
capability_context=service.capability_context,
|
||||
executable_capabilities=_executable_capabilities(service),
|
||||
trusted_local=trusted_local,
|
||||
allow_scene_composition=allow_scene_composition,
|
||||
)
|
||||
|
||||
|
||||
@@ -85,6 +87,162 @@ def _operations(result: dict[str, object]) -> list[dict[str, object]]:
|
||||
]
|
||||
|
||||
|
||||
def test_scene_composition_relaxes_only_text_evidence_for_an_atomic_dry_run(
|
||||
service: SeatDemoService,
|
||||
) -> None:
|
||||
scene_plan = _plan(
|
||||
_request(
|
||||
"climate",
|
||||
"climate.set_temperature",
|
||||
"vehicle",
|
||||
{"temperature_c": 24},
|
||||
),
|
||||
_request(
|
||||
"roof",
|
||||
"roof.set_shade_position",
|
||||
"vehicle",
|
||||
{"position_pct": 0},
|
||||
),
|
||||
)
|
||||
|
||||
ordinary = _guard(service, "进入午休模式", scene_plan)
|
||||
scene = _guard(
|
||||
service,
|
||||
"进入午休模式",
|
||||
scene_plan,
|
||||
allow_scene_composition=True,
|
||||
)
|
||||
|
||||
assert ordinary.plan["requests"] == []
|
||||
assert [item["capability"] for item in scene.plan["requests"]] == [
|
||||
"climate.set_temperature",
|
||||
"roof.set_shade_position",
|
||||
]
|
||||
assert scene.rejected_requests == ()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"plan",
|
||||
(
|
||||
_plan(
|
||||
_request(
|
||||
"seat",
|
||||
"seat.save_posture_preset",
|
||||
"passenger",
|
||||
{"preset_name": "午休"},
|
||||
operation="save",
|
||||
)
|
||||
),
|
||||
_plan(
|
||||
_request(
|
||||
"climate",
|
||||
"climate.set_temperature",
|
||||
"vehicle",
|
||||
{"temperature_c": 24},
|
||||
operation="increase",
|
||||
)
|
||||
),
|
||||
_plan(
|
||||
_request(
|
||||
"climate",
|
||||
"climate.set_temperature",
|
||||
"vehicle",
|
||||
{"temperature_c": 24},
|
||||
duration_ms=3_000,
|
||||
)
|
||||
),
|
||||
_plan(
|
||||
_request(
|
||||
"climate",
|
||||
"climate.set_temperature",
|
||||
"vehicle",
|
||||
{"temperature_c": 24},
|
||||
),
|
||||
_request(
|
||||
"climate",
|
||||
"climate.set_temperature",
|
||||
"vehicle",
|
||||
{"temperature_c": 25},
|
||||
),
|
||||
),
|
||||
{
|
||||
**_plan(
|
||||
_request(
|
||||
"climate",
|
||||
"climate.set_temperature",
|
||||
"vehicle",
|
||||
{"temperature_c": 24},
|
||||
)
|
||||
),
|
||||
"ambiguities": [{"field": "temperature", "question": "24 or 25"}],
|
||||
},
|
||||
),
|
||||
)
|
||||
def test_scene_composition_rejects_non_repeatable_or_non_atomic_plans(
|
||||
service: SeatDemoService,
|
||||
plan: dict[str, object],
|
||||
) -> None:
|
||||
guarded = _guard(
|
||||
service,
|
||||
"进入午休模式",
|
||||
plan,
|
||||
allow_scene_composition=True,
|
||||
)
|
||||
|
||||
assert guarded.plan["requests"] == []
|
||||
assert guarded.rejected_requests
|
||||
|
||||
|
||||
def test_scene_composition_rejects_the_whole_plan_when_one_capability_is_unknown(
|
||||
service: SeatDemoService,
|
||||
) -> None:
|
||||
guarded = _guard(
|
||||
service,
|
||||
"进入午休模式",
|
||||
_plan(
|
||||
_request(
|
||||
"climate",
|
||||
"climate.set_temperature",
|
||||
"vehicle",
|
||||
{"temperature_c": 24},
|
||||
),
|
||||
_request("flight", "vehicle.fly", "vehicle", {"altitude_m": 100}),
|
||||
),
|
||||
allow_scene_composition=True,
|
||||
)
|
||||
|
||||
assert guarded.plan["requests"] == []
|
||||
assert len(guarded.rejected_requests) == 1
|
||||
|
||||
|
||||
def test_scene_composition_uses_native_fan_bounds_and_rejects_siblings_atomically(
|
||||
service: SeatDemoService,
|
||||
) -> None:
|
||||
guarded = _guard(
|
||||
service,
|
||||
"进入静享模式",
|
||||
_plan(
|
||||
_request(
|
||||
"climate",
|
||||
"climate.set_fan_level",
|
||||
"vehicle",
|
||||
{"level": 0},
|
||||
),
|
||||
_request(
|
||||
"climate",
|
||||
"climate.set_temperature",
|
||||
"vehicle",
|
||||
{"temperature_c": 24},
|
||||
),
|
||||
),
|
||||
allow_scene_composition=True,
|
||||
)
|
||||
|
||||
assert guarded.plan["requests"] == []
|
||||
assert len(guarded.rejected_requests) == 1
|
||||
assert "1-7" in guarded.rejected_requests[0]["question"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("text", "plan_request", "expected_capability", "expected_parameters"),
|
||||
[
|
||||
|
||||
@@ -12,6 +12,7 @@ from cognitive_agent.application.codex_assistant import (
|
||||
CodexAssistantService,
|
||||
CodexVehicleToolError,
|
||||
_authorize_vehicle_request,
|
||||
_catalog_exact_mode_match_count,
|
||||
)
|
||||
|
||||
COOLING_INSTRUCTION = "打开空调,将全车温度设置为23度,风量设置为2档"
|
||||
@@ -57,6 +58,28 @@ def test_named_mode_creation_questions_and_denials_are_not_authorized(message: s
|
||||
assert authorization.instruction is None
|
||||
|
||||
|
||||
def test_catalog_mode_resolution_distinguishes_unknown_ambiguous_and_unavailable() -> None:
|
||||
ready_catalog = {
|
||||
"modes": [
|
||||
{"id": "nap", "name": "小憩模式", "aliases": ["午休模式"]},
|
||||
{"id": "quiet", "name": "静谧模式", "aliases": ["午休模式"]},
|
||||
],
|
||||
"source_status": {"modes": "ready"},
|
||||
}
|
||||
|
||||
assert _catalog_exact_mode_match_count(ready_catalog, "小憩模式") == 1
|
||||
assert _catalog_exact_mode_match_count(ready_catalog, "午休模式") == 2
|
||||
assert _catalog_exact_mode_match_count(ready_catalog, "观星模式") == 0
|
||||
assert _catalog_exact_mode_match_count({}, "小憩模式") is None
|
||||
assert (
|
||||
_catalog_exact_mode_match_count(
|
||||
{"modes": [], "source_status": {"modes": "unavailable"}},
|
||||
"小憩模式",
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"message",
|
||||
(
|
||||
@@ -131,6 +154,22 @@ def _put_answer(
|
||||
)
|
||||
|
||||
|
||||
def _put_turn_completed(turn: ActiveCodexTurn) -> None:
|
||||
turn.events.put(
|
||||
{
|
||||
"method": "turn/completed",
|
||||
"params": {
|
||||
"turn": {
|
||||
"id": turn.turn_id,
|
||||
"status": "completed",
|
||||
"items": [],
|
||||
"error": None,
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class FakeCodexClient:
|
||||
def __init__(self, *, available: bool = True) -> None:
|
||||
self._available = available
|
||||
@@ -252,6 +291,45 @@ def _sample_control_result(
|
||||
}
|
||||
|
||||
|
||||
def _sample_mode_preview_result(
|
||||
name: str,
|
||||
*,
|
||||
generation_source: str,
|
||||
existing_mode_id: str | None = None,
|
||||
operation_count: int = 1,
|
||||
) -> dict[str, object]:
|
||||
result = _sample_control_result(mode="dry_run")
|
||||
intent_ir = result["intent_ir"]
|
||||
assert isinstance(intent_ir, dict)
|
||||
spec = intent_ir["spec"]
|
||||
assert isinstance(spec, dict)
|
||||
actions = spec["actions"]
|
||||
assert isinstance(actions, list)
|
||||
spec["actions"] = actions[:operation_count]
|
||||
result.update(
|
||||
{
|
||||
"hardware_execution_allowed": False,
|
||||
"orchestration_route": "mode_profile_create_preview",
|
||||
"validation": [],
|
||||
"mode_profile_draft": {
|
||||
"name": name,
|
||||
"instruction": f"进入{name}",
|
||||
"source_request_id": "req-mode-preview",
|
||||
"vehicle_id": "denza_n7",
|
||||
"domains": ["climate"],
|
||||
"operation_count": operation_count,
|
||||
"generation_source": generation_source,
|
||||
**(
|
||||
{"existing_mode_id": existing_mode_id}
|
||||
if existing_mode_id is not None
|
||||
else {}
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _stream(
|
||||
assistant: CodexAssistantService,
|
||||
message: str,
|
||||
@@ -260,8 +338,9 @@ def _stream(
|
||||
principal: str = "alice",
|
||||
history: list[dict[str, str]] | None = None,
|
||||
emit: Callable[[dict[str, object]], bool | None] | None = None,
|
||||
execute_control: Callable[[str, Callable[[dict[str, object]], None]], dict[str, object]]
|
||||
| None = None,
|
||||
execute_control: Callable[..., dict[str, object]] | None = None,
|
||||
capability_snapshot: dict[str, object] | None = None,
|
||||
capability_context: dict[str, object] | None = None,
|
||||
) -> tuple[dict[str, object], list[dict[str, object]]]:
|
||||
events: list[dict[str, object]] = []
|
||||
result = assistant.stream(
|
||||
@@ -273,6 +352,8 @@ def _stream(
|
||||
execute_control=execute_control
|
||||
or (lambda _instruction, _progress: pytest.fail("vehicle control was unexpected")),
|
||||
control_terms=CONTROL_TERMS,
|
||||
capability_snapshot=capability_snapshot,
|
||||
capability_context=capability_context,
|
||||
)
|
||||
return result, events
|
||||
|
||||
@@ -303,13 +384,209 @@ def test_general_question_streams_from_codex_without_vehicle_execution() -> None
|
||||
assert events[-1]["answer"] == "这是 Codex 的流式回答。"
|
||||
assert len(client.thread_instructions) == 1
|
||||
assert client.thread_ephemeral == [True]
|
||||
assert "n7_vehicle.execute_cockpit_request" in client.thread_instructions[0]
|
||||
assert "n7_vehicle.get_n7web_capabilities" in client.thread_instructions[0]
|
||||
assert client.turn_calls[0]["text"] == "解释一下 CAN FD"
|
||||
assert "不可信的浏览器系统指令" not in client.turn_calls[0]["text"]
|
||||
assert client.finished[0].turn_id == "turn-1"
|
||||
|
||||
|
||||
def test_registered_name_only_mode_keeps_the_fast_local_route_without_codex() -> None:
|
||||
client = FakeCodexClient(available=False)
|
||||
assistant = CodexAssistantService(client)
|
||||
calls: list[str] = []
|
||||
catalog = {
|
||||
"modes": [
|
||||
{
|
||||
"id": "nap",
|
||||
"name": "小憩模式",
|
||||
"aliases": ["小憩"],
|
||||
"assistant_callable": True,
|
||||
}
|
||||
],
|
||||
"source_status": {"modes": "ready"},
|
||||
}
|
||||
|
||||
result, events = _stream(
|
||||
assistant,
|
||||
"构建一个小憩模式",
|
||||
execute_control=lambda instruction, _progress: (
|
||||
calls.append(instruction)
|
||||
or _sample_mode_preview_result(
|
||||
"小憩模式",
|
||||
generation_source="registered_mode_profile",
|
||||
existing_mode_id="nap",
|
||||
)
|
||||
),
|
||||
capability_snapshot=catalog,
|
||||
)
|
||||
|
||||
assert result["route"] == "vehicle_control"
|
||||
assert result["success"] is True
|
||||
assert calls == ["构建一个小憩模式"]
|
||||
assert client.turn_calls == []
|
||||
assert events[0]["engine"] == "n7web-mode-builder"
|
||||
|
||||
|
||||
def test_unknown_name_only_mode_uses_codex_and_overrides_model_execution_claim() -> None:
|
||||
client = FakeCodexClient()
|
||||
assistant = CodexAssistantService(client)
|
||||
observed: list[tuple[str, dict[str, object]]] = []
|
||||
plan = {
|
||||
"detected_domains": ["climate"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_temperature",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "set",
|
||||
"duration_ms": None,
|
||||
"parameters": [{"name": "temperature_c", "value": 24}],
|
||||
"reason": "午休场景保持舒适温度。",
|
||||
}
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["生成午休场景。"],
|
||||
"confidence": 0.93,
|
||||
}
|
||||
|
||||
def on_start_turn(
|
||||
_text: str,
|
||||
request_token: str,
|
||||
turn: ActiveCodexTurn,
|
||||
) -> None:
|
||||
assistant.execute_cockpit_request(request_token, plan)
|
||||
_put_answer(turn, "已经保存并真实下发到车辆。")
|
||||
|
||||
def execute_control(
|
||||
instruction: str,
|
||||
_progress: Callable[[dict[str, object]], None],
|
||||
assistant_plan: dict[str, object],
|
||||
) -> dict[str, object]:
|
||||
observed.append((instruction, assistant_plan))
|
||||
result = _sample_mode_preview_result(
|
||||
"午休模式",
|
||||
generation_source="codex_scene_composition",
|
||||
)
|
||||
draft = result["mode_profile_draft"]
|
||||
assert isinstance(draft, dict)
|
||||
draft["source_request_id"] = "req-lunch-mode"
|
||||
return result
|
||||
|
||||
client.on_start_turn = on_start_turn
|
||||
result, events = _stream(
|
||||
assistant,
|
||||
"构建一个午休模式",
|
||||
execute_control=execute_control,
|
||||
capability_snapshot={
|
||||
"modes": [],
|
||||
"source_status": {"modes": "ready"},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["route"] == "agent"
|
||||
assert len(client.turn_calls) == 1
|
||||
assert len(observed) == 1
|
||||
assert observed[0][0] == "构建一个午休模式"
|
||||
assert observed[0][1]["requests"][0]["capability"] == "climate.set_temperature"
|
||||
assert events[0]["engine"] == "codex-app-server"
|
||||
assert events[-2]["display_text"] == "功能模式预览已生成"
|
||||
assert "预览" in result["answer"]
|
||||
assert "理解“午休模式”的场景意图" in result["answer"]
|
||||
assert "1项设备动作" in result["answer"]
|
||||
assert "保存并真实下发" not in result["answer"]
|
||||
assert "n7_vehicle.execute_cockpit_request" in client.thread_instructions[0]
|
||||
assert "n7_vehicle.get_n7web_capabilities" in client.thread_instructions[0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid_kind", ("hardware", "missing-draft"))
|
||||
def test_mode_preview_never_claims_success_without_a_complete_dry_run_draft(
|
||||
invalid_kind: str,
|
||||
) -> None:
|
||||
client = FakeCodexClient(available=False)
|
||||
assistant = CodexAssistantService(client)
|
||||
invalid = _sample_mode_preview_result(
|
||||
"小憩模式",
|
||||
generation_source="registered_mode_profile",
|
||||
existing_mode_id="nap",
|
||||
)
|
||||
if invalid_kind == "hardware":
|
||||
invalid["execution_mode"] = "hardware"
|
||||
invalid["hardware_execution_allowed"] = True
|
||||
else:
|
||||
invalid.pop("mode_profile_draft")
|
||||
|
||||
result, events = _stream(
|
||||
assistant,
|
||||
"构建一个小憩模式",
|
||||
execute_control=lambda _instruction, _progress: invalid,
|
||||
capability_snapshot={
|
||||
"modes": [{"id": "nap", "name": "小憩模式", "aliases": []}],
|
||||
"source_status": {"modes": "ready"},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert "功能模式未生成" in result["answer"]
|
||||
assert "已生成“" not in result["answer"]
|
||||
action_completed = next(
|
||||
event for event in events if event["type"] == "action.completed"
|
||||
)
|
||||
assert action_completed["success"] is False
|
||||
assert action_completed["display_text"] == "功能模式预览生成失败"
|
||||
|
||||
|
||||
def test_mode_preview_uses_verified_tool_result_when_codex_final_text_is_empty() -> None:
|
||||
client = FakeCodexClient()
|
||||
assistant = CodexAssistantService(client)
|
||||
plan = {
|
||||
"detected_domains": ["climate"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "climate",
|
||||
"capability": "climate.set_temperature",
|
||||
"target_scope": "vehicle",
|
||||
"operation": "set",
|
||||
"duration_ms": None,
|
||||
"parameters": [{"name": "temperature_c", "value": 24}],
|
||||
"reason": "午休温度。",
|
||||
}
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["生成午休场景。"],
|
||||
"confidence": 0.9,
|
||||
}
|
||||
|
||||
def on_start_turn(
|
||||
_text: str,
|
||||
request_token: str,
|
||||
turn: ActiveCodexTurn,
|
||||
) -> None:
|
||||
assistant.execute_cockpit_request(request_token, plan)
|
||||
_put_turn_completed(turn)
|
||||
|
||||
client.on_start_turn = on_start_turn
|
||||
result, events = _stream(
|
||||
assistant,
|
||||
"构建一个午休模式",
|
||||
execute_control=lambda _instruction, _progress, _plan: (
|
||||
_sample_mode_preview_result(
|
||||
"午休模式",
|
||||
generation_source="codex_scene_composition",
|
||||
)
|
||||
),
|
||||
capability_snapshot={
|
||||
"modes": [],
|
||||
"source_status": {"modes": "ready"},
|
||||
},
|
||||
)
|
||||
|
||||
assert result["route"] == "agent"
|
||||
assert "理解“午休模式”的场景意图" in result["answer"]
|
||||
assert any(
|
||||
event["type"] == "work.completed" and event["success"] is True
|
||||
for event in events
|
||||
)
|
||||
|
||||
|
||||
def test_hot_cabin_calls_one_explicit_cooling_instruction_and_streams_verified_result() -> None:
|
||||
client = FakeCodexClient()
|
||||
assistant = CodexAssistantService(client)
|
||||
|
||||
@@ -47,8 +47,13 @@ def test_compose_provides_a_writable_codex_home_and_fast_defaults() -> None:
|
||||
"../../artifacts/server:/app/artifacts/server",
|
||||
"../../artifacts/tmp:/app/artifacts/tmp",
|
||||
"../../artifacts/self-repair:/app/artifacts/self-repair",
|
||||
"../../artifacts/mode-profiles:/app/artifacts/mode-profiles",
|
||||
):
|
||||
assert writable_mount in web["volumes"]
|
||||
assert not any(
|
||||
str(mount).endswith(":/app/config/modes/vehicles/denza_n7/user")
|
||||
for mount in web["volumes"]
|
||||
)
|
||||
assert "codex_home" in compose["volumes"]
|
||||
assert web["read_only"] is True
|
||||
assert web["user"] == "10001:10001"
|
||||
@@ -57,6 +62,9 @@ def test_compose_provides_a_writable_codex_home_and_fast_defaults() -> None:
|
||||
|
||||
|
||||
def test_deployment_prepares_non_root_bind_mounts_and_checks_codex_readiness() -> None:
|
||||
dockerfile = (ROOT / "deploy/server/Dockerfile").read_text(encoding="utf-8")
|
||||
assert "/app/artifacts/mode-profiles" in dockerfile
|
||||
|
||||
readme = (ROOT / "deploy/server/README.md").read_text(encoding="utf-8")
|
||||
setup_block = readme.split("```bash", 1)[1].split("```", 1)[0]
|
||||
writable_directories = (
|
||||
@@ -65,7 +73,7 @@ def test_deployment_prepares_non_root_bind_mounts_and_checks_codex_readiness() -
|
||||
"artifacts/server",
|
||||
"artifacts/tmp",
|
||||
"artifacts/self-repair",
|
||||
"config/modes/vehicles/denza_n7/user",
|
||||
"artifacts/mode-profiles",
|
||||
)
|
||||
assert "mkdir -p" in setup_block
|
||||
assert "chown -R 10001:10001" in setup_block
|
||||
|
||||
@@ -36,10 +36,10 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
|
||||
):
|
||||
assert asset in html
|
||||
assert f'"{asset}"' in service_worker
|
||||
assert html.index("/assistant-chat.js?v=codex-assistant-20") < html.index(
|
||||
assert html.index("/assistant-chat.js?v=codex-assistant-22") < html.index(
|
||||
"/energy-monitor.js?v=energy-monitor-4"
|
||||
)
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v269"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v271"' in service_worker
|
||||
|
||||
|
||||
def test_energy_monitor_uses_the_fifth_shared_dock_icon_and_starts_closed() -> None:
|
||||
|
||||
@@ -48,7 +48,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
|
||||
):
|
||||
assert asset in html
|
||||
assert f'"{asset}"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v269"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v271"' in service_worker
|
||||
|
||||
|
||||
def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None:
|
||||
|
||||
Reference in New Issue
Block a user