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
@@ -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