fix(modes): only activate peripherals and show individual seat actions

This commit is contained in:
Miayui
2026-09-22 13:34:29 +08:00
parent 6b9ee0f349
commit 2b365b3922
21 changed files with 176 additions and 215 deletions
+4 -4
View File
@@ -1071,7 +1071,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-39-video-media", timeout=3
f"{self.base_url}/assistant-chat.js?v=codex-assistant-40-seat-rows", timeout=3
) as response:
assistant_script = response.read().decode("utf-8")
with self.opener.open(
@@ -1156,7 +1156,7 @@ operations:
self.assertIn('id="assistantTranscript"', template)
self.assertIn('import "../assistant-chat.css";', vite_entry)
self.assertIn(
f'src="{runtime_url("assistant-chat.js", "/assistant-chat.js?v=codex-assistant-39-video-media")}"',
f'src="{runtime_url("assistant-chat.js", "/assistant-chat.js?v=codex-assistant-40-seat-rows")}"',
html,
)
self.assertIn('import "../energy-monitor.css";', vite_entry)
@@ -1472,13 +1472,13 @@ operations:
self.assertIn(f'/{runtime_assets[asset_name]}', service_worker)
else:
self.assertIn(
'const CACHE_VERSION = "cognitive-agent-pad-v336-seat-preset"',
'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"',
service_worker,
)
self.assertIn('/console-v2.css?v=delivery-state-69', service_worker)
self.assertIn('/console-v2.js?v=local-first-79-cinema-status', service_worker)
self.assertIn('/assistant-chat.css?v=codex-assistant-30', service_worker)
self.assertIn('/assistant-chat.js?v=codex-assistant-39-video-media', service_worker)
self.assertIn('/assistant-chat.js?v=codex-assistant-40-seat-rows', service_worker)
self.assertIn('/energy-monitor.css?v=energy-monitor-5-dialog-size', service_worker)
self.assertIn('/energy-monitor.js?v=energy-monitor-5-performance', service_worker)
self.assertIn('"/app.js?v=dual-seat-148-seat-preset"', service_worker)
@@ -256,9 +256,9 @@ def test_romantic_cinema_template_preserves_cinema_and_adds_authorized_light(
assert result["mode_profile_draft"]["generation_source"] == "scene_template_copy"
if stale_profile is not None:
assert result["mode_profile_draft"]["existing_mode_id"] == stale_profile.id
assert result["mode_profile_draft"]["operation_count"] == (25 if full_scene else 7)
assert result["mode_profile_draft"]["operation_count"] == (23 if full_scene else 7)
safe_result = CodexAssistantService._safe_vehicle_result(result)
assert len(safe_result["actions"]) == (25 if full_scene else 7)
assert len(safe_result["actions"]) == (23 if full_scene else 7)
native_actions = {
(
@@ -282,10 +282,9 @@ def test_romantic_cinema_template_preserves_cinema_and_adds_authorized_light(
video = [action for action in received_cinema["actions"] if action["domain"] == "android"]
assert result["n7_transfer_candidates"] == video
slots = [action["slot"] for action in result["infrared_actions"]]
assert sorted(slots) == ([2, 3, 22, 23, 41] if full_scene else [1])
assert sorted(slots) == ([2, 22, 41] if full_scene else [1])
if full_scene:
assert slots.index(2) < slots.index(3)
assert slots.index(22) < slots.index(23)
assert all(action["action"] == "打开" for action in result["infrared_actions"])
draft = result["mode_profile_draft"]
arguments = (
@@ -429,18 +428,16 @@ def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
"climate",
"self_extension",
]
assert preview["mode_profile_draft"]["operation_count"] == 3
assert preview["mode_profile_draft"]["operation_count"] == 2
assert set(preview["function_configs"]) == {"native_climate", "self_extension"}
infrared_commands = preview["command_plans"]["self_extension"]["spec"][
"commands"
]
assert [command["message"] for command in infrared_commands] == [
"infrared.emit",
"infrared.emit",
]
assert [command["signals"] for command in infrared_commands] == [
{"slot": 1},
{"slot": 7},
]
assert preview["infrared_actions"][0]["parse_source"] == (
"scene_composer_capability_ref"
@@ -465,10 +462,9 @@ def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
assert [operation.capability for operation in profile.operations] == [
"climate.set_temperature",
"infrared.emit",
"infrared.emit",
]
entries = handler._saved_mode_infrared_entries(profile)
assert [entry["capability"]["slot"] for entry in entries] == [1, 7]
assert [entry["capability"]["slot"] for entry in entries] == [1]
native_execution = service.execute_mode_profile_card(
profile.id,
conversation_id="conv-saved-scene-peripheral",
@@ -489,7 +485,7 @@ def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
entries,
)
assert executed["status"] == "succeeded"
assert [action["slot"] for action in executed["infrared_actions"]] == [1, 7]
assert [action["slot"] for action in executed["infrared_actions"]] == [1]
invoked = handler._execute_request_with_peripherals(
"/api/requests/preview",
{
@@ -506,7 +502,7 @@ def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
assert invoked is not None
assert invoked["status"] == "succeeded"
assert invoked["execution_mode"] == "dry_run"
assert [action["slot"] for action in invoked["infrared_actions"]] == [1, 7]
assert [action["slot"] for action in invoked["infrared_actions"]] == [1]
template_plan = _catalog_exact_scene_template_plan(
catalog.snapshot(),
@@ -611,8 +607,8 @@ def test_scene_composer_mixed_can_ir_preview_save_and_revalidate(
assert explicit is not None
assert explicit["status"] == "succeeded"
assert explicit["execution_mode"] == "dry_run"
assert explicit["mode_profile_draft"]["operation_count"] == 3
assert [action["slot"] for action in explicit["infrared_actions"]] == [1, 7]
assert explicit["mode_profile_draft"]["operation_count"] == 2
assert [action["slot"] for action in explicit["infrared_actions"]] == [1]
# Removing the durable NFC whitelist invalidates both cached refs and saved
# mode execution before any transport can be reached.
@@ -21,17 +21,17 @@ def test_console_contains_ai_assistant_surface():
assert 'placeholder="向 AI助手提问或交代工作"' in template
assert 'aria-label="发送消息"' in template
assert 'import "../assistant-chat.css";' in vite_entry
assert 'src="/assistant-chat.js?v=codex-assistant-39-video-media"' in html
assert 'src="/assistant-chat.js?v=codex-assistant-40-seat-rows"' 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-v336-seat-preset"'
'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"'
in service_worker
)
assert '"/assistant-chat.css?v=codex-assistant-30"' in service_worker
assert '"/assistant-chat.js?v=codex-assistant-39-video-media"' in service_worker
assert '"/assistant-chat.js?v=codex-assistant-40-seat-rows"' in service_worker
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
@@ -293,9 +293,9 @@ def test_assistant_mode_creation_previews_then_saves_to_the_compact_mode_strip()
assert "已载入现有功能模式" in assistant
for action_label in (
"座椅前后位置",
"坐垫角度",
"靠背角度",
"腿托高度",
"坐垫位置",
"靠背位置",
"腿托位置",
"空调循环",
"内循环",
):
+26 -6
View File
@@ -55,9 +55,9 @@ def action(capability, parameters, zone="driver"):
@pytest.mark.parametrize(
("parameters", "expected"),
[
({"state": "on", "level": 1}, "开启 · 高档"),
({"state": "on", "level": 2}, "开启 · 低档"),
({"state": "on"}, "开启 · 低档"),
({"state": "on", "level": 1}, "开启 · 3档(高)"),
({"state": "on", "level": 2}, "开启 · 1档(低)"),
({"state": "on"}, "开启 · 1档(低)"),
({"state": "off", "level": 1}, "关闭"),
],
)
@@ -135,7 +135,7 @@ def test_position_and_power_rows_keep_existing_values(render_rows):
action("seat.set_ventilation", {"state": "off", "level": 2}, "passenger"),
])
assert [(row["label"], row["value"]) for row in rows] == [
("副驾靠背角度", "81%(150°)"),
("副驾靠背位置", "81%(150°)"),
("氛围灯", "关闭"),
("副驾座椅通风", "关闭"),
]
@@ -193,7 +193,7 @@ def test_mode_preview_localizes_external_device_name_and_action_parameters(rende
assert "infrared.emit" not in json.dumps(rows, ensure_ascii=False)
def test_default_bundle_rows_show_power_window_and_grouped_passenger_comfort(render_rows):
def test_default_bundle_rows_show_each_passenger_action_separately(render_rows):
rows = render_rows([
action("climate.set_power", {"state": "on"}, "vehicle"),
action("window.set_position", {"position_pct": 0}, "front_left"),
@@ -205,5 +205,25 @@ def test_default_bundle_rows_show_power_window_and_grouped_passenger_comfort(ren
assert [(row["label"], row["value"]) for row in rows] == [
("空调电源", "开启"),
("主驾车窗", "完全关闭(目标开度 0%)"),
("副驾座椅舒适模式", "通风开启 1档 · 靠背 45% · 腿托 35%"),
("副驾座椅通风", "开启 · 1档"),
("副驾靠背位置", "45%"),
("副驾腿托位置", "35%"),
]
def test_cinema_draft_shows_all_four_axes_and_ventilation(render_rows):
actions = [
action("seat.set_longitudinal_position", {"position_pct": 51}, "passenger"),
action("seat.set_cushion_tilt", {"position_pct": 30}, "passenger"),
action("seat.set_backrest_recline", {"position_pct": 44}, "front_right"),
action("seat.set_legrest_height", {"position_pct": 53}, "front_right"),
action("seat.set_ventilation", {"state": "off"}, "passenger"),
]
rows = render_rows(actions, "进入浪漫影院模式")
assert [(row["label"], row["value"]) for row in rows] == [
("副驾座椅前后位置", "51%"),
("副驾坐垫位置", "30%"),
("副驾靠背位置", "44%"),
("副驾腿托位置", "53%"),
("副驾座椅通风", "关闭"),
]
@@ -37,11 +37,11 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
assert f'"{asset}"' in service_worker
assert 'import "../energy-monitor.css";' in vite_entry
assert "/energy-monitor.js?v=energy-monitor-5-performance" in html
assert html.index("/assistant-chat.js?v=codex-assistant-39-video-media") < html.index(
assert html.index("/assistant-chat.js?v=codex-assistant-40-seat-rows") < html.index(
"/energy-monitor.js?v=energy-monitor-5-performance"
)
assert (
'const CACHE_VERSION = "cognitive-agent-pad-v336-seat-preset"'
'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"'
in service_worker
)
@@ -99,5 +99,5 @@ def test_performance_assets_have_a_new_cache_generation() -> None:
):
assert asset in html
assert f'"{asset}"' in worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v336-seat-preset"' in worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"' in worker
assert '"/codepen-glass-theme.css?v=creative-cloud-6-performance"' in worker
+1 -1
View File
@@ -54,7 +54,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
assert source_asset in main
assert f'"{cached_asset}"' in service_worker
assert 'import "./styles/design-system.css"' in main
assert 'const CACHE_VERSION = "cognitive-agent-pad-v336-seat-preset"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"' in service_worker
def test_reference_glass_theme_covers_the_complete_workspace() -> None:
+6 -8
View File
@@ -387,7 +387,7 @@ def test_import_upserts_yaml_refreshes_catalog_and_does_not_echo(
assert "upstream_origin: user" in yaml_text
def test_later_n7_cinema_mode_inherits_base_and_adds_external_light_colors(
def test_later_n7_cinema_mode_inherits_base_and_only_opens_external_lights(
tmp_path: Path,
) -> None:
service = configured_service(tmp_path)
@@ -426,14 +426,14 @@ def test_later_n7_cinema_mode_inherits_base_and_adds_external_light_colors(
"base_scene_id": "cinema",
"base_action_count": base_action_count,
"incoming_action_count": 1,
"peripheral_action_count": 4,
"merged_action_count": base_action_count + 4,
"peripheral_action_count": 2,
"merged_action_count": base_action_count + 2,
}
profile = service.get_mode_profile_card("blue-cinema", "denza_n7")
source_scene = profile.command_flow["source_scene"]
actions = source_scene["actions"]
assert source_scene["operation_count"] == base_action_count + 4
assert source_scene["operation_count"] == base_action_count + 2
assert "self_extension" in source_scene["domains"]
temperatures = [
action["parameters"]["temperature_c"]
@@ -455,20 +455,18 @@ def test_later_n7_cinema_mode_inherits_base_and_adds_external_light_colors(
for action in peripheral_actions
] == [
("flash-light", "打开", 1),
("flash-light", "切换为蓝色", 3),
("light-strip", "打开", 21),
("light-strip", "切换为蓝色", 23),
]
repeated = service.import_n7_mode_scene(variant)
assert (
repeated["cinema_augmentation"]["merged_action_count"]
== base_action_count + 4
== base_action_count + 2
)
reloaded = service.get_mode_profile_card("blue-cinema", "denza_n7")
assert (
len(reloaded.command_flow["source_scene"]["actions"])
== base_action_count + 4
== base_action_count + 2
)
+42 -11
View File
@@ -10,7 +10,9 @@ from cognitive_agent.application.romantic_lighting import romantic_light_actions
from cognitive_agent.application.scene_composer import scene_required_peripheral_refs
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 GatewayError, SelfExtensionService, SimulationGateway
from cognitive_agent.application.self_extension import (
ExtensionIntegrationError, GatewayError, SelfExtensionService, SimulationGateway,
)
from cognitive_agent.server import SeatDemoHandler
@@ -33,22 +35,22 @@ def catalog():
]
def test_romantic_lights_all_use_on_then_red_and_preserve_on_only_candles():
def test_romantic_lights_only_use_power_on():
entries = catalog()
entries += [
light("light5", "落地灯", "红色", 61), # No known power-on command.
light("light6", "投影灯", "打开", 71),
light("light6", "投影灯", "蓝色", 72), # Never substitute blue for red.
light("light6", "投影灯", "蓝色", 72), # Color capability never becomes a mode action.
light("light7", "灯笼", "打开", 81, enabled=False),
light("light7", "灯笼", "红色", 82, enabled=False),
]
before = deepcopy(entries)
assert [item["slot"] for item in romantic_light_actions(entries)] == [2, 3, 22, 23, 41, 51, 52]
assert [item["slot"] for item in romantic_light_actions(entries)] == [2, 22, 41, 51, 71]
assert entries == before
@pytest.mark.parametrize("name", ["浪漫模式", "浪漫影院模式", "约会模式", "Romantic cinema"])
def test_creation_replaces_old_colors_and_orders_all_required_lights(name):
def test_creation_removes_colors_and_only_opens_required_lights(name):
actions = [{
"ref": f"ir:{item['slot']}", "device_key": item["device_id"],
"device_label": item["device_name"], "action_label": item["action"],
@@ -56,7 +58,7 @@ def test_creation_replaces_old_colors_and_orders_all_required_lights(name):
} for item in catalog()]
context = {"peripheral_actions": actions}
required = scene_required_peripheral_refs(name, context)
assert required == ["ir:2", "ir:3", "ir:22", "ir:23", "ir:41", "ir:51", "ir:52"]
assert required == ["ir:2", "ir:22", "ir:41", "ir:51"]
context["scene_semantics"] = {"mode_name": name, "peripheral_policy": {"required_refs": required}}
plan = {"requests": [{
"domain": "self_extension", "capability": "infrared.emit",
@@ -73,10 +75,39 @@ def test_same_named_lights_remain_separate_devices():
"device_label": "灯笼", "action_label": label,
} for device in ["one", "two"] for action, label in [("on", "打开"), ("red", "红色")]]
assert [item["ref"] for item in romantic_light_actions(actions)] == [
"ir:one-on", "ir:one-red", "ir:two-on", "ir:two-red",
"ir:one-on", "ir:two-on",
]
def test_mode_creation_deduplicates_colors_into_authorized_activation():
capabilities = catalog()
handler = object.__new__(SeatDemoHandler)
handler.extension = SimpleNamespace(list_peripheral_capabilities=lambda: capabilities)
entries = [{"capability": capabilities[index]} for index in [3, 2, 1, 5, 4, 6]]
selected = handler._mode_creation_activation_entries(entries)
assert [entry["capability"]["slot"] for entry in selected] == [2, 22, 41]
def test_mode_creation_rejects_color_only_device_or_rebound_power_on():
red = light("light1", "灯笼", "红色", 3)
handler = object.__new__(SeatDemoHandler)
for available in [
[red],
[red, light("light1", "灯笼", "打开", 2, enabled=False)],
[red, light("light1", "灯笼", "打开", 2, card_uid="00000002")],
]:
handler.extension = SimpleNamespace(list_peripheral_capabilities=lambda: available)
with pytest.raises(ExtensionIntegrationError, match="开启指令"):
handler._mode_creation_activation_entries([{"capability": red}])
def test_scene_defaults_never_fall_back_to_color_only_refs():
context = {"peripheral_actions": [{
"ref": "ir:strip-red", "device_label": "灯带", "action_label": "红色",
}]}
assert scene_required_peripheral_refs("中秋模式", context) == []
def test_romantic_import_adds_ordered_lights_without_requiring_cinema_base(tmp_path: Path):
service = SeatDemoService(REPOSITORY_ROOT)
service.mode_profiles = ModeProfileRegistry(tmp_path / "presets", user_root=tmp_path / "modes")
@@ -92,9 +123,9 @@ def test_romantic_import_adds_ordered_lights_without_requiring_cinema_base(tmp_p
profile = service.get_mode_profile_card("romantic-date", "denza_n7")
actions = profile.command_flow["source_scene"]["actions"]
assert actions[0] == climate
assert [item["parameters"]["slot"] for item in actions[1:]] == [2, 3, 22, 23, 41, 51, 52]
assert [item["parameters"]["slot"] for item in actions[1:]] == [2, 22, 41, 51]
scene = profile.command_flow["source_scene"]
assert len(profile.operations) == 8
assert len(profile.operations) == 5
finally:
service.close()
@@ -113,7 +144,7 @@ def test_saved_romantic_mode_orders_real_dispatch_and_stops_on_power_failure(tmp
}})
entries = handler._saved_mode_infrared_entries(profile)
capabilities = [entry["capability"] for entry in entries]
assert [item["slot"] for item in capabilities] == [2, 3, 22, 23, 41, 51, 52]
assert [item["slot"] for item in capabilities] == [2, 22, 41, 51]
sent = []
def send(_command, parameters):
@@ -130,5 +161,5 @@ def test_saved_romantic_mode_orders_real_dispatch_and_stops_on_power_failure(tmp
{"kind": "peripheral", "capability_id": item["id"]} for item in capabilities
]}, capabilities, conversation_id="romantic-order-test", execution_mode="hardware",
)
assert sent == ([2] if fail_power else [2, 3, 22, 23, 41, 51, 52])
assert sent == ([2] if fail_power else [2, 22, 41, 51])
assert result["status"] == ("failed" if fail_power else "succeeded")
+2 -6
View File
@@ -695,9 +695,7 @@ def test_romantic_template_always_adds_authorized_decorative_lights() -> None:
)
assert _peripheral_refs(observed[0]) == [
"ir:flash-open-000000000001",
"ir:flash-red-0000000000002",
"ir:strip-open-000000000001",
"ir:strip-red-0000000000002",
"ir:ambient-open-0000000001",
]
assert "ir:flash-close-00000000003" not in _peripheral_refs(observed[0])
@@ -742,9 +740,7 @@ def test_romantic_cinema_template_keeps_native_base_and_adds_lights_without_mode
assert native_requests == template["plan"]["requests"]
assert _peripheral_refs(observed[0]) == [
"ir:flash-open-000000000001",
"ir:flash-red-0000000000002",
"ir:strip-open-000000000001",
"ir:strip-red-0000000000002",
"ir:ambient-open-0000000001",
]
@@ -1220,8 +1216,8 @@ def test_unknown_mode_uses_one_structured_turn_then_local_dry_run() -> None:
"required_refs": [],
"selection_goal": (
"Include as many semantically relevant scene-safe peripheral devices "
"as are useful; normally choose activation plus at most one stable "
"state action per device."
"as are useful; choose only one authorized power-on "
"action per device, without color or effect settings."
),
"required_reason": "No peripheral action is mandatory for this name.",
}
+1 -1
View File
@@ -521,7 +521,7 @@ def test_romantic_and_mid_autumn_require_decorative_light_actions(
schema = build_scene_output_schema(context)
romantic_refs = [
"ir:flash-open", "ir:flash-red", "ir:strip-open", "ir:strip-red", "ir:ambient-open",
"ir:flash-open", "ir:strip-open", "ir:ambient-open",
]
assert required == (romantic_refs if "浪漫" in mode_name else [
"ir:flash-open",
+1 -1
View File
@@ -31,7 +31,7 @@ def test_vue_vite_application_is_the_primary_frontend_entry() -> None:
assert package["scripts"]["build"] == "vite build"
assert 'import vue from "@vitejs/plugin-vue"' in vite
assert 'outDir: outputRoot' in vite
assert 'const CACHE_VERSION = "cognitive-agent-pad-v336-seat-preset"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v337-seat-rows"' in service_worker
assert 'createHash("sha256")' in vite
assert 'runtime-assets.json' in vite
assert 'rewriteRuntimeReferences' in vite