#!/usr/bin/env python3 """Generate the expanded natural-language to CAN command test catalog.""" from __future__ import annotations import csv from collections import Counter from itertools import combinations import json from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] JSON_PATH = ROOT / "tests/fixtures/natural_can_command_combination_suite.json" CSV_PATH = ROOT / "docs/testing/自然语言转CAN组合测试用例.csv" DOC_PATH = ROOT / "docs/testing/自然语言转CAN组合测试说明.md" def can0_operation( capability: str, parameters: dict[str, Any], target: str = "passenger", ) -> dict[str, Any]: return { "target": target, "capability": capability, "parameters": parameters, } def build_cases() -> list[dict[str, Any]]: cases: list[dict[str, Any]] = [] def add( case_id: str, utterance: str, *, domains: tuple[str, ...], channels: tuple[str, ...] = ("can1",), commands: tuple[str, ...] = (), can0_operations: tuple[dict[str, Any], ...] = (), kind: str = "atomic", status: str = "succeeded", sources: tuple[str, ...] = (), ) -> dict[str, Any]: case = { "id": case_id, "kind": kind, "channels": list(channels), "domains": list(domains), "utterance": utterance, "expected_status": status, "expected_commands": list(commands), "expected_can0_operations": list(can0_operations), } if sources: case["source_case_ids"] = list(sources) cases.append(case) return case add( "CAN1-DOOR-REAR-LOCK", "后排车门上锁", domains=("door",), commands=("door rear lock",), ) add( "CAN1-DOOR-REAR-UNLOCK", "后排门锁解锁", domains=("door",), commands=("door rear unlock",), ) window_targets = { "ALL": ("全部车窗", "all"), "FRONT": ("前排车窗", "front"), "REAR": ("后排车窗", "rear"), "FRONT-LEFT": ("左前车窗", "front-left"), "FRONT-RIGHT": ("右前车窗", "front-right"), "REAR-LEFT": ("左后车窗", "rear-left"), "REAR-RIGHT": ("右后车窗", "rear-right"), } window_motions = { "OPEN": ("打开", "open"), "CLOSE": ("关闭", "close"), "STOP": ("停止", "stop"), } for target_id, (target_text, target_command) in window_targets.items(): for motion_id, (verb, motion) in window_motions.items(): add( f"CAN1-WINDOW-{target_id}-{motion_id}", f"{verb}{target_text}", domains=("window",), commands=(f"window {target_command} {motion}",), ) for target_id, (target_text, target_command) in tuple(window_targets.items())[3:]: for position in (0, 25, 50, 100): add( f"CAN1-WINDOW-{target_id}-POSITION-{position}", f"把{target_text}开到{position}%", domains=("window",), commands=(f"window {target_command} position {position}",), ) for alias_id, alias, target in ( ("DRIVER", "主驾", "front-left"), ("PASSENGER", "副驾", "front-right"), ): for motion_id, (verb, motion) in window_motions.items(): add( f"CAN1-WINDOW-{alias_id}-ALIAS-{motion_id}", f"{verb}{alias}车窗", domains=("window",), commands=(f"window {target} {motion}",), ) add("CAN1-AC-ON", "打开空调", domains=("climate",), commands=("ac on",)) add("CAN1-AC-OFF", "关闭空调", domains=("climate",), commands=("ac off",)) for level in range(1, 8): add( f"CAN1-AC-FAN-{level}", f"空调风量调到{level}档", domains=("climate",), commands=(f"ac fan {level}",), ) for alias_id, alias, level in ( ("MIN", "最小", 1), ("LOWEST", "最低", 1), ("LOW", "低档", 1), ("MIDDLE", "中等", 4), ("MEDIUM", "中档", 4), ("HIGH", "高档", 7), ("HIGHEST", "最高", 7), ("MAX", "最大", 7), ): add( f"CAN1-AC-FAN-ALIAS-{alias_id}", f"空调风量设置为{alias}", domains=("climate",), commands=(f"ac fan {level}",), ) outlet_modes = ( ("FACE", "吹脸", "face"), ("FACE-FEET", "吹面和吹脚", "face-feet"), ("FEET", "吹脚", "feet"), ("FEET-DEFROST", "吹脚除霜", "feet-defrost"), ("DEFROST", "除霜", "defrost"), ("FACE-FEET-DEFROST", "吹脸、吹脚和除霜", "face-feet-defrost"), ("FACE-DEFROST", "吹脸和除霜", "face-defrost"), ) for mode_id, mode_text, mode in outlet_modes: add( f"CAN1-AC-MODE-{mode_id}", f"空调出风模式设置为{mode_text}", domains=("climate",), commands=(f"ac mode {mode}",), ) for target_id, target_text, target in ( ("DRIVER", "主驾", "driver"), ("PASSENGER", "副驾", "passenger"), ("REAR", "后排", "rear"), ): for temperature in (18, 24, 32): add( f"CAN1-AC-TEMP-{target_id}-{temperature}", f"{target_text}空调温度设置为{temperature}度", domains=("climate",), commands=(f"ac temp {target} {temperature}",), ) for mode_id, mode_text, mode in ( ("MANUAL", "手动控制", "manual"), ("AUTO", "自动控制", "auto"), ): add( f"CAN1-AC-CONTROL-{mode_id}", f"空调设置为{mode_text}", domains=("climate",), commands=(f"ac control {mode}",), ) for cycle_id, cycle_text, cycle in ( ("INTERNAL", "内循环", "internal"), ("EXTERNAL", "外循环", "external"), ): add( f"CAN1-AC-CYCLE-{cycle_id}", f"空调设置为{cycle_text}", domains=("climate",), commands=(f"ac cycle {cycle}",), ) outlet_targets = ( ("DRIVER-LEFT", "主驾左", "driver-left", (-25, -40)), ("DRIVER-RIGHT", "主驾右", "driver-right", (30, 30)), ("PASSENGER-LEFT", "副驾左", "passenger-left", (-10, -40)), ("PASSENGER-RIGHT", "副驾右", "passenger-right", (5, 30)), ) for target_id, target_text, target, calibrated in outlet_targets: for horizontal, vertical in ((0, 0), (25, 75), (100, 100)): add( f"CAN1-OUTLET-{target_id}-PERCENT-{horizontal}-{vertical}", f"{target_text}风口水平{horizontal}%上下{vertical}%", domains=("outlet",), commands=( f"outlet {target} percent {horizontal} {vertical}", ), ) add( f"CAN1-OUTLET-{target_id}-CALIBRATED", f"{target_text}风口标定坐标左右{calibrated[0]}上下{calibrated[1]}", domains=("outlet",), commands=( f"outlet {target} position {calibrated[0]} {calibrated[1]}", ), ) for feature_id, feature_text, feature in ( ("VENT", "通风", "vent"), ("HEAT", "加热", "heat"), ): for level_id, level_text, level in ( ("OFF", "关闭", "off"), ("LOW", "低档", "low"), ("MIN", "最低", "low"), ("HIGH", "高档", "high"), ("MAX", "最大", "high"), ): utterance = ( f"关闭主驾座椅{feature_text}" if level == "off" else f"主驾座椅{feature_text}设置为{level_text}" ) add( f"CAN1-SEAT-DRIVER-{feature_id}-{level_id}", utterance, domains=("seat",), commands=(f"seat driver {feature} {level}",), ) add( f"CAN1-SEAT-DRIVER-{feature_id}-OPEN-DEFAULT", f"打开主驾座椅{feature_text}", domains=("seat",), commands=(f"seat driver {feature} low",), ) add( "CAN1-SEAT-DRIVER-MASSAGE-OPEN", "打开主驾座椅按摩", domains=("seat",), commands=("seat driver massage mode relax",), ) add( "CAN1-SEAT-DRIVER-MASSAGE-OFF", "关闭主驾座椅按摩", domains=("seat",), commands=("seat driver massage off",), ) for strength_id, strength_text, strength in ( ("WEAK", "最小", "weak"), ("MEDIUM", "中等", "medium"), ("STRONG", "最大", "strong"), ("LOWEST", "最低", "weak"), ("HIGHEST", "最高", "strong"), ): add( f"CAN1-SEAT-DRIVER-MASSAGE-STRENGTH-{strength_id}", f"主驾座椅按摩强度设置为{strength_text}", domains=("seat",), commands=(f"seat driver massage strength {strength}",), ) for mode_id, mode_text, mode in ( ("WAVE", "波浪", "wave"), ("PULSE", "脉冲", "pulse"), ("STRETCH", "伸展", "stretch"), ("SLACK", "松弛", "slack"), ("RELAX", "放松", "relax"), ): add( f"CAN1-SEAT-DRIVER-MASSAGE-MODE-{mode_id}", f"主驾座椅按摩模式设置为{mode_text}", domains=("seat",), commands=(f"seat driver massage mode {mode}",), ) driver_motion_cases = ( ("HORIZONTAL-FORWARD", "主驾座椅向前移动500毫秒", "horizontal forward 500"), ("HORIZONTAL-BACKWARD", "主驾座椅后退两秒", "horizontal backward 2000"), ("HORIZONTAL-STOP", "停止主驾座椅前后移动", "horizontal stop"), ("HEIGHT-UP", "主驾座椅往上移动300毫秒", "height up 300"), ("HEIGHT-DOWN", "主驾座椅向下移动一秒", "height down 1000"), ("HEIGHT-STOP", "停止主驾座椅高度调节", "height stop"), ("BACKREST-FORWARD", "主驾座椅靠背调直500毫秒", "backrest forward 500"), ("BACKREST-BACKWARD", "主驾座椅椅背放倒800毫秒", "backrest backward 800"), ("BACKREST-STOP", "停止主驾座椅靠背动作", "backrest stop"), ("BASIN-UP", "主驾座椅坐盆抬高300毫秒", "basin up 300"), ("BASIN-DOWN", "主驾座椅座垫放低一秒", "basin down 1000"), ("BASIN-STOP", "停止主驾座椅坐盆动作", "basin stop"), ) for case_id, utterance, suffix in driver_motion_cases: add( f"CAN1-SEAT-DRIVER-{case_id}", utterance, domains=("seat",), commands=(f"seat driver {suffix}",), ) add( "CAN1-SEAT-DRIVER-STOP-ALL", "停止主驾座椅移动", domains=("seat",), commands=( "seat driver horizontal stop", "seat driver height stop", "seat driver backrest stop", "seat driver basin stop", ), ) for slot in (1, 2, 3): add( f"CAN1-SEAT-DRIVER-MEMORY-{slot}", f"调用主驾座椅记忆位置{slot}", domains=("seat",), commands=(f"seat driver position {slot}",), ) for state_id, state_text, state in ( ("ON", "打开", "on"), ("OFF", "关闭", "off"), ): add( f"CAN1-LIGHT-INDOOR-{state_id}", f"{state_text}室内灯", domains=("interior_light",), commands=(f"light indoor {state}",), ) add( f"CAN1-LIGHT-READING-ALL-{state_id}", f"{state_text}全部阅读灯", domains=("interior_light",), commands=(f"light reading all {state}",), ) add( f"CAN1-LIGHT-AMBIENT-{state_id}", f"{state_text}氛围灯", domains=("ambient_light",), commands=(f"light ambient {state}",), ) for color in (1, 32, 70, 127): add( f"CAN1-LIGHT-AMBIENT-COLOR-{color}", f"氛围灯颜色编号设置为{color}", domains=("ambient_light",), commands=(f"light ambient color {color}",), ) for brightness in range(1, 6): add( f"CAN1-LIGHT-AMBIENT-BRIGHTNESS-{brightness}", f"氛围灯亮度设置为{brightness}档", domains=("ambient_light",), commands=(f"light ambient brightness {brightness}",), ) for alias_id, alias, brightness in ( ("MIN", "最低", 1), ("MIDDLE", "中等", 3), ("MAX", "最高", 5), ): add( f"CAN1-LIGHT-AMBIENT-BRIGHTNESS-ALIAS-{alias_id}", f"氛围灯亮度设置为{alias}", domains=("ambient_light",), commands=(f"light ambient brightness {brightness}",), ) for motion_id, motion_text, motion in ( ("OPEN", "打开", "open"), ("CLOSE", "关闭", "close"), ("HALF", "半开", "half"), ("STOP", "停止", "stop"), ): utterance = f"遮阳帘{motion_text}" if motion == "half" else f"{motion_text}遮阳帘" add( f"CAN1-SHADE-{motion_id}", utterance, domains=("roof",), commands=(f"shade {motion}",), ) for position in (0, 20, 30, 50, 100): add( f"CAN1-SHADE-POSITION-{position}", f"遮阳帘打开到{position}%", domains=("roof",), commands=(f"shade position {position}",), ) can0_atoms = ( ( "CAN0-SEAT-PASSENGER-HEAT-1", "副驾驶座椅加热开到1档", can0_operation("seat.set_heating", {"state": "on", "level": 1}), ), ( "CAN0-SEAT-PASSENGER-HEAT-2", "副驾驶座椅加热开到2档", can0_operation("seat.set_heating", {"state": "on", "level": 2}), ), ( "CAN0-SEAT-PASSENGER-HEAT-3", "副驾驶座椅加热开到3档", can0_operation("seat.set_heating", {"state": "on", "level": 3}), ), ( "CAN0-SEAT-PASSENGER-HEAT-OFF", "关闭副驾驶座椅加热", can0_operation("seat.set_heating", {"state": "off"}), ), ( "CAN0-SEAT-PASSENGER-VENT-1", "副驾驶座椅通风开到1档", can0_operation("seat.set_ventilation", {"state": "on", "level": 1}), ), ( "CAN0-SEAT-PASSENGER-VENT-2", "副驾驶座椅通风开到2档", can0_operation("seat.set_ventilation", {"state": "on", "level": 2}), ), ( "CAN0-SEAT-PASSENGER-VENT-3", "副驾驶座椅通风开到3档", can0_operation("seat.set_ventilation", {"state": "on", "level": 3}), ), ( "CAN0-SEAT-PASSENGER-VENT-OFF", "关闭副驾驶座椅通风", can0_operation("seat.set_ventilation", {"state": "off"}), ), ( "CAN0-SEAT-PASSENGER-HORIZONTAL-30", "把副驾驶座椅水平位置调到30%", can0_operation("seat.set_longitudinal_position", {"position_pct": 30}), ), ( "CAN0-SEAT-PASSENGER-BASIN-40", "把副驾驶坐盆倾角调到40%", can0_operation("seat.set_cushion_tilt", {"position_pct": 40}), ), ( "CAN0-SEAT-PASSENGER-BACKREST-120", "把副驾驶靠背调到120度", can0_operation( "seat.set_backrest_recline", {"position_pct": 37, "angle_deg": 120}, ), ), ( "CAN0-SEAT-PASSENGER-LEGREST-125", "把副驾驶腿托抬到125度", can0_operation( "seat.set_legrest_height", {"position_pct": 42, "angle_deg": 125}, ), ), ) for case_id, utterance, operation in can0_atoms: add( case_id, utterance, domains=("seat",), channels=("can0",), can0_operations=(operation,), ) atom_by_id = {case["id"]: case for case in cases} def combine( case_id: str, source_ids: tuple[str, ...], *, separator: str = ",", kind: str, ) -> None: source_cases = [atom_by_id[source_id] for source_id in source_ids] channels = tuple( channel for channel in ("can0", "can1") if any(channel in item["channels"] for item in source_cases) ) domains = tuple( dict.fromkeys( domain for item in source_cases for domain in item["domains"] ) ) add( case_id, separator.join(item["utterance"] for item in source_cases), domains=domains, channels=channels, commands=tuple( command for item in source_cases for command in item["expected_commands"] ), can0_operations=tuple( operation for item in source_cases for operation in item["expected_can0_operations"] ), kind=kind, sources=source_ids, ) pair_pool = ( "CAN1-DOOR-REAR-LOCK", "CAN1-WINDOW-FRONT-LEFT-OPEN", "CAN1-WINDOW-REAR-RIGHT-POSITION-25", "CAN1-AC-ON", "CAN1-AC-FAN-2", "CAN1-AC-MODE-FACE-FEET", "CAN1-AC-TEMP-DRIVER-24", "CAN1-AC-TEMP-PASSENGER-24", "CAN1-AC-TEMP-REAR-24", "CAN1-AC-CONTROL-AUTO", "CAN1-AC-CYCLE-INTERNAL", "CAN1-OUTLET-DRIVER-LEFT-PERCENT-25-75", "CAN1-OUTLET-PASSENGER-RIGHT-PERCENT-25-75", "CAN1-SEAT-DRIVER-VENT-HIGH", "CAN1-SEAT-DRIVER-HEAT-LOW", "CAN1-SEAT-DRIVER-MASSAGE-MODE-RELAX", "CAN1-SEAT-DRIVER-MASSAGE-STRENGTH-MEDIUM", "CAN1-SEAT-DRIVER-HORIZONTAL-FORWARD", "CAN1-SEAT-DRIVER-HEIGHT-UP", "CAN1-LIGHT-INDOOR-ON", "CAN1-LIGHT-READING-ALL-ON", "CAN1-LIGHT-AMBIENT-ON", "CAN1-LIGHT-AMBIENT-COLOR-70", "CAN1-LIGHT-AMBIENT-BRIGHTNESS-3", "CAN1-SHADE-POSITION-30", ) for index, source_pair in enumerate(combinations(pair_pool, 2), start=1): combine( f"COMBO-PAIR-COMMA-{index:03d}", source_pair, kind="pairwise", ) combine( f"COMBO-PAIR-SEMICOLON-{index:03d}", source_pair, separator=";", kind="pairwise_separator_variant", ) multi_anchors = ( "CAN1-DOOR-REAR-LOCK", "CAN1-WINDOW-FRONT-LEFT-OPEN", "CAN1-AC-FAN-2", "CAN1-OUTLET-DRIVER-LEFT-PERCENT-25-75", "CAN1-SEAT-DRIVER-VENT-HIGH", "CAN1-LIGHT-INDOOR-ON", "CAN1-LIGHT-AMBIENT-COLOR-70", "CAN1-SHADE-POSITION-30", "CAN0-SEAT-PASSENGER-HEAT-2", ) for index, source_ids in enumerate(combinations(multi_anchors, 3), start=1): combine( f"COMBO-TRIPLE-{index:03d}", source_ids, kind="three_command", ) for index, source_ids in enumerate(combinations(multi_anchors, 4), start=1): combine( f"COMBO-QUADRUPLE-{index:03d}", source_ids, kind="four_command", ) cross_can1 = pair_pool[:15] cross_can0 = tuple(item[0] for item in can0_atoms[0:8]) for index, (can1_id, can0_id) in enumerate( ((left, right) for left in cross_can1 for right in cross_can0), start=1, ): combine( f"COMBO-CROSS-CHANNEL-{index:03d}", (can1_id, can0_id), kind="cross_can0_can1", ) scenes = ( ( "MOVIE", ( "CAN1-AC-ON", "CAN1-AC-TEMP-DRIVER-24", "CAN1-AC-FAN-2", "CAN1-SHADE-POSITION-100", "CAN1-LIGHT-AMBIENT-COLOR-70", "CAN1-LIGHT-AMBIENT-BRIGHTNESS-3", ), ), ( "DRIVER-COMFORT", ( "CAN1-SEAT-DRIVER-VENT-LOW", "CAN1-SEAT-DRIVER-MASSAGE-MODE-RELAX", "CAN1-SEAT-DRIVER-MASSAGE-STRENGTH-MEDIUM", "CAN1-AC-TEMP-DRIVER-24", "CAN1-AC-FAN-2", ), ), ( "PASSENGER-COMFORT", ( "CAN0-SEAT-PASSENGER-HEAT-2", "CAN0-SEAT-PASSENGER-HORIZONTAL-30", "CAN0-SEAT-PASSENGER-BASIN-40", "CAN0-SEAT-PASSENGER-BACKREST-120", "CAN0-SEAT-PASSENGER-LEGREST-125", ), ), ( "CABIN-BRIGHT", ( "CAN1-LIGHT-INDOOR-ON", "CAN1-LIGHT-READING-ALL-ON", "CAN1-LIGHT-AMBIENT-ON", "CAN1-LIGHT-AMBIENT-COLOR-70", "CAN1-LIGHT-AMBIENT-BRIGHTNESS-5", ), ), ( "VENTILATION", ( "CAN1-AC-ON", "CAN1-AC-FAN-7", "CAN1-AC-MODE-FACE-FEET", "CAN1-AC-CYCLE-EXTERNAL", "CAN1-OUTLET-DRIVER-LEFT-PERCENT-25-75", "CAN1-OUTLET-PASSENGER-RIGHT-PERCENT-25-75", ), ), ( "PARK-CLOSE", ( "CAN1-WINDOW-ALL-CLOSE", "CAN1-SHADE-CLOSE", "CAN1-DOOR-REAR-LOCK", "CAN1-LIGHT-INDOOR-OFF", "CAN1-LIGHT-AMBIENT-OFF", "CAN1-AC-OFF", ), ), ( "DUAL-SEAT", ( "CAN1-SEAT-DRIVER-VENT-HIGH", "CAN1-SEAT-DRIVER-MASSAGE-MODE-WAVE", "CAN0-SEAT-PASSENGER-VENT-2", "CAN0-SEAT-PASSENGER-BACKREST-120", "CAN1-AC-TEMP-DRIVER-24", "CAN1-AC-TEMP-PASSENGER-24", ), ), ( "ALL-ZONES-CLIMATE", ( "CAN1-AC-ON", "CAN1-AC-TEMP-DRIVER-24", "CAN1-AC-TEMP-PASSENGER-24", "CAN1-AC-TEMP-REAR-24", "CAN1-AC-FAN-4", "CAN1-AC-CONTROL-AUTO", "CAN1-AC-CYCLE-INTERNAL", ), ), ( "WINDOWS-SHADE", ( "CAN1-WINDOW-FRONT-LEFT-POSITION-25", "CAN1-WINDOW-FRONT-RIGHT-POSITION-50", "CAN1-WINDOW-REAR-LEFT-POSITION-100", "CAN1-WINDOW-REAR-RIGHT-POSITION-0", "CAN1-SHADE-POSITION-30", ), ), ( "DRIVER-POSTURE", ( "CAN1-SEAT-DRIVER-HORIZONTAL-FORWARD", "CAN1-SEAT-DRIVER-HEIGHT-UP", "CAN1-SEAT-DRIVER-BACKREST-BACKWARD", "CAN1-SEAT-DRIVER-BASIN-DOWN", ), ), ) for scene_id, source_ids in scenes: combine( f"COMBO-SCENE-{scene_id}", source_ids, kind="multi_command_scene", ) safety_cases = ( ("DRIVER-VENT-LEVEL-4", "主驾座椅通风4档"), ("DRIVER-VENT-MIDDLE", "主驾座椅通风中档"), ("DRIVER-ABSOLUTE-BACKREST", "把驾驶位靠背调到50%"), ("DRIVER-UNVERIFIED-MODE", "主驾进入零重力模式"), ("FRONT-ROW-SEAT", "打开前排座椅加热"), ("PASSENGER-HEAT-LEVEL-4", "副驾座椅加热开到4档"), ("PASSENGER-VENT-MISSING-STATE", "副驾座椅通风"), ("PASSENGER-CONTRADICTORY", "打开副驾加热然后关闭副驾加热"), ("PASSENGER-MASSAGE", "把副驾座椅按摩打开"), ("MULTIPLE-SEAT-MODES", "进入办公模式和零重力模式"), ("PASSENGER-LEGREST-EXTEND", "把副驾腿托伸长到50%"), ("WRONG-AXIS-UNIT", "坐垫向前移动10度"), ("MISSING-SEAT-AXIS", "向前移动10度"), ("REAR-SEAT", "打开后排座椅加热"), ("ALL-SEATS", "打开所有座椅加热"), ("OUTLET-DISABLE-WITHOUT-MODE", "关闭吹面出风口"), ) for case_id, utterance in safety_cases: add( f"SAFETY-{case_id}", utterance, domains=("safety",), channels=(), kind="safety_rejection", status="needs_confirmation", ) return cases def write_outputs(cases: list[dict[str, Any]]) -> None: kind_counts = Counter(case["kind"] for case in cases) channel_counts = Counter( "+".join(case["channels"]) or "none" for case in cases ) payload = { "schema": "natural-can-command-combination-suite/v1", "generated_on": "2026-08-24", "description": "自然语言控制指令到CAN0/CAN1语义控制指令的原子、组合和安全测试目录。", "summary": { "total_cases": len(cases), "by_kind": dict(sorted(kind_counts.items())), "by_channel": dict(sorted(channel_counts.items())), }, "cases": cases, } JSON_PATH.write_text( json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) with CSV_PATH.open("w", encoding="utf-8-sig", newline="") as handle: writer = csv.DictWriter( handle, fieldnames=( "id", "kind", "channels", "domains", "utterance", "expected_status", "expected_commands", "expected_can0_operations", "source_case_ids", ), ) writer.writeheader() for case in cases: writer.writerow( { **case, "channels": "/".join(case["channels"]), "domains": "/".join(case["domains"]), "expected_commands": " | ".join(case["expected_commands"]), "expected_can0_operations": json.dumps( case["expected_can0_operations"], ensure_ascii=False ), "source_case_ids": " | ".join( case.get("source_case_ids", []) ), } ) kind_lines = "\n".join( f"| `{kind}` | {count} |" for kind, count in sorted(kind_counts.items()) ) channel_lines = "\n".join( f"| `{channel}` | {count} |" for channel, count in sorted(channel_counts.items()) ) DOC_PATH.write_text( f"""# 自然语言转 CAN 控制指令组合测试说明 - 生成日期:2026-08-24 - 总用例数:**{len(cases)}** - 执行方式:本地规则、`dry_run`,不发送真实 CAN 帧 - 完整逐条清单:`docs/testing/自然语言转CAN组合测试用例.csv` - 机器可执行数据:`tests/fixtures/natural_can_command_combination_suite.json` ## 覆盖数量 | 用例类型 | 数量 | | --- | ---: | {kind_lines} | 通道 | 数量 | | --- | ---: | {channel_lines} ## 组合策略 1. 原子用例覆盖 CAN1 门锁、车窗、空调、风口、主驾座椅、灯光、遮阳帘,以及 CAN0 副驾座椅。 2. 两两组合从 25 个互不冲突的控制槽中取组合,同时覆盖中文逗号和分号分隔形式。 3. 三指令和四指令组合覆盖 CAN0/CAN1、座椅、空调、灯光、车窗、门锁、风口与遮阳帘的交叉。 4. 跨通道组合明确检查副驾座椅留在 CAN0、主驾和车身控制留在 CAN1。 5. 多指令场景覆盖观影、双座椅、全温区空调、泊车关闭、车窗与遮阳帘等常用场景。 6. 安全用例要求白名单外目标、档位、能力和矛盾指令不生成 CAN 控制流。 ## 执行 ```bash PYTHONPATH=src:domains/seat/src .venv/bin/python -m pytest -q tests/integration/test_natural_can_command_combinations.py ``` 重新生成清单: ```bash .venv/bin/python tools/generate_natural_can_command_suite.py ``` """, encoding="utf-8", ) def main() -> None: cases = build_cases() ids = [case["id"] for case in cases] if len(ids) != len(set(ids)): duplicates = [item for item, count in Counter(ids).items() if count > 1] raise ValueError(f"duplicate case ids: {duplicates}") write_outputs(cases) print(f"generated {len(cases)} cases") print(JSON_PATH) print(CSV_PATH) print(DOC_PATH) if __name__ == "__main__": main()