282 lines
12 KiB
Python
282 lines
12 KiB
Python
"""Execute the mode preview formatter against actual seat/light parameter shapes."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import shutil
|
||
import subprocess
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
|
||
|
||
@pytest.fixture(scope="module")
|
||
def render_rows():
|
||
node = shutil.which("node")
|
||
if not node:
|
||
runtime = (
|
||
Path.home() / ".cache" / "codex-runtimes" / "codex-primary-runtime"
|
||
/ "dependencies" / "node" / "bin"
|
||
)
|
||
node = next((str(path) for path in (runtime / "node.exe", runtime / "node") if path.is_file()), None)
|
||
assert node, "Node.js is required to validate rendered mode action text"
|
||
source = (ROOT / "apps" / "web-console" / "assistant-chat.js").read_text(encoding="utf-8")
|
||
start = source.index(" function modeDraftRows(")
|
||
end = source.index("\n function ", start + 1)
|
||
script = source[start:end] + "\nconst input = JSON.parse(require('node:fs').readFileSync(0, 'utf8'));\n"
|
||
script += "console.log(JSON.stringify(modeDraftRows(input.draft, input.actions)));"
|
||
|
||
def render(actions, instruction=""):
|
||
completed = subprocess.run(
|
||
[node, "-e", script],
|
||
input=json.dumps({"draft": {"instruction": instruction}, "actions": actions}, ensure_ascii=False),
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
check=True,
|
||
)
|
||
return json.loads(completed.stdout)
|
||
|
||
return render
|
||
|
||
|
||
def action(capability, parameters, zone="driver"):
|
||
return {
|
||
"domain": capability.split(".")[0],
|
||
"capability": capability,
|
||
"target": {"zone": zone},
|
||
"parameters": parameters,
|
||
}
|
||
|
||
|
||
@pytest.mark.parametrize("kind", ["ventilation", "heating"])
|
||
@pytest.mark.parametrize(
|
||
("parameters", "expected"),
|
||
[
|
||
({"state": "on", "level": 1}, "开启 · 3档(高)"),
|
||
({"state": "on", "level": 2}, "开启 · 1档(低)"),
|
||
({"state": "on"}, "开启 · 1档(低)"),
|
||
({"state": "off", "level": 1}, "关闭"),
|
||
],
|
||
)
|
||
def test_driver_native_comfort_uses_its_own_level_scale(render_rows, kind, parameters, expected):
|
||
rows = render_rows([action(f"seat.set_driver_{kind}_native", parameters, "front_left")])
|
||
assert rows[0]["label"] == f"主驾座椅{'通风' if kind == 'ventilation' else '加热'}"
|
||
assert rows[0]["value"] == expected
|
||
|
||
|
||
@pytest.mark.parametrize("kind", ["ventilation", "heating"])
|
||
@pytest.mark.parametrize("level", [1, 2, 3, 4, 5])
|
||
def test_passenger_levels_are_not_translated_using_driver_can_codes(render_rows, kind, level):
|
||
rows = render_rows([action(f"seat.set_{kind}", {"state": "on", "level": level}, "passenger")])
|
||
assert rows[0]["label"] == f"副驾座椅{'通风' if kind == 'ventilation' else '加热'}"
|
||
assert rows[0]["value"] == f"开启 · {level}档"
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("parameters", "expected"),
|
||
[
|
||
({"action": "off", "mode": "relax"}, "关闭"),
|
||
({"action": "mode", "mode": "wave"}, "波浪模式"),
|
||
({"action": "mode", "mode": "pulse"}, "脉冲模式"),
|
||
({"action": "mode", "mode": "stretch"}, "舒展模式"),
|
||
({"action": "mode", "mode": "slack"}, "舒缓模式"),
|
||
({"action": "mode", "mode": "relax"}, "放松模式"),
|
||
({"action": "strength", "strength": 1}, "轻柔力度"),
|
||
({"action": "strength", "strength": 2}, "中等力度"),
|
||
({"action": "strength", "strength": 3}, "较强力度"),
|
||
({"action": "mode"}, "按摩模式待确认"),
|
||
({"action": "strength"}, "按摩力度待确认"),
|
||
],
|
||
)
|
||
def test_massage_preview_distinguishes_operation_mode_and_strength(render_rows, parameters, expected):
|
||
rows = render_rows([action("seat.set_driver_massage_native", parameters)])
|
||
assert rows[0]["label"] == "主驾座椅按摩"
|
||
assert rows[0]["value"] == expected
|
||
|
||
|
||
def test_ambient_native_color_and_brightness_remain_distinct_rows(render_rows):
|
||
rows = render_rows([
|
||
action("ambient_light.set_color_index", {"color_index": 127}, "vehicle"),
|
||
action("ambient_light.set_brightness_level", {"brightness_level": 2}, "vehicle"),
|
||
])
|
||
assert [(row["label"], row["value"]) for row in rows] == [
|
||
("氛围灯颜色", "色号 127"),
|
||
("氛围灯亮度", "2档"),
|
||
]
|
||
|
||
|
||
@pytest.mark.parametrize("brightness", [0, 20, 100])
|
||
def test_rgb_illumination_keeps_exact_color_and_brightness(render_rows, brightness):
|
||
rows = render_rows([
|
||
action("ambient_light.set_illumination", {"color_hex": "#FFB347", "brightness_pct": brightness}, "vehicle"),
|
||
])
|
||
assert rows[0]["value"] == f"颜色 #FFB347 · 亮度 {brightness}%"
|
||
|
||
|
||
def test_zero_gravity_grouping_keeps_driver_comfort_visible(render_rows):
|
||
rows = render_rows([
|
||
action("seat.set_longitudinal_position", {"position_pct": 100}, "passenger"),
|
||
action("seat.set_backrest_recline", {"position_pct": 81, "angle_deg": 150}, "front_right"),
|
||
action("seat.set_driver_massage_native", {"action": "mode", "mode": "relax"}),
|
||
], "副驾零重力模式,并开启主驾放松按摩")
|
||
assert [(row["label"], row["value"]) for row in rows] == [
|
||
("副驾座椅", "零重力模式"),
|
||
("主驾座椅按摩", "放松模式"),
|
||
]
|
||
|
||
|
||
def test_position_and_power_rows_keep_existing_values(render_rows):
|
||
rows = render_rows([
|
||
action("seat.set_backrest_recline", {"position_pct": 81, "angle_deg": 150}, "passenger"),
|
||
action("ambient_light.set_power", {"state": "off"}, "vehicle"),
|
||
action("seat.set_ventilation", {"state": "off", "level": 2}, "passenger"),
|
||
])
|
||
assert [(row["label"], row["value"]) for row in rows] == [
|
||
("副驾靠背位置", "81%(150°)"),
|
||
("氛围灯", "关闭"),
|
||
("副驾座椅通风", "关闭"),
|
||
]
|
||
|
||
|
||
@pytest.mark.parametrize(
|
||
("capability", "mode", "label", "value"),
|
||
[
|
||
("climate.set_control_mode", "auto", "空调控制模式", "自动控制"),
|
||
("climate.set_control_mode", "manual", "空调控制模式", "手动控制"),
|
||
("climate.set_cycle_mode", "internal", "空调循环", "内循环"),
|
||
("climate.set_cycle_mode", "external", "空调循环", "外循环"),
|
||
("climate.set_outlet_mode", "face", "空调出风模式", "吹面"),
|
||
("climate.set_outlet_mode", "face_feet", "空调出风模式", "吹面 / 吹脚"),
|
||
("climate.set_outlet_mode", "feet", "空调出风模式", "吹脚"),
|
||
("climate.set_outlet_mode", "feet_defrost", "空调出风模式", "吹脚 / 除霜"),
|
||
("climate.set_outlet_mode", "defrost", "空调出风模式", "除霜"),
|
||
("climate.set_outlet_mode", "face_feet_defrost", "空调出风模式", "吹面 / 吹脚 / 除霜"),
|
||
("climate.set_outlet_mode", "face_defrost", "空调出风模式", "吹面 / 除霜"),
|
||
],
|
||
)
|
||
def test_climate_modes_use_capability_specific_labels(render_rows, capability, mode, label, value):
|
||
rows = render_rows([action(capability, {"mode": mode}, "vehicle")])
|
||
assert (rows[0]["label"], rows[0]["value"]) == (label, value)
|
||
|
||
|
||
def test_climate_control_circulation_and_outlet_are_not_conflated(render_rows):
|
||
rows = render_rows([
|
||
action("climate.set_control_mode", {"mode": "auto"}, "vehicle"),
|
||
action("climate.set_cycle_mode", {"mode": "internal"}, "vehicle"),
|
||
action("climate.set_outlet_mode", {"mode": "face_feet"}, "vehicle"),
|
||
action("climate.set_fan_level", {"level": 2}, "vehicle"),
|
||
])
|
||
assert [(row["label"], row["value"]) for row in rows] == [
|
||
("空调控制模式", "自动控制"),
|
||
("空调循环", "内循环"),
|
||
("空调出风模式", "吹面 / 吹脚"),
|
||
("空调风量", "2档"),
|
||
]
|
||
|
||
|
||
def test_mode_preview_localizes_external_device_name_and_action_parameters(render_rows):
|
||
rows = render_rows([
|
||
{
|
||
"domain": "self_extension",
|
||
"capability": "infrared.emit",
|
||
"target": {"device": "flashing-light", "name": "闪烁灯"},
|
||
"parameters": {"action": "打开", "slot": 7},
|
||
},
|
||
])
|
||
|
||
assert [(row["label"], row["value"]) for row in rows] == [
|
||
("闪烁灯", "动作:打开"),
|
||
]
|
||
assert "infrared.emit" not in json.dumps(rows, ensure_ascii=False)
|
||
|
||
|
||
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"),
|
||
action("seat.set_ventilation", {"state": "on", "level": 1}, "passenger"),
|
||
action("seat.set_backrest_recline", {"position_pct": 45}, "passenger"),
|
||
action("seat.set_legrest_height", {"position_pct": 35}, "passenger"),
|
||
])
|
||
|
||
assert [(row["label"], row["value"]) for row in rows] == [
|
||
("空调电源", "开启"),
|
||
("左前车窗(主驾)", "完全关闭(目标开度 0%)"),
|
||
("副驾座椅通风", "开启 · 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%"),
|
||
("副驾座椅通风", "关闭"),
|
||
]
|
||
|
||
|
||
def test_collapsible_preview_keeps_all_actions_and_identifies_each_window():
|
||
node = shutil.which("node")
|
||
assert node
|
||
source = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||
start = source.index(" const assistantControlLabels =")
|
||
end = source.index(" function focusLatestDispatchedControls", start)
|
||
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}, "passenger"),
|
||
action("seat.set_legrest_height", {"position_pct": 53}, "passenger"),
|
||
action("seat.set_ventilation", {"state": "off"}, "passenger"),
|
||
*(action("window.set_position", {"position_pct": value}, zone)
|
||
for zone, value in [("front_left", 0), ("front_right", 50),
|
||
("rear_left", 100), ("rear_right", 0)]),
|
||
action("climate.set_power", {"state": "on"}, "vehicle"),
|
||
action("ambient_light.set_power", {"state": "on"}, "vehicle"),
|
||
]
|
||
payload = {
|
||
"execution_mode": "dry_run", "mode_profile_draft": {"name": "浪漫影院模式"},
|
||
"actions": actions,
|
||
# IDs can collide across domains, and seat display projections may be absent.
|
||
"validated_control_commands": [{
|
||
"operation": {**operation, "id": "shared-operation-id"},
|
||
"actual_command": f"command-{index}",
|
||
} for index, operation in enumerate(actions[5:])],
|
||
}
|
||
script = "const window = {};\n" + source[start:end] + "\n" + r'''
|
||
const payload = JSON.parse(require('node:fs').readFileSync(0, 'utf8'));
|
||
const show = (value) => assistantDispatchedCommands(value).map(item => ({
|
||
label: item.category, value: assistantControlValue(item), command: item.command,
|
||
}));
|
||
const preview = show(payload);
|
||
delete payload.validated_control_commands;
|
||
console.log(JSON.stringify({preview, withoutProjections: show(payload)}));
|
||
'''
|
||
output = subprocess.run([node, "-e", script], input=json.dumps(payload),
|
||
check=True, capture_output=True, text=True, encoding="utf-8")
|
||
results = json.loads(output.stdout)
|
||
for rows in (results["preview"], results["withoutProjections"]):
|
||
assert len(rows) == len(actions)
|
||
assert [row["value"] for row in rows[:5]] == ["51%", "30%", "44%", "53%", "关闭"]
|
||
assert [(row["label"], row["value"]) for row in rows[5:9]] == [
|
||
("左前车窗(主驾)", "0%(完全关闭)"), ("右前车窗(副驾)", "50%"),
|
||
("左后车窗", "100%(完全打开)"), ("右后车窗", "0%(完全关闭)"),
|
||
]
|
||
assert [row["label"] for row in rows[-2:]] == ["空调电源", "氛围灯"]
|
||
assert [row["command"] for row in results["preview"][5:]] == [
|
||
f"command-{index}" for index in range(6)
|
||
]
|