fix: compose named cockpit modes from capabilities

This commit is contained in:
vee
2026-09-07 20:52:19 +08:00
parent cf9b89508c
commit 6b2b644ee6
23 changed files with 2917 additions and 124 deletions
+27 -6
View File
@@ -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
+158
View File
@@ -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"),
[
+281 -4
View File
@@ -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)
+9 -1
View File
@@ -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:
+1 -1
View File
@@ -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: