diff --git a/apps/web-console/assistant-chat.js b/apps/web-console/assistant-chat.js
index d2dc86a..4ef225e 100644
--- a/apps/web-console/assistant-chat.js
+++ b/apps/web-console/assistant-chat.js
@@ -168,11 +168,11 @@
if (!compact || !context?.name) return false;
const politePrefix = "(?:(?:请|麻烦)(?:帮我|帮忙|给我)?|帮我|帮忙|给我)?";
const prefixUpdateVerb = "(?:修改一下|修改|调整|更改|更新|改一下|变更)";
- const changePredicate = "(?:修改|改成|改为|更改|更新|变更|变为|换成|调整|调节|调高|调低|调到|调成|调为|调至|设置|设为|设成|设到|设至|打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|复位|半开|开到|关到|拉开|拉上|上锁|解锁)";
+ const changePredicate = "(?:修改|改成|改为|更改|更新|变更|变为|变成|换成|调整|调节|调高|调低|调到|调成|调为|调至|设置|设为|设成|设到|设至|打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|复位|半开|开到|关到|拉开|拉上|上锁|解锁)";
const modeName = "([^,。!?!?、;;::]{1,64}?模式)";
const modePossessiveBoundary = "(?:中的|里的|内的|之中的|的|中|里|内)";
const modeMemberBoundary = `(?:${modePossessiveBoundary}|[,,::])`;
- const question = /为什么|什么|怎么|如何|介绍|解释|作用|区别|原理|哪些|多少|几(?:度|档|级|个|项)?|是否|能否|可不可以|合适吗|合理吗|会怎样|列出|告诉我|[??]|吗[。.!!]?$/;
+ const question = /为什么|什么|怎么|如何|介绍|解释|作用|区别|原理|哪些|多少|几(?:度|档|级|个|项)?|是否|能否|可不可以|合适吗|合理吗|会怎样|列出|告诉我|[??]|(?:吗|么|呢)[。.!!]?$/;
const candidates = [
{
pattern: new RegExp(`^${politePrefix}${prefixUpdateVerb}${modeName}${modeMemberBoundary}[,,::]?(.*)$`),
@@ -469,6 +469,18 @@
on: "开启",
off: "关闭",
}[String(value)] || String(value || "已配置"));
+ const shadePositionLabel = (value) => {
+ if (value == null || String(value).trim() === "") return "目标开度待确认";
+ const position = Number(value);
+ if (!Number.isFinite(position) || position < 0 || position > 100) {
+ return "目标开度待确认";
+ }
+ const target = `目标开度 ${position}%`;
+ if (position === 0) return `完全关闭(${target})`;
+ if (position === 50) return `半开(${target})`;
+ if (position === 100) return `完全打开(${target})`;
+ return target;
+ };
actions.forEach((action, index) => {
if (consumed.has(index)) return;
const capability = String(action.capability || "");
@@ -482,7 +494,9 @@
if (capability === "roof.set_shade_motion" || capability === "roof.set_shade_position") {
label = "遮阳帘";
- value = params.position_pct != null ? `${params.position_pct}%` : motionLabel(params.motion);
+ value = params.position_pct != null
+ ? shadePositionLabel(params.position_pct)
+ : motionLabel(params.motion);
icon = "blinds";
order = instructionOrder("遮阳帘", "天幕帘");
} else if (domain === "window") {
diff --git a/apps/web-console/index.html b/apps/web-console/index.html
index 13c53ca..69b964c 100755
--- a/apps/web-console/index.html
+++ b/apps/web-console/index.html
@@ -13,7 +13,7 @@
-
+
@@ -26,7 +26,7 @@
-
+
diff --git a/apps/web-console/service-worker.js b/apps/web-console/service-worker.js
index 4e0d693..925e37f 100644
--- a/apps/web-console/service-worker.js
+++ b/apps/web-console/service-worker.js
@@ -16,8 +16,8 @@ const APP_SHELL = [
"/speech-input.css?v=sensevoice-7",
"/speech-input.js?v=sensevoice-12",
"/speech-audio-worklet.js?v=sensevoice-5",
- "/assistant-chat.css?v=codex-assistant-23",
- "/assistant-chat.js?v=codex-assistant-23",
+ "/assistant-chat.css?v=codex-assistant-24",
+ "/assistant-chat.js?v=codex-assistant-24",
"/energy-monitor.css?v=energy-monitor-4",
"/energy-monitor.js?v=energy-monitor-4",
"/assets/pets/self-extension-cat.png?v=3",
diff --git a/src/cognitive_agent/application/codex_assistant.py b/src/cognitive_agent/application/codex_assistant.py
index 71dfa3c..77edb5b 100644
--- a/src/cognitive_agent/application/codex_assistant.py
+++ b/src/cognitive_agent/application/codex_assistant.py
@@ -983,6 +983,22 @@ class CodexAssistantService:
if draft.get("existing_mode_id")
else "确认后可保存为功能模式。"
)
+ requested_operation_count = draft.get("requested_operation_count")
+ changed_operation_count = draft.get("changed_operation_count")
+ no_effective_change = (
+ isinstance(requested_operation_count, int)
+ and not isinstance(requested_operation_count, bool)
+ and requested_operation_count > 0
+ and isinstance(changed_operation_count, int)
+ and not isinstance(changed_operation_count, bool)
+ and changed_operation_count == 0
+ )
+ if no_effective_change:
+ return (
+ f"待保存的“{name}”模式中,该项原本已是目标状态,"
+ f"已保持不变并重新校验全部{operation_count}项组合动作。"
+ f"原草稿未被保存或执行,可以继续调整;{persistence_hint}"
+ )
return (
f"已调整待保存的“{name}”模式,并重新校验全部"
f"{operation_count}项组合动作。原草稿未被保存或执行,"
@@ -1839,6 +1855,8 @@ class CodexAssistantService:
"generation_source",
"draft_revision",
"revises_source_request_id",
+ "requested_operation_count",
+ "changed_operation_count",
)
if key in mode_profile_draft
}
diff --git a/src/cognitive_agent/application/mode_profile_intent.py b/src/cognitive_agent/application/mode_profile_intent.py
index 6f8825e..29aa8e9 100644
--- a/src/cognitive_agent/application/mode_profile_intent.py
+++ b/src/cognitive_agent/application/mode_profile_intent.py
@@ -29,7 +29,7 @@ _CONTEXTUAL_NAME_FIRST_UPDATE = re.compile(
r"(?P.+?)[。.!!]?$"
)
MODE_PROFILE_CHANGE_PREDICATE = re.compile(
- r"修改|改成|改为|更改|更新|变更|变为|换成|"
+ r"修改|改成|改为|更改|更新|变更|变成|变为|换成|"
r"调整|调节|调高|调低|调到|调成|调为|调至|"
r"设置|设为|设成|设到|设至|"
r"打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|"
@@ -40,7 +40,7 @@ MODE_PROFILE_QUERY_CUE = re.compile(
r"什么|哪些|多少|几(?:度|档|级|个|项)?|是否|是不是|"
r"合适(?:吗)?|合理(?:吗)?|会(?:怎么样|怎样|如何)|"
r"怎么|如何|列(?:出|一下)|介绍(?:一下)?|说明(?:一下)?|"
- r"告诉我|查询|查看|有什么作用|是何作用|还是|[??]"
+ r"告诉我|查询|查看|有什么作用|是何作用|还是|[??]|(?:吗|么|呢)$"
)
diff --git a/src/cognitive_agent/application/native_can_intent.py b/src/cognitive_agent/application/native_can_intent.py
index e07df12..3a97a55 100644
--- a/src/cognitive_agent/application/native_can_intent.py
+++ b/src/cognitive_agent/application/native_can_intent.py
@@ -1349,7 +1349,8 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
shade_text = _scoped_text(compact, r"遮阳帘|天幕帘")
if shade_text and _allows_vehicle_wide_target(shade_text):
position = re.search(
- r"(?:位置|开度|调整到|调到|设为|设置为|开到|打开(?:到)?|开启(?:到)?)(\d{1,3})%",
+ r"(?:位置|开度|调整到|调整为|调到|调为|改为|改成|变为|变成|"
+ r"换成|设为|设置为|开到|打开(?:到)?|开启(?:到)?)(\d{1,3})%",
shade_text,
)
if position and int(position[1]) <= 100:
diff --git a/src/cognitive_agent/application/seat_service.py b/src/cognitive_agent/application/seat_service.py
index 95acc4c..3f8a8fe 100755
--- a/src/cognitive_agent/application/seat_service.py
+++ b/src/cognitive_agent/application/seat_service.py
@@ -4309,7 +4309,7 @@ class SeatDemoService:
if not change_text:
raise ValueError("请说明要调整的设备和明确目标值。")
parseable_change_text = re.sub(
- r"改为|改成|变更为|换成|变为",
+ r"改为|改成|变更为|换成|变成|变为",
"调整为",
change_text,
)
@@ -4387,6 +4387,22 @@ class SeatDemoService:
): item
for item in normalized_changes
}
+ base_by_key = {
+ (
+ str(item["domain"]),
+ str(item["capability"]),
+ str(item["target_scope"]),
+ ): item
+ for item in base_operations
+ }
+ requested_operation_count = len(change_by_key)
+ changed_operation_count = sum(
+ 1
+ for key, item in change_by_key.items()
+ if key not in base_by_key
+ or dict(base_by_key[key].get("parameters") or {})
+ != dict(item.get("parameters") or {})
+ )
merged: list[dict[str, Any]] = []
for operation in base_operations:
key = (
@@ -4397,7 +4413,7 @@ class SeatDemoService:
merged.append(deepcopy(change_by_key.pop(key, operation)))
merged.extend(deepcopy(list(change_by_key.values())))
- return self._process_mode_profile_create(
+ revised_result = self._process_mode_profile_create(
utterance,
"local",
conversation_id,
@@ -4410,6 +4426,26 @@ class SeatDemoService:
assistant_plan=self._mode_profile_draft_plan(merged),
source_draft=source_draft,
)
+ revised_draft = revised_result.get("mode_profile_draft")
+ if (
+ revised_result.get("status") == "succeeded"
+ and isinstance(revised_draft, dict)
+ and revised_draft.get("generation_source") == "draft_revision"
+ ):
+ revised_draft["requested_operation_count"] = (
+ requested_operation_count
+ )
+ revised_draft["changed_operation_count"] = changed_operation_count
+ mode_creation = revised_result.get("mode_creation")
+ if isinstance(mode_creation, dict):
+ mode_creation["requested_operation_count"] = (
+ requested_operation_count
+ )
+ mode_creation["changed_operation_count"] = (
+ changed_operation_count
+ )
+ self._persist(revised_result)
+ return revised_result
except ValueError as exc:
result["status"] = "needs_confirmation"
result["execution_mode"] = "dry_run"
@@ -7180,7 +7216,21 @@ class SeatDemoService:
allow_scene_composition: bool = False,
) -> tuple[dict[str, Any], str, str | None]:
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
- clause_results = self._local_clause_results(utterance, context)
+ if assistant_plan is not None and allow_scene_composition:
+ # ``utterance`` is a synthesized ``进入`` carrier for a
+ # name-only scene (and for a server-owned draft revision). Words
+ # inside that label are not user-authored atomic instructions:
+ # e.g. ``外循环通风模式`` previously leaked ``通风`` into the seat
+ # parser and added an AMBIGUOUS_INTENT beside an otherwise valid
+ # climate plan. In this narrow path the complete assistant plan is
+ # the candidate source of actions; it still passes the scene
+ # allowlist, registered capability contract, ModelPlanAdapter,
+ # schema/safety validation and native compiler below.
+ clause_results = [
+ (utterance, self._merge_parsed_results([]), "unparsed")
+ ]
+ else:
+ clause_results = self._local_clause_results(utterance, context)
trusted_local = self._merge_parsed_results(
[parsed for _, parsed, status in clause_results if status != "unparsed"]
diff --git a/tests/e2e/test_assistant_web_api.py b/tests/e2e/test_assistant_web_api.py
index 8886ea4..71db6e6 100644
--- a/tests/e2e/test_assistant_web_api.py
+++ b/tests/e2e/test_assistant_web_api.py
@@ -770,6 +770,37 @@ class TestCodexAssistantWebApi:
assert action["payload"]["hardware_execution_allowed"] is False
assert events[0]["engine"] == "n7web-mode-builder"
+ def test_become_percentage_update_keeps_movie_draft_route_end_to_end(
+ self,
+ ) -> None:
+ reference = {
+ "name": "观影模式",
+ "source_request_id": "req-mode-preview",
+ "vehicle_id": "denza_n7",
+ }
+ message = "把观影模式的遮阳帘变成100%"
+
+ events = self.post_assistant(
+ {
+ "message": message,
+ "conversation_id": "conv-api-movie-shade-become-percent",
+ "execution_mode": "hardware",
+ "mode_profile_draft_context": reference,
+ }
+ )
+
+ assert not FAKE_CODEX.turn_calls
+ call = AssistantApiHandler.service.calls[-1]
+ assert call["utterance"] == message
+ assert call["mode_profile_draft_context"] == reference
+ action = next(event for event in events if event["type"] == "action.completed")
+ assert action["success"] is True
+ assert action["payload"]["execution_mode"] == "dry_run"
+ assert action["payload"]["hardware_execution_allowed"] is False
+ assert action["payload"]["mode_profile_draft"]["draft_revision"] == 2
+ assert events[0]["route"] == "vehicle_control"
+ assert events[0]["engine"] == "n7web-mode-builder"
+
def test_active_draft_reference_is_not_forwarded_for_an_unrelated_question(
self,
) -> None:
diff --git a/tests/e2e/test_seat_web_api.py b/tests/e2e/test_seat_web_api.py
index 88087dd..d0e4a33 100755
--- a/tests/e2e/test_seat_web_api.py
+++ b/tests/e2e/test_seat_web_api.py
@@ -656,7 +656,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-23", timeout=3
+ f"{self.base_url}/assistant-chat.js?v=codex-assistant-24", timeout=3
) as response:
assistant_script = response.read().decode("utf-8")
with self.opener.open(
@@ -718,8 +718,8 @@ operations:
self.assertNotIn("滚动查看更多", html)
self.assertIn('aria-label="发送消息"', html)
self.assertIn('id="assistantTranscript"', html)
- self.assertIn('href="/assistant-chat.css?v=codex-assistant-23"', html)
- self.assertIn('src="/assistant-chat.js?v=codex-assistant-23"', html)
+ self.assertIn('href="/assistant-chat.css?v=codex-assistant-24"', html)
+ self.assertIn('src="/assistant-chat.js?v=codex-assistant-24"', html)
self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html)
self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html)
self.assertNotIn('"admin"', console_script)
@@ -995,8 +995,8 @@ operations:
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v272"', service_worker)
self.assertIn('/console-v2.css?v=delivery-state-65', service_worker)
self.assertIn('/console-v2.js?v=delivery-state-62', service_worker)
- self.assertIn('/assistant-chat.css?v=codex-assistant-23', service_worker)
- self.assertIn('/assistant-chat.js?v=codex-assistant-23', service_worker)
+ self.assertIn('/assistant-chat.css?v=codex-assistant-24', service_worker)
+ self.assertIn('/assistant-chat.js?v=codex-assistant-24', service_worker)
self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker)
self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker)
self.assertIn('"/app.js?v=dual-seat-127"', service_worker)
diff --git a/tests/integration/test_mode_draft_parameter_matrix.py b/tests/integration/test_mode_draft_parameter_matrix.py
new file mode 100644
index 0000000..1430250
--- /dev/null
+++ b/tests/integration/test_mode_draft_parameter_matrix.py
@@ -0,0 +1,1230 @@
+from __future__ import annotations
+
+from copy import deepcopy
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any
+from unittest.mock import patch
+
+import pytest
+
+from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
+from cognitive_agent.application.assistant_plan_guard import MAX_PLAN_REQUESTS
+from cognitive_agent.application.codex_assistant import _authorize_vehicle_request
+from cognitive_agent.application.mode_profile import ModeProfileRegistry
+from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
+
+
+@dataclass(frozen=True)
+class SceneCase:
+ case_id: str
+ mode_name: str
+ capability: str
+ target: str
+ parameters: dict[str, object]
+
+ @property
+ def domain(self) -> str:
+ return self.capability.split(".", 1)[0]
+
+
+@dataclass(frozen=True)
+class DraftChangeCase:
+ case_id: str
+ message: str
+ capability: str
+ target: str
+ parameters: dict[str, object]
+
+
+def _scene_request(
+ capability: str,
+ target: str,
+ parameters: dict[str, object],
+ *,
+ operation: str = "set",
+ duration_ms: int | None = None,
+) -> dict[str, object]:
+ return {
+ "domain": capability.split(".", 1)[0],
+ "capability": capability,
+ "target_scope": target,
+ "operation": operation,
+ "duration_ms": duration_ms,
+ "parameters": [
+ {"name": name, "value": value}
+ for name, value in parameters.items()
+ ],
+ "reason": "按用户场景要求设置当前工程已登记的绝对目标。",
+ }
+
+
+def _scene_plan(*requests: dict[str, object]) -> dict[str, object]:
+ return {
+ "detected_domains": list(
+ dict.fromkeys(str(request["domain"]) for request in requests)
+ ) or ["seat"],
+ "requests": [deepcopy(request) for request in requests],
+ "ambiguities": [],
+ "decision_summary": ["只组合当前工程注册表允许的稳定目标。"],
+ "confidence": 0.95,
+ }
+
+
+@pytest.fixture()
+def mode_service(tmp_path: Path) -> tuple[SeatDemoService, Path]:
+ service = SeatDemoService(REPOSITORY_ROOT)
+ service.artifact_root = tmp_path / "artifacts"
+ user_root = tmp_path / "user-modes"
+ service.mode_profiles = ModeProfileRegistry(
+ REPOSITORY_ROOT / "config" / "modes",
+ user_root=user_root,
+ )
+ return service, user_root
+
+
+def _preview_without_hardware(
+ service: SeatDemoService,
+ message: str,
+ *,
+ conversation_id: str,
+ assistant_plan: dict[str, object] | None = None,
+ draft_context: dict[str, str] | None = None,
+) -> dict[str, Any]:
+ with (
+ patch.object(
+ service.hardware_execution,
+ "reserve",
+ side_effect=AssertionError("mode draft route reached a hardware lease"),
+ ),
+ patch.object(
+ service.openai,
+ "interpret",
+ side_effect=AssertionError("the deterministic matrix called an external model"),
+ ),
+ ):
+ return service.preview_native_can(
+ message,
+ provider="local",
+ conversation_id=conversation_id,
+ execution_mode="hardware",
+ vehicle_id="denza_n7",
+ assistant_plan=deepcopy(assistant_plan),
+ mode_profile_draft_context=deepcopy(draft_context),
+ )
+
+
+def _intent_actions(result: dict[str, Any]) -> list[dict[str, Any]]:
+ intent_ir = result.get("intent_ir")
+ if not isinstance(intent_ir, dict):
+ return []
+ spec = intent_ir.get("spec")
+ if not isinstance(spec, dict):
+ return []
+ actions = spec.get("actions")
+ if not isinstance(actions, list):
+ return []
+ return [action for action in actions if isinstance(action, dict)]
+
+
+def _target(action: dict[str, Any]) -> str:
+ raw = action.get("target")
+ if isinstance(raw, dict):
+ return str(
+ raw.get("instance")
+ or raw.get("zone")
+ or raw.get("scope")
+ or ""
+ )
+ return str(raw or "")
+
+
+def _action_map(
+ result: dict[str, Any],
+) -> dict[tuple[str, str], dict[str, object]]:
+ return {
+ (str(action.get("capability") or ""), _target(action)): dict(
+ action.get("parameters") or {}
+ )
+ for action in _intent_actions(result)
+ }
+
+
+def _assert_parameter_subset(
+ result: dict[str, Any],
+ capability: str,
+ target: str,
+ parameters: dict[str, object],
+) -> None:
+ action = _action_map(result).get((capability, target))
+ assert action is not None, (capability, target, _action_map(result))
+ for name, value in parameters.items():
+ assert action.get(name) == value
+
+
+def _draft_reference(result: dict[str, Any]) -> dict[str, str]:
+ draft = result.get("mode_profile_draft")
+ assert isinstance(draft, dict)
+ return {
+ "name": str(draft["name"]),
+ "source_request_id": str(draft["source_request_id"]),
+ "vehicle_id": str(draft["vehicle_id"]),
+ }
+
+
+def _assert_successful_preview(
+ result: dict[str, Any],
+ *,
+ name: str,
+ operation_count: int,
+) -> dict[str, Any]:
+ assert result["status"] == "succeeded"
+ assert result["execution_mode"] == "dry_run"
+ assert result["hardware_execution_allowed"] is False
+ assert result["orchestration_route"] == "mode_profile_create_preview"
+ assert result.get("mode_profile") is None
+ assert not any(
+ isinstance(issue, dict) and issue.get("severity") == "error"
+ for issue in result.get("validation", [])
+ )
+ draft = result.get("mode_profile_draft")
+ assert isinstance(draft, dict)
+ assert draft["name"] == name
+ assert draft["operation_count"] == operation_count
+ assert len(_intent_actions(result)) == operation_count
+ return draft
+
+
+def _assert_atomic_failure(result: dict[str, Any], user_root: Path) -> None:
+ assert result["status"] != "succeeded"
+ assert result["execution_mode"] == "dry_run"
+ assert result["hardware_execution_allowed"] is False
+ assert result.get("mode_profile_draft") is None
+ assert _intent_actions(result) == []
+ assert result.get("function_config") is None
+ assert result.get("function_configs") == {}
+ assert result.get("command_plan") is None
+ assert result.get("command_plans") == {}
+ assert result.get("scene_config") is None
+ assert result.get("execution_report") is None
+ assert not list(user_root.rglob("*.yaml"))
+
+
+TYPED_SCENE_CASES = (
+ SceneCase(
+ "integer-temperature",
+ "整数恒温模式",
+ "climate.set_temperature",
+ "vehicle",
+ {"temperature_c": 25},
+ ),
+ SceneCase(
+ "percentage-shade",
+ "柔光遮阳模式",
+ "roof.set_shade_position",
+ "vehicle",
+ {"position_pct": 37},
+ ),
+ SceneCase(
+ "binary-state",
+ "迎宾氛围模式",
+ "ambient_light.set_power",
+ "vehicle",
+ {"state": "on"},
+ ),
+ SceneCase(
+ "color-and-percentage",
+ "蓝光阅读模式",
+ "ambient_light.set_illumination",
+ "vehicle",
+ {"color_hex": "#2040FF", "brightness_pct": 60},
+ ),
+ SceneCase(
+ "fan-level",
+ "清风通勤模式",
+ "climate.set_fan_level",
+ "vehicle",
+ {"level": 4},
+ ),
+ SceneCase(
+ "two-axis-percentage",
+ "定向送风模式",
+ "outlet.set_position_percent",
+ "passenger_right",
+ {"horizontal_pct": 20, "vertical_pct": 80},
+ ),
+ SceneCase(
+ "scoped-window",
+ "后排换气模式",
+ "window.set_position",
+ "rear_right",
+ {"position_pct": 45},
+ ),
+ SceneCase(
+ "state-and-level",
+ "副驾暖座模式",
+ "seat.set_heating",
+ "passenger",
+ {"state": "on", "level": 3},
+ ),
+ SceneCase(
+ "cycle-enum",
+ "外循环通风模式",
+ "climate.set_cycle_mode",
+ "vehicle",
+ {"mode": "external"},
+ ),
+ SceneCase(
+ "control-enum",
+ "自动温控模式",
+ "climate.set_control_mode",
+ "vehicle",
+ {"mode": "auto"},
+ ),
+ SceneCase(
+ "all-reading-lights",
+ "全舱暗光模式",
+ "interior_light.set_reading_power",
+ "all",
+ {"state": "off"},
+ ),
+ SceneCase(
+ "seat-angle",
+ "副驾舒展模式",
+ "seat.set_backrest_recline",
+ "passenger",
+ {"angle_deg": 120},
+ ),
+)
+
+
+@pytest.mark.parametrize("case", TYPED_SCENE_CASES, ids=lambda case: case.case_id)
+def test_name_only_scene_parameter_types_are_registry_bounded_and_compile(
+ mode_service: tuple[SeatDemoService, Path],
+ case: SceneCase,
+) -> None:
+ service, user_root = mode_service
+ catalog = AssistantCapabilityCatalog(service).snapshot()
+ registered = {
+ str(item["id"]): item
+ for item in catalog["capabilities"]
+ if isinstance(item, dict)
+ }
+ assert case.capability in registered
+ assert registered[case.capability]["assistant_callable"] is True
+
+ result = _preview_without_hardware(
+ service,
+ f"构建一个{case.mode_name}",
+ conversation_id=f"conv-typed-scene-{case.case_id}",
+ assistant_plan=_scene_plan(
+ _scene_request(case.capability, case.target, case.parameters)
+ ),
+ )
+
+ draft = _assert_successful_preview(result, name=case.mode_name, operation_count=1)
+ assert draft["generation_source"] == "codex_scene_composition"
+ _assert_parameter_subset(
+ result,
+ case.capability,
+ case.target,
+ case.parameters,
+ )
+ assert not list(user_root.rglob("*.yaml"))
+
+
+BOUNDARY_SCENE_CASES = (
+ SceneCase("temperature-min", "温度下界模式", "climate.set_temperature", "vehicle", {"temperature_c": 18}),
+ SceneCase("temperature-max", "温度上界模式", "climate.set_temperature", "vehicle", {"temperature_c": 32}),
+ SceneCase("fan-min", "微风下界模式", "climate.set_fan_level", "vehicle", {"level": 1}),
+ SceneCase("fan-max", "强风上界模式", "climate.set_fan_level", "vehicle", {"level": 7}),
+ SceneCase("shade-min", "遮阳下界模式", "roof.set_shade_position", "vehicle", {"position_pct": 0}),
+ SceneCase("shade-max", "遮阳上界模式", "roof.set_shade_position", "vehicle", {"position_pct": 100}),
+ SceneCase("window-min", "车窗下界模式", "window.set_position", "front_left", {"position_pct": 0}),
+ SceneCase("window-max", "车窗上界模式", "window.set_position", "front_left", {"position_pct": 100}),
+ SceneCase("brightness-min", "暗光下界模式", "ambient_light.set_brightness_level", "vehicle", {"brightness_level": 1}),
+ SceneCase("brightness-max", "亮光上界模式", "ambient_light.set_brightness_level", "vehicle", {"brightness_level": 5}),
+ SceneCase("color-index-min", "色号下界模式", "ambient_light.set_color_index", "vehicle", {"color_index": 1}),
+ SceneCase("color-index-max", "色号上界模式", "ambient_light.set_color_index", "vehicle", {"color_index": 127}),
+ SceneCase("outlet-min", "风口下界模式", "outlet.set_position_percent", "driver_left", {"horizontal_pct": 0, "vertical_pct": 0}),
+ SceneCase("outlet-max", "风口上界模式", "outlet.set_position_percent", "driver_left", {"horizontal_pct": 100, "vertical_pct": 100}),
+ SceneCase("heating-min", "暖座下界模式", "seat.set_heating", "passenger", {"state": "on", "level": 1}),
+ # The passenger CAN0 reusable chain is physically bounded to levels 1-3.
+ # Levels 4-5 remain declared by the broad domain catalog but currently do
+ # not compile into a reusable Denza N7 mode.
+ SceneCase("heating-max", "暖座上界模式", "seat.set_heating", "passenger", {"state": "on", "level": 3}),
+)
+
+
+@pytest.mark.parametrize("case", BOUNDARY_SCENE_CASES, ids=lambda case: case.case_id)
+def test_scene_parameter_inclusive_boundaries_compile_without_hardware(
+ mode_service: tuple[SeatDemoService, Path],
+ case: SceneCase,
+) -> None:
+ service, user_root = mode_service
+ result = _preview_without_hardware(
+ service,
+ f"构建一个{case.mode_name}",
+ conversation_id=f"conv-boundary-{case.case_id}",
+ assistant_plan=_scene_plan(
+ _scene_request(case.capability, case.target, case.parameters)
+ ),
+ )
+
+ _assert_successful_preview(result, name=case.mode_name, operation_count=1)
+ _assert_parameter_subset(result, case.capability, case.target, case.parameters)
+ assert not list(user_root.rglob("*.yaml"))
+
+
+BASE_MODE_NAME = "参数基线模式"
+BASE_REQUESTS = (
+ _scene_request("climate.set_temperature", "vehicle", {"temperature_c": 23}),
+ _scene_request("climate.set_fan_level", "vehicle", {"level": 2}),
+ _scene_request("climate.set_cycle_mode", "vehicle", {"mode": "internal"}),
+ _scene_request("roof.set_shade_position", "vehicle", {"position_pct": 30}),
+ _scene_request(
+ "ambient_light.set_illumination",
+ "vehicle",
+ {"color_hex": "#0000FF", "brightness_pct": 50},
+ ),
+ _scene_request("window.set_position", "front_right", {"position_pct": 25}),
+ _scene_request("seat.set_heating", "passenger", {"state": "on", "level": 2}),
+ _scene_request(
+ "outlet.set_position_percent",
+ "driver_left",
+ {"horizontal_pct": 25, "vertical_pct": 75},
+ ),
+)
+
+
+def _build_parameter_base(
+ service: SeatDemoService,
+ *,
+ conversation_id: str,
+) -> dict[str, Any]:
+ result = _preview_without_hardware(
+ service,
+ f"构建一个{BASE_MODE_NAME}",
+ conversation_id=conversation_id,
+ assistant_plan=_scene_plan(*BASE_REQUESTS),
+ )
+ _assert_successful_preview(
+ result,
+ name=BASE_MODE_NAME,
+ operation_count=len(BASE_REQUESTS),
+ )
+ return result
+
+
+@pytest.mark.parametrize(
+ ("message", "expected_name"),
+ (
+ ("麻烦帮我创建一个晨间一号模式", "晨间一号模式"),
+ ("给我新建一套午后二号模式", "午后二号模式"),
+ ("请帮忙新增夜间三号模式", "夜间三号模式"),
+ ("生成一个周末四号模式", "周末四号模式"),
+ ),
+)
+def test_name_only_creation_verbs_counters_and_politeness_share_one_route(
+ mode_service: tuple[SeatDemoService, Path],
+ message: str,
+ expected_name: str,
+) -> None:
+ service, user_root = mode_service
+ result = _preview_without_hardware(
+ service,
+ message,
+ conversation_id="conv-create-language-" + str(abs(hash(message)))[:12],
+ assistant_plan=_scene_plan(
+ _scene_request("climate.set_temperature", "vehicle", {"temperature_c": 24})
+ ),
+ )
+
+ draft = _assert_successful_preview(result, name=expected_name, operation_count=1)
+ assert draft["generation_source"] == "codex_scene_composition"
+ assert not list(user_root.rglob("*.yaml"))
+
+
+DRAFT_CHANGE_CASES = (
+ DraftChangeCase(
+ "integer-temperature",
+ "把参数基线模式的空调温度改为26度",
+ "climate.set_temperature",
+ "vehicle",
+ {"temperature_c": 26},
+ ),
+ DraftChangeCase(
+ "percentage-shade",
+ "请帮我把参数基线模式里的遮阳帘开到65%",
+ "roof.set_shade_position",
+ "vehicle",
+ {"position_pct": 65},
+ ),
+ DraftChangeCase(
+ "color-pair",
+ "将参数基线模式中的氛围灯改为红色且亮度40%",
+ "ambient_light.set_illumination",
+ "vehicle",
+ {"color_hex": "#FF0000", "brightness_pct": 40},
+ ),
+ DraftChangeCase(
+ "fan-level",
+ "参数基线模式,空调风量调到5档",
+ "climate.set_fan_level",
+ "vehicle",
+ {"level": 5},
+ ),
+ DraftChangeCase(
+ "scoped-window",
+ "把参数基线模式中的右前车窗开到60%",
+ "window.set_position",
+ "front_right",
+ {"position_pct": 60},
+ ),
+ DraftChangeCase(
+ "seat-state-level",
+ "把参数基线模式里的副驾座椅加热调到3档",
+ "seat.set_heating",
+ "passenger",
+ {"state": "on", "level": 3},
+ ),
+ DraftChangeCase(
+ "cycle-enum",
+ "修改参数基线模式,空调切换为外循环",
+ "climate.set_cycle_mode",
+ "vehicle",
+ {"mode": "external"},
+ ),
+ DraftChangeCase(
+ "outlet-two-axis",
+ "把参数基线模式里的主驾左风口调到水平60%垂直40%",
+ "outlet.set_position_percent",
+ "driver_left",
+ {"horizontal_pct": 60, "vertical_pct": 40},
+ ),
+)
+
+
+@pytest.mark.parametrize("case", DRAFT_CHANGE_CASES, ids=lambda case: case.case_id)
+def test_pending_draft_can_replace_each_parameter_type_and_preserve_siblings(
+ mode_service: tuple[SeatDemoService, Path],
+ case: DraftChangeCase,
+) -> None:
+ service, user_root = mode_service
+ conversation_id = f"conv-draft-change-{case.case_id}"
+ built = _build_parameter_base(service, conversation_id=conversation_id)
+ before = _action_map(built)
+
+ revised = _preview_without_hardware(
+ service,
+ case.message,
+ conversation_id=conversation_id,
+ assistant_plan=_scene_plan(
+ _scene_request(case.capability, case.target, case.parameters)
+ ),
+ draft_context=_draft_reference(built),
+ )
+
+ source_draft = built["mode_profile_draft"]
+ draft = _assert_successful_preview(
+ revised,
+ name=BASE_MODE_NAME,
+ operation_count=len(BASE_REQUESTS),
+ )
+ assert draft["generation_source"] == "draft_revision"
+ assert draft["draft_revision"] == source_draft["draft_revision"] + 1
+ assert draft["revises_source_request_id"] == source_draft["source_request_id"]
+ assert draft["requested_operation_count"] == 1
+ assert draft["changed_operation_count"] == 1
+ _assert_parameter_subset(
+ revised,
+ case.capability,
+ case.target,
+ case.parameters,
+ )
+ after = _action_map(revised)
+ changed_key = (case.capability, case.target)
+ assert set(after) == set(before)
+ assert {
+ key: parameters
+ for key, parameters in after.items()
+ if key != changed_key
+ } == {
+ key: parameters
+ for key, parameters in before.items()
+ if key != changed_key
+ }
+ assert not list(user_root.rglob("*.yaml"))
+
+
+def test_pending_draft_can_append_one_new_capability_without_replacing_siblings(
+ mode_service: tuple[SeatDemoService, Path],
+) -> None:
+ service, user_root = mode_service
+ conversation_id = "conv-draft-append-capability"
+ built = _build_parameter_base(service, conversation_id=conversation_id)
+ before = _action_map(built)
+ revised = _preview_without_hardware(
+ service,
+ "把参数基线模式里的氛围灯关闭",
+ conversation_id=conversation_id,
+ assistant_plan=_scene_plan(
+ _scene_request("ambient_light.set_power", "vehicle", {"state": "off"})
+ ),
+ draft_context=_draft_reference(built),
+ )
+
+ draft = _assert_successful_preview(
+ revised,
+ name=BASE_MODE_NAME,
+ operation_count=len(BASE_REQUESTS) + 1,
+ )
+ assert draft["requested_operation_count"] == 1
+ assert draft["changed_operation_count"] == 1
+ after = _action_map(revised)
+ assert after[("ambient_light.set_power", "vehicle")] == {"state": "off"}
+ assert {key: after[key] for key in before} == before
+ assert not list(user_root.rglob("*.yaml"))
+
+
+def test_repeating_the_existing_value_is_a_safe_noop_revision(
+ mode_service: tuple[SeatDemoService, Path],
+) -> None:
+ service, user_root = mode_service
+ conversation_id = "conv-draft-noop-revision"
+ built = _build_parameter_base(service, conversation_id=conversation_id)
+ revised = _preview_without_hardware(
+ service,
+ "把参数基线模式的空调温度调到23度",
+ conversation_id=conversation_id,
+ assistant_plan=_scene_plan(
+ _scene_request("climate.set_temperature", "vehicle", {"temperature_c": 23})
+ ),
+ draft_context=_draft_reference(built),
+ )
+
+ draft = _assert_successful_preview(
+ revised,
+ name=BASE_MODE_NAME,
+ operation_count=len(BASE_REQUESTS),
+ )
+ assert draft["draft_revision"] == 2
+ assert draft["requested_operation_count"] == 1
+ assert draft["changed_operation_count"] == 0
+ assert _action_map(revised) == _action_map(built)
+ assert not list(user_root.rglob("*.yaml"))
+
+
+@pytest.mark.parametrize(
+ "message",
+ (
+ "把参数基线模式的空调温度调整到26度",
+ "请帮我 把 参数基线模式 中的 空调温度设置为26度",
+ "修改参数基线模式,空调温度调至26℃",
+ "参数基线模式,空调温度调成26度。",
+ "麻烦将参数基线模式里的空调温度变更为26度",
+ ),
+)
+def test_draft_update_word_order_synonyms_and_politeness_keep_one_meaning(
+ mode_service: tuple[SeatDemoService, Path],
+ message: str,
+) -> None:
+ service, user_root = mode_service
+ conversation_id = "conv-language-" + str(abs(hash(message)))[:12]
+ built = _build_parameter_base(service, conversation_id=conversation_id)
+ revised = _preview_without_hardware(
+ service,
+ message,
+ conversation_id=conversation_id,
+ assistant_plan=_scene_plan(
+ _scene_request(
+ "climate.set_temperature",
+ "vehicle",
+ {"temperature_c": 26},
+ )
+ ),
+ draft_context=_draft_reference(built),
+ )
+
+ _assert_successful_preview(
+ revised,
+ name=BASE_MODE_NAME,
+ operation_count=len(BASE_REQUESTS),
+ )
+ assert revised["mode_profile_draft"]["requested_operation_count"] == 1
+ assert revised["mode_profile_draft"]["changed_operation_count"] == 1
+ _assert_parameter_subset(
+ revised,
+ "climate.set_temperature",
+ "vehicle",
+ {"temperature_c": 26},
+ )
+ assert not list(user_root.rglob("*.yaml"))
+
+
+def test_multi_action_revision_is_atomic_and_preserves_every_unmentioned_sibling(
+ mode_service: tuple[SeatDemoService, Path],
+) -> None:
+ service, user_root = mode_service
+ conversation_id = "conv-multi-action-revision"
+ built = _build_parameter_base(service, conversation_id=conversation_id)
+ before = _action_map(built)
+ replacements = (
+ _scene_request("climate.set_temperature", "vehicle", {"temperature_c": 27}),
+ _scene_request("climate.set_fan_level", "vehicle", {"level": 6}),
+ _scene_request("roof.set_shade_position", "vehicle", {"position_pct": 10}),
+ )
+
+ revised = _preview_without_hardware(
+ service,
+ "把参数基线模式的空调温度调到27度、风量调到6档,遮阳帘开到10%",
+ conversation_id=conversation_id,
+ assistant_plan=_scene_plan(*replacements),
+ draft_context=_draft_reference(built),
+ )
+
+ _assert_successful_preview(
+ revised,
+ name=BASE_MODE_NAME,
+ operation_count=len(BASE_REQUESTS),
+ )
+ assert revised["mode_profile_draft"]["requested_operation_count"] == 3
+ assert revised["mode_profile_draft"]["changed_operation_count"] == 3
+ expected_changes = {
+ ("climate.set_temperature", "vehicle"): {"temperature_c": 27},
+ ("climate.set_fan_level", "vehicle"): {"level": 6},
+ ("roof.set_shade_position", "vehicle"): {"position_pct": 10},
+ }
+ after = _action_map(revised)
+ for key, parameters in expected_changes.items():
+ for name, value in parameters.items():
+ assert after[key][name] == value
+ assert {
+ key: parameters for key, parameters in after.items() if key not in expected_changes
+ } == {
+ key: parameters for key, parameters in before.items() if key not in expected_changes
+ }
+ assert not list(user_root.rglob("*.yaml"))
+
+
+def test_continuous_revisions_form_a_lineage_and_keep_prior_changes(
+ mode_service: tuple[SeatDemoService, Path],
+) -> None:
+ service, user_root = mode_service
+ conversation_id = "conv-continuous-draft-lineage"
+ first = _build_parameter_base(service, conversation_id=conversation_id)
+ second = _preview_without_hardware(
+ service,
+ "把参数基线模式的空调温度调到26度",
+ conversation_id=conversation_id,
+ assistant_plan=_scene_plan(
+ _scene_request("climate.set_temperature", "vehicle", {"temperature_c": 26})
+ ),
+ draft_context=_draft_reference(first),
+ )
+ third = _preview_without_hardware(
+ service,
+ "把参数基线模式的遮阳帘开到70%",
+ conversation_id=conversation_id,
+ assistant_plan=_scene_plan(
+ _scene_request("roof.set_shade_position", "vehicle", {"position_pct": 70})
+ ),
+ draft_context=_draft_reference(second),
+ )
+
+ first_draft = first["mode_profile_draft"]
+ second_draft = second["mode_profile_draft"]
+ third_draft = _assert_successful_preview(
+ third,
+ name=BASE_MODE_NAME,
+ operation_count=len(BASE_REQUESTS),
+ )
+ assert [
+ first_draft["draft_revision"],
+ second_draft["draft_revision"],
+ third_draft["draft_revision"],
+ ] == [1, 2, 3]
+ assert second_draft["revises_source_request_id"] == first_draft["source_request_id"]
+ assert third_draft["revises_source_request_id"] == second_draft["source_request_id"]
+ _assert_parameter_subset(
+ third,
+ "climate.set_temperature",
+ "vehicle",
+ {"temperature_c": 26},
+ )
+ _assert_parameter_subset(
+ third,
+ "roof.set_shade_position",
+ "vehicle",
+ {"position_pct": 70},
+ )
+ assert not list(user_root.rglob("*.yaml"))
+
+
+def test_unscoped_revision_rejects_a_multi_target_capability_atomically(
+ mode_service: tuple[SeatDemoService, Path],
+) -> None:
+ service, user_root = mode_service
+ conversation_id = "conv-multizone-ambiguous-revision"
+ built = _preview_without_hardware(
+ service,
+ "构建一个双区矩阵模式",
+ conversation_id=conversation_id,
+ assistant_plan=_scene_plan(
+ _scene_request("climate.set_temperature", "driver", {"temperature_c": 23}),
+ _scene_request("climate.set_temperature", "passenger", {"temperature_c": 24}),
+ _scene_request("climate.set_fan_level", "vehicle", {"level": 2}),
+ ),
+ )
+ _assert_successful_preview(built, name="双区矩阵模式", operation_count=3)
+
+ rejected = _preview_without_hardware(
+ service,
+ "把双区矩阵模式的空调温度调到25度",
+ conversation_id=conversation_id,
+ assistant_plan=_scene_plan(
+ _scene_request("climate.set_temperature", "vehicle", {"temperature_c": 25})
+ ),
+ draft_context=_draft_reference(built),
+ )
+
+ _assert_atomic_failure(rejected, user_root)
+ assert "多个目标" in rejected["mode_creation_failure"]["reason"]
+
+
+def test_explicit_target_revision_changes_only_that_zone(
+ mode_service: tuple[SeatDemoService, Path],
+) -> None:
+ service, user_root = mode_service
+ conversation_id = "conv-multizone-explicit-revision"
+ built = _preview_without_hardware(
+ service,
+ "构建一个双区独立模式",
+ conversation_id=conversation_id,
+ assistant_plan=_scene_plan(
+ _scene_request("climate.set_temperature", "driver", {"temperature_c": 23}),
+ _scene_request("climate.set_temperature", "passenger", {"temperature_c": 24}),
+ ),
+ )
+
+ revised = _preview_without_hardware(
+ service,
+ "把双区独立模式的副驾空调温度调到26度",
+ conversation_id=conversation_id,
+ assistant_plan=_scene_plan(
+ _scene_request("climate.set_temperature", "passenger", {"temperature_c": 26})
+ ),
+ draft_context=_draft_reference(built),
+ )
+
+ _assert_successful_preview(revised, name="双区独立模式", operation_count=2)
+ _assert_parameter_subset(
+ revised,
+ "climate.set_temperature",
+ "driver",
+ {"temperature_c": 23},
+ )
+ _assert_parameter_subset(
+ revised,
+ "climate.set_temperature",
+ "passenger",
+ {"temperature_c": 26},
+ )
+ assert not list(user_root.rglob("*.yaml"))
+
+
+@pytest.mark.parametrize(
+ "message",
+ (
+ "把参数基线模式的空调温度调到26度合适吗?",
+ "参数基线模式里的遮阳帘为什么要关到30%?",
+ "调整参数基线模式的氛围灯设置有哪些?",
+ "把参数基线模式的副驾座椅加热调到4档会怎样?",
+ "参数基线模式的右前车窗开到60%是否安全?",
+ ),
+)
+def test_question_shaped_mode_turns_never_modify_a_draft_or_touch_hardware(
+ mode_service: tuple[SeatDemoService, Path],
+ message: str,
+) -> None:
+ service, user_root = mode_service
+ conversation_id = "conv-question-" + str(abs(hash(message)))[:12]
+ built = _build_parameter_base(service, conversation_id=conversation_id)
+ reference = _draft_reference(built)
+
+ authorization = _authorize_vehicle_request(
+ message,
+ mode_profile_draft_context=reference,
+ )
+ assert authorization.instruction is None
+ assert authorization.reason == "mode_profile_query"
+ result = _preview_without_hardware(
+ service,
+ message,
+ conversation_id=conversation_id,
+ draft_context=reference,
+ )
+
+ assert result["status"] == "needs_confirmation"
+ assert result["orchestration_route"] == "mode_profile_query"
+ assert result["execution_mode"] == "dry_run"
+ assert result["hardware_execution_allowed"] is False
+ assert result.get("mode_profile_draft") is None
+ assert _intent_actions(result) == []
+ assert result["function_configs"] == {}
+ assert result["command_plans"] == {}
+ assert not list(user_root.rglob("*.yaml"))
+
+
+INVALID_PARAMETER_CASES = (
+ SceneCase("temperature-low", "非法低温模式", "climate.set_temperature", "vehicle", {"temperature_c": 17}),
+ SceneCase("temperature-high", "非法高温模式", "climate.set_temperature", "vehicle", {"temperature_c": 33}),
+ SceneCase("fan-low", "非法低风模式", "climate.set_fan_level", "vehicle", {"level": 0}),
+ SceneCase("fan-high", "非法高风模式", "climate.set_fan_level", "vehicle", {"level": 8}),
+ SceneCase("shade-low", "非法遮阳下界模式", "roof.set_shade_position", "vehicle", {"position_pct": -1}),
+ SceneCase("shade-high", "非法遮阳上界模式", "roof.set_shade_position", "vehicle", {"position_pct": 101}),
+ SceneCase("window-target", "非法全窗比例模式", "window.set_position", "all", {"position_pct": 50}),
+ SceneCase("window-high", "非法车窗比例模式", "window.set_position", "front_left", {"position_pct": 101}),
+ SceneCase("color-format", "非法颜色模式", "ambient_light.set_illumination", "vehicle", {"color_hex": "red", "brightness_pct": 50}),
+ SceneCase("color-missing-brightness", "缺少亮度模式", "ambient_light.set_illumination", "vehicle", {"color_hex": "#FF0000"}),
+ SceneCase("brightness-high", "非法亮度模式", "ambient_light.set_brightness_level", "vehicle", {"brightness_level": 6}),
+ SceneCase("outlet-missing-axis", "缺少风口轴模式", "outlet.set_position_percent", "driver_left", {"horizontal_pct": 50}),
+ SceneCase("outlet-high", "非法风口比例模式", "outlet.set_position_percent", "driver_left", {"horizontal_pct": 101, "vertical_pct": 50}),
+ SceneCase("heating-native-gap-4", "暖座四档模式", "seat.set_heating", "passenger", {"state": "on", "level": 4}),
+ SceneCase("heating-native-gap-5", "暖座五档模式", "seat.set_heating", "passenger", {"state": "on", "level": 5}),
+ SceneCase("heating-level", "非法暖座档位模式", "seat.set_heating", "passenger", {"state": "on", "level": 6}),
+ SceneCase("heating-target", "非法暖座目标模式", "seat.set_heating", "driver", {"state": "on", "level": 2}),
+ SceneCase("cycle-enum", "非法循环模式", "climate.set_cycle_mode", "vehicle", {"mode": "mixed"}),
+)
+
+
+@pytest.mark.parametrize("case", INVALID_PARAMETER_CASES, ids=lambda case: case.case_id)
+def test_one_invalid_parameter_discards_valid_siblings_and_never_returns_a_card(
+ mode_service: tuple[SeatDemoService, Path],
+ case: SceneCase,
+) -> None:
+ service, user_root = mode_service
+ result = _preview_without_hardware(
+ service,
+ f"构建一个{case.mode_name}",
+ conversation_id=f"conv-invalid-{case.case_id}",
+ assistant_plan=_scene_plan(
+ _scene_request(
+ "ambient_light.set_power",
+ "vehicle",
+ {"state": "off"},
+ ),
+ _scene_request(case.capability, case.target, case.parameters),
+ ),
+ )
+
+ _assert_atomic_failure(result, user_root)
+
+
+INVALID_DRAFT_CHANGES = (
+ (
+ "temperature-low",
+ "把参数基线模式的空调温度调到17度",
+ _scene_plan(
+ _scene_request("climate.set_temperature", "vehicle", {"temperature_c": 17})
+ ),
+ ),
+ (
+ "temperature-high",
+ "把参数基线模式的空调温度调到33度",
+ _scene_plan(
+ _scene_request("climate.set_temperature", "vehicle", {"temperature_c": 33})
+ ),
+ ),
+ (
+ "fan-high",
+ "把参数基线模式的空调风量调到8档",
+ _scene_plan(
+ _scene_request("climate.set_fan_level", "vehicle", {"level": 8})
+ ),
+ ),
+ (
+ "shade-high",
+ "把参数基线模式的遮阳帘开到101%",
+ _scene_plan(
+ _scene_request("roof.set_shade_position", "vehicle", {"position_pct": 101})
+ ),
+ ),
+ (
+ "window-high",
+ "把参数基线模式的右前车窗开到101%",
+ _scene_plan(
+ _scene_request("window.set_position", "front_right", {"position_pct": 101})
+ ),
+ ),
+ (
+ "heating-native-gap",
+ "把参数基线模式的副驾座椅加热调到4档",
+ _scene_plan(
+ _scene_request(
+ "seat.set_heating",
+ "passenger",
+ {"state": "on", "level": 4},
+ )
+ ),
+ ),
+ (
+ "outlet-missing-axis",
+ "把参数基线模式的主驾左风口水平调到60%",
+ _scene_plan(
+ _scene_request(
+ "outlet.set_position_percent",
+ "driver_left",
+ {"horizontal_pct": 60},
+ )
+ ),
+ ),
+ (
+ "multiple-values",
+ "把参数基线模式的空调温度调到24度和26度",
+ None,
+ ),
+ (
+ "valid-plus-unsupported",
+ "把参数基线模式的空调温度调到26度,同时启动曲速引擎",
+ None,
+ ),
+)
+
+
+@pytest.mark.parametrize(
+ ("case_id", "message", "assistant_plan"),
+ INVALID_DRAFT_CHANGES,
+ ids=[item[0] for item in INVALID_DRAFT_CHANGES],
+)
+def test_invalid_draft_change_is_atomic_and_leaves_the_source_draft_usable(
+ mode_service: tuple[SeatDemoService, Path],
+ case_id: str,
+ message: str,
+ assistant_plan: dict[str, object] | None,
+) -> None:
+ service, user_root = mode_service
+ conversation_id = f"conv-invalid-draft-{case_id}"
+ built = _build_parameter_base(service, conversation_id=conversation_id)
+ before = _action_map(built)
+ source_draft = built["mode_profile_draft"]
+
+ rejected = _preview_without_hardware(
+ service,
+ message,
+ conversation_id=conversation_id,
+ assistant_plan=assistant_plan,
+ draft_context=_draft_reference(built),
+ )
+
+ _assert_atomic_failure(rejected, user_root)
+ assert any(
+ isinstance(issue, dict)
+ and issue.get("code") == "MODE_PROFILE_DRAFT_UPDATE_REJECTED"
+ for issue in rejected.get("validation", [])
+ )
+ with patch.object(
+ service.hardware_execution,
+ "reserve",
+ side_effect=AssertionError("source draft trial reached a hardware lease"),
+ ):
+ trial = service.trial_mode_profile_draft(
+ str(source_draft["name"]),
+ str(source_draft["instruction"]),
+ str(source_draft["source_request_id"]),
+ str(source_draft["vehicle_id"]),
+ )
+ assert trial["status"] == "succeeded"
+ assert trial["execution_mode"] == "dry_run"
+ assert trial["hardware_execution_allowed"] is False
+ assert _action_map(trial) == before
+ assert not list(user_root.rglob("*.yaml"))
+
+
+FORBIDDEN_REUSABLE_ACTIONS = (
+ (
+ "rear-door-lock",
+ _scene_request("door.set_rear_lock", "rear", {"state": "lock"}),
+ ),
+ (
+ "compressor-shortcut",
+ _scene_request("climate.set_ac", "vehicle", {"state": "on"}),
+ ),
+ (
+ "driver-timed-axis",
+ _scene_request(
+ "seat.set_driver_axis_native",
+ "driver",
+ {"axis": "horizontal", "direction": "forward"},
+ duration_ms=500,
+ ),
+ ),
+ (
+ "dynamic-seat",
+ _scene_request("seat.start_dynamic_mode", "passenger", {"mode_id": 1}),
+ ),
+ (
+ "save-preset-side-effect",
+ _scene_request(
+ "seat.save_posture_preset",
+ "passenger",
+ {"preset_name": "不应保存"},
+ operation="save",
+ ),
+ ),
+ (
+ "apply-preset-indirection",
+ _scene_request(
+ "seat.apply_posture_preset",
+ "passenger",
+ {"preset_name": "不存在"},
+ operation="apply",
+ ),
+ ),
+ (
+ "calibrated-outlet",
+ _scene_request(
+ "outlet.set_calibrated_position",
+ "driver_left",
+ {"horizontal_value": -20, "vertical_value": 30},
+ ),
+ ),
+ (
+ "transient-window-motion",
+ _scene_request("window.set_motion", "front_left", {"motion": "stop"}),
+ ),
+ (
+ "transient-shade-motion",
+ _scene_request("roof.set_shade_motion", "vehicle", {"motion": "stop"}),
+ ),
+)
+
+
+@pytest.mark.parametrize(
+ ("case_id", "forbidden_request"),
+ FORBIDDEN_REUSABLE_ACTIONS,
+ ids=[item[0] for item in FORBIDDEN_REUSABLE_ACTIONS],
+)
+def test_registered_but_non_reusable_or_transient_actions_are_rejected_atomically(
+ mode_service: tuple[SeatDemoService, Path],
+ case_id: str,
+ forbidden_request: dict[str, object],
+) -> None:
+ service, user_root = mode_service
+ catalog_ids = {
+ item["id"] for item in AssistantCapabilityCatalog(service).snapshot()["capabilities"]
+ }
+ assert forbidden_request["capability"] in catalog_ids
+ result = _preview_without_hardware(
+ service,
+ f"构建一个禁止动作{case_id}模式",
+ conversation_id=f"conv-forbidden-{case_id}",
+ assistant_plan=_scene_plan(
+ _scene_request("climate.set_temperature", "vehicle", {"temperature_c": 24}),
+ forbidden_request,
+ ),
+ )
+
+ _assert_atomic_failure(result, user_root)
+
+
+def test_duplicate_scene_key_is_rejected_instead_of_last_write_wins(
+ mode_service: tuple[SeatDemoService, Path],
+) -> None:
+ service, user_root = mode_service
+ result = _preview_without_hardware(
+ service,
+ "构建一个冲突温度模式",
+ conversation_id="conv-duplicate-scene-key",
+ assistant_plan=_scene_plan(
+ _scene_request("climate.set_temperature", "vehicle", {"temperature_c": 24}),
+ _scene_request("climate.set_temperature", "vehicle", {"temperature_c": 26}),
+ ),
+ )
+
+ _assert_atomic_failure(result, user_root)
+
+
+MAXIMUM_SCENE_REQUESTS = (
+ _scene_request("climate.set_power", "vehicle", {"state": "on"}),
+ _scene_request("climate.set_temperature", "vehicle", {"temperature_c": 24}),
+ _scene_request("climate.set_fan_level", "vehicle", {"level": 2}),
+ _scene_request("climate.set_cycle_mode", "vehicle", {"mode": "internal"}),
+ _scene_request("climate.set_control_mode", "vehicle", {"mode": "auto"}),
+ _scene_request("climate.set_front_defrost", "vehicle", {"state": "off"}),
+ _scene_request("roof.set_shade_position", "vehicle", {"position_pct": 0}),
+ _scene_request("window.set_position", "front_left", {"position_pct": 0}),
+ _scene_request("window.set_position", "front_right", {"position_pct": 0}),
+ _scene_request("ambient_light.set_power", "vehicle", {"state": "off"}),
+ _scene_request("interior_light.set_indoor_power", "vehicle", {"state": "off"}),
+ _scene_request("interior_light.set_reading_power", "all", {"state": "off"}),
+)
+
+
+def test_scene_request_limit_accepts_exactly_twelve_verified_actions(
+ mode_service: tuple[SeatDemoService, Path],
+) -> None:
+ service, user_root = mode_service
+ assert len(MAXIMUM_SCENE_REQUESTS) == MAX_PLAN_REQUESTS == 12
+ result = _preview_without_hardware(
+ service,
+ "构建一个十二动作模式",
+ conversation_id="conv-twelve-scene-actions",
+ assistant_plan=_scene_plan(*MAXIMUM_SCENE_REQUESTS),
+ )
+
+ _assert_successful_preview(result, name="十二动作模式", operation_count=12)
+ assert not list(user_root.rglob("*.yaml"))
+
+
+def test_scene_request_limit_rejects_thirteen_before_any_hardware_or_persistence(
+ mode_service: tuple[SeatDemoService, Path],
+) -> None:
+ service, user_root = mode_service
+ oversized = (
+ *MAXIMUM_SCENE_REQUESTS,
+ _scene_request("outlet.set_switch_state", "driver_left", {"state": "close"}),
+ )
+ assert len(oversized) == MAX_PLAN_REQUESTS + 1
+
+ with pytest.raises(ValueError, match="too many requests"):
+ _preview_without_hardware(
+ service,
+ "构建一个十三动作模式",
+ conversation_id="conv-thirteen-scene-actions",
+ assistant_plan=_scene_plan(*oversized),
+ )
+ assert not list(user_root.rglob("*.yaml"))
+
+
+def test_known_mode_name_reuses_repository_profile_even_if_a_plan_is_supplied(
+ mode_service: tuple[SeatDemoService, Path],
+) -> None:
+ service, user_root = mode_service
+ nap = service.mode_profiles.get_profile("nap", "denza_n7")
+ result = _preview_without_hardware(
+ service,
+ "构建一个小憩模式",
+ conversation_id="conv-known-mode-exact-reuse-matrix",
+ assistant_plan=_scene_plan(
+ _scene_request("climate.set_temperature", "vehicle", {"temperature_c": 31})
+ ),
+ )
+
+ draft = _assert_successful_preview(
+ result,
+ name=nap.name,
+ operation_count=len(nap.operations),
+ )
+ assert draft["generation_source"] == "registered_mode_profile"
+ assert draft["existing_mode_id"] == "nap"
+ expected = {
+ (operation.capability, operation.target_scope): operation.parameters
+ for operation in nap.operations
+ }
+ actual = _action_map(result)
+ for (capability, target), parameters in expected.items():
+ rendered_target = "passenger" if target == "front_right" and capability.startswith("seat.") else target
+ for name, value in parameters.items():
+ assert actual[(capability, rendered_target)][name] == value
+ assert not list(user_root.rglob("*.yaml"))
diff --git a/tests/integration/test_named_mode_inference_regression.py b/tests/integration/test_named_mode_inference_regression.py
index c02cc32..ba5b1c5 100644
--- a/tests/integration/test_named_mode_inference_regression.py
+++ b/tests/integration/test_named_mode_inference_regression.py
@@ -95,6 +95,51 @@ LUNCH_BREAK_SCENE_PLAN = {
"confidence": 0.92,
}
+VIEWING_SCENE_PLAN = {
+ "detected_domains": ["ambient_light", "roof", "climate", "interior_light"],
+ "requests": [
+ _scene_request(
+ "ambient_light",
+ "ambient_light.set_mode",
+ "vehicle",
+ {"mode": "cinema"},
+ ),
+ _scene_request(
+ "roof",
+ "roof.set_shade_position",
+ "vehicle",
+ {"position_pct": 0},
+ ),
+ _scene_request(
+ "climate",
+ "climate.set_temperature",
+ "vehicle",
+ {"temperature_c": 24},
+ ),
+ _scene_request(
+ "climate",
+ "climate.set_fan_level",
+ "vehicle",
+ {"level": 2},
+ ),
+ _scene_request(
+ "climate",
+ "climate.set_cycle_mode",
+ "vehicle",
+ {"mode": "internal"},
+ ),
+ _scene_request(
+ "interior_light",
+ "interior_light.set_indoor_power",
+ "vehicle",
+ {"state": "off"},
+ ),
+ ],
+ "ambiguities": [],
+ "decision_summary": ["组合遮光、低照明和舒适温度的观影场景。"],
+ "confidence": 0.94,
+}
+
@pytest.fixture
def vehicle_service(tmp_path: Path) -> SeatDemoService:
@@ -124,6 +169,20 @@ def _build_nap_mode(service: SeatDemoService, conversation_id: str) -> dict[str,
)
+def _build_viewing_mode(
+ service: SeatDemoService,
+ conversation_id: str,
+) -> dict[str, object]:
+ return service.preview_native_can(
+ "构建一个观影模式",
+ provider="local",
+ conversation_id=conversation_id,
+ execution_mode="hardware",
+ vehicle_id="denza_n7",
+ assistant_plan=VIEWING_SCENE_PLAN,
+ )
+
+
def _intent_actions(result: dict[str, object]) -> list[dict[str, object]]:
intent_ir = result.get("intent_ir")
assert isinstance(intent_ir, dict)
@@ -455,6 +514,250 @@ def test_pending_draft_can_be_revised_repeatedly_from_latest_server_artifact(
) == 26
+def test_viewing_draft_shade_close_then_100_percent_is_an_atomic_dry_run_chain(
+ vehicle_service: SeatDemoService,
+) -> None:
+ conversation_id = "conv-viewing-shade-revision-chain"
+ built = _build_viewing_mode(vehicle_service, conversation_id)
+ assert built["status"] == "succeeded"
+ assert built["mode_profile_draft"]["operation_count"] == 6
+
+ def semantic_actions(result: dict[str, object]) -> dict[tuple[str, str, str], object]:
+ return {
+ (
+ str(action["domain"]),
+ str(action["capability"]),
+ str(
+ action["target"].get("instance")
+ or action["target"].get("zone")
+ ),
+ ): action["parameters"]
+ for action in _intent_actions(result)
+ }
+
+ with patch.object(
+ vehicle_service.hardware_execution,
+ "reserve",
+ side_effect=AssertionError("viewing draft revision reached hardware lease"),
+ ):
+ closed = vehicle_service.preview_native_can(
+ "把观影模式的遮阳帘变成关闭状态。",
+ provider="local",
+ conversation_id=conversation_id,
+ execution_mode="hardware",
+ vehicle_id="denza_n7",
+ mode_profile_draft_context=_draft_reference(built),
+ )
+ opened = vehicle_service.preview_native_can(
+ "把观影模式的遮阳帘变成100%",
+ provider="local",
+ conversation_id=conversation_id,
+ execution_mode="hardware",
+ vehicle_id="denza_n7",
+ mode_profile_draft_context=_draft_reference(closed),
+ )
+
+ built_actions = semantic_actions(built)
+ closed_actions = semantic_actions(closed)
+ opened_actions = semantic_actions(opened)
+ shade_key = ("roof", "roof.set_shade_position", "vehicle")
+
+ assert closed["status"] == opened["status"] == "succeeded"
+ assert closed["execution_mode"] == opened["execution_mode"] == "dry_run"
+ assert closed["hardware_execution_allowed"] is False
+ assert opened["hardware_execution_allowed"] is False
+ assert closed["orchestration_route"] == "mode_profile_create_preview"
+ assert opened["orchestration_route"] == "mode_profile_create_preview"
+ assert len(built_actions) == len(closed_actions) == len(opened_actions) == 6
+ assert closed_actions[shade_key] == {"position_pct": 0}
+ assert opened_actions[shade_key] == {"position_pct": 100}
+ assert {key: value for key, value in closed_actions.items() if key != shade_key} == {
+ key: value for key, value in built_actions.items() if key != shade_key
+ }
+ assert {key: value for key, value in opened_actions.items() if key != shade_key} == {
+ key: value for key, value in built_actions.items() if key != shade_key
+ }
+
+ closed_draft = closed["mode_profile_draft"]
+ opened_draft = opened["mode_profile_draft"]
+ assert closed_draft["draft_revision"] == 2
+ assert closed_draft["requested_operation_count"] == 1
+ assert closed_draft["changed_operation_count"] == 0
+ assert opened_draft["draft_revision"] == 3
+ assert opened_draft["revises_source_request_id"] == closed_draft[
+ "source_request_id"
+ ]
+ assert opened_draft["requested_operation_count"] == 1
+ assert opened_draft["changed_operation_count"] == 1
+
+
+@pytest.mark.parametrize(
+ ("change", "expected_position"),
+ (
+ ("遮阳帘调整为1%", 1),
+ ("遮阳帘调为20%", 20),
+ ("遮阳帘改为30%", 30),
+ ("遮阳帘改成40%", 40),
+ ("遮阳帘变为50%", 50),
+ ("遮阳帘变成99%", 99),
+ ("遮阳帘换成100%", 100),
+ ("遮阳帘设置为0%", 0),
+ ("遮阳帘变成打开状态", 100),
+ ("遮阳帘变成半开状态", 50),
+ ),
+)
+def test_viewing_draft_accepts_supported_shade_absolute_expressions(
+ vehicle_service: SeatDemoService,
+ change: str,
+ expected_position: int,
+) -> None:
+ conversation_id = f"conv-viewing-shade-alias-{expected_position}-{len(change)}"
+ built = _build_viewing_mode(vehicle_service, conversation_id)
+
+ with patch.object(
+ vehicle_service.hardware_execution,
+ "reserve",
+ side_effect=AssertionError("shade draft alias reached hardware lease"),
+ ):
+ revised = vehicle_service.preview_native_can(
+ f"把观影模式的{change}",
+ provider="local",
+ conversation_id=conversation_id,
+ execution_mode="hardware",
+ vehicle_id="denza_n7",
+ mode_profile_draft_context=_draft_reference(built),
+ )
+
+ actions = _intent_actions(revised)
+ assert revised["status"] == "succeeded"
+ assert revised["execution_mode"] == "dry_run"
+ assert revised["hardware_execution_allowed"] is False
+ assert revised["mode_profile_draft"]["operation_count"] == 6
+ assert next(
+ action["parameters"]["position_pct"]
+ for action in actions
+ if action["capability"] == "roof.set_shade_position"
+ ) == expected_position
+
+
+def test_viewing_draft_updates_percent_integer_and_state_in_one_atomic_turn(
+ vehicle_service: SeatDemoService,
+) -> None:
+ conversation_id = "conv-viewing-multi-parameter-revision"
+ built = _build_viewing_mode(vehicle_service, conversation_id)
+
+ with patch.object(
+ vehicle_service.hardware_execution,
+ "reserve",
+ side_effect=AssertionError("multi-parameter draft update reached hardware"),
+ ):
+ revised = vehicle_service.preview_native_can(
+ "把观影模式的遮阳帘变成100%,空调温度变成25度,室内灯变成打开状态",
+ provider="local",
+ conversation_id=conversation_id,
+ execution_mode="hardware",
+ vehicle_id="denza_n7",
+ mode_profile_draft_context=_draft_reference(built),
+ )
+
+ by_capability = {
+ str(action["capability"]): action["parameters"]
+ for action in _intent_actions(revised)
+ }
+ assert revised["status"] == "succeeded"
+ assert revised["execution_mode"] == "dry_run"
+ assert revised["hardware_execution_allowed"] is False
+ assert revised["mode_profile_draft"]["operation_count"] == 6
+ assert revised["mode_profile_draft"]["requested_operation_count"] == 3
+ assert revised["mode_profile_draft"]["changed_operation_count"] == 3
+ assert by_capability["roof.set_shade_position"] == {"position_pct": 100}
+ assert by_capability["climate.set_temperature"] == {"temperature_c": 25}
+ assert by_capability["interior_light.set_indoor_power"] == {"state": "on"}
+ assert by_capability["ambient_light.set_mode"] == {"mode": "cinema"}
+ assert by_capability["climate.set_fan_level"] == {"level": 2}
+ assert by_capability["climate.set_cycle_mode"] == {"mode": "internal"}
+
+
+@pytest.mark.parametrize("position", (-1, 101))
+def test_viewing_draft_rejects_out_of_range_shade_without_partial_evidence(
+ vehicle_service: SeatDemoService,
+ position: int,
+) -> None:
+ conversation_id = f"conv-viewing-invalid-shade-{abs(position)}"
+ built = _build_viewing_mode(vehicle_service, conversation_id)
+
+ with patch.object(
+ vehicle_service.hardware_execution,
+ "reserve",
+ side_effect=AssertionError("invalid shade draft update reached hardware"),
+ ):
+ rejected = vehicle_service.preview_native_can(
+ f"把观影模式的遮阳帘变成{position}%",
+ provider="local",
+ conversation_id=conversation_id,
+ execution_mode="hardware",
+ vehicle_id="denza_n7",
+ mode_profile_draft_context=_draft_reference(built),
+ )
+
+ assert rejected["status"] == "needs_confirmation"
+ assert rejected["execution_mode"] == "dry_run"
+ assert rejected["hardware_execution_allowed"] is False
+ assert rejected["orchestration_route"] == "mode_profile_create_preview"
+ assert rejected.get("mode_profile_draft") is None
+ assert rejected["function_configs"] == {}
+ assert rejected["command_plans"] == {}
+ assert rejected["mode_creation_failure"]["code"] == (
+ "MODE_PROFILE_DRAFT_UPDATE_REJECTED"
+ )
+
+
+@pytest.mark.parametrize(
+ "message",
+ (
+ "把观影模式的遮阳帘变成100%吗",
+ "把观影模式的遮阳帘调整为50%好吗?",
+ "把观影模式的遮阳帘变成关闭状态吗。",
+ "把观影模式的遮阳帘变成100%合适么",
+ "观影模式里的遮阳帘应该变成多少呢",
+ ),
+)
+def test_viewing_draft_shade_question_never_updates_or_reaches_hardware(
+ vehicle_service: SeatDemoService,
+ message: str,
+) -> None:
+ conversation_id = "conv-viewing-shade-question"
+ built = _build_viewing_mode(vehicle_service, conversation_id)
+ reference = _draft_reference(built)
+
+ assert (
+ parse_mode_profile_update(message, expected_mode_name=reference["name"])
+ is None
+ )
+ assert is_mode_profile_query(message, expected_mode_name=reference["name"])
+ with patch.object(
+ vehicle_service.hardware_execution,
+ "reserve",
+ side_effect=AssertionError("shade question reached hardware lease"),
+ ):
+ result = vehicle_service.preview_native_can(
+ message,
+ provider="local",
+ conversation_id=conversation_id,
+ execution_mode="hardware",
+ vehicle_id="denza_n7",
+ mode_profile_draft_context=reference,
+ )
+
+ assert result["status"] == "needs_confirmation"
+ assert result["execution_mode"] == "dry_run"
+ assert result["hardware_execution_allowed"] is False
+ assert result["orchestration_route"] == "mode_profile_query"
+ assert result.get("mode_profile_draft") is None
+ assert result["function_configs"] == {}
+ assert result["command_plans"] == {}
+
+
def test_pending_draft_reference_is_bound_to_conversation_name_and_vehicle(
vehicle_service: SeatDemoService,
) -> None:
@@ -871,6 +1174,68 @@ def test_codex_composes_an_unregistered_name_only_scene_from_runtime_capabilitie
)
+def test_name_only_scene_label_does_not_leak_device_words_into_local_parser(
+ vehicle_service: SeatDemoService,
+) -> None:
+ external_cycle_plan = {
+ "detected_domains": ["climate"],
+ "requests": [
+ _scene_request(
+ "climate",
+ "climate.set_cycle_mode",
+ "vehicle",
+ {"mode": "external"},
+ )
+ ],
+ "ambiguities": [],
+ "decision_summary": ["使用外循环通风。"],
+ "confidence": 0.96,
+ }
+
+ with patch.object(
+ vehicle_service.hardware_execution,
+ "reserve",
+ side_effect=AssertionError("name-only scene reached hardware lease"),
+ ):
+ result = vehicle_service.preview_native_can(
+ "构建一个外循环通风模式",
+ provider="local",
+ conversation_id="conv-name-only-label-is-not-an-action",
+ execution_mode="hardware",
+ vehicle_id="denza_n7",
+ assistant_plan=external_cycle_plan,
+ )
+
+ actions = _intent_actions(result)
+ assert result["status"] == "succeeded"
+ assert result["execution_mode"] == "dry_run"
+ assert result["hardware_execution_allowed"] is False
+ assert result["provider_used"] == "local_rules+codex_app_server"
+ assert result["orchestration_route"] == "mode_profile_create_preview"
+ assert result["mode_profile_draft"]["name"] == "外循环通风模式"
+ assert result["mode_profile_draft"]["operation_count"] == 1
+ assert result["mode_profile_draft"]["generation_source"] == (
+ "codex_scene_composition"
+ )
+ assert not result["intent_ir"]["spec"]["ambiguities"]
+ assert [
+ (
+ action["domain"],
+ action["capability"],
+ action["target"],
+ action["parameters"],
+ )
+ for action in actions
+ ] == [
+ (
+ "climate",
+ "climate.set_cycle_mode",
+ {"device": "climate", "zone": "vehicle"},
+ {"mode": "external"},
+ )
+ ]
+
+
def test_codex_scene_composition_still_rejects_capabilities_outside_the_registry(
vehicle_service: SeatDemoService,
) -> None:
diff --git a/tests/unit/assistant_stream_runtime.test.cjs b/tests/unit/assistant_stream_runtime.test.cjs
index a4e88fc..e606711 100644
--- a/tests/unit/assistant_stream_runtime.test.cjs
+++ b/tests/unit/assistant_stream_runtime.test.cjs
@@ -323,6 +323,86 @@ test("a follow-up turn carries only the active authoritative draft reference", a
assert.equal(Object.hasOwn(request.mode_profile_draft_context, "actions"), false);
});
+test("the change-into predicate carries only a same-name opaque draft reference", async () => {
+ const events = modeDraftEvents("req-draft-change-into", 24);
+ const harness = createHarness(events);
+ harness.input.value = "构建一个午休模式";
+ await harness.sandbox.CognitiveAgentAssistant.submit();
+
+ events.splice(0, events.length,
+ { type: "message.accepted", route: "agent", engine: "codex-app-server" },
+ { type: "answer.completed", route: "agent", answer: "已处理" },
+ );
+ harness.input.value = "把午休模式的空调温度变成25度";
+ await harness.sandbox.CognitiveAgentAssistant.submit();
+ harness.input.value = "午休模式的空调温度变成25度了吗?";
+ await harness.sandbox.CognitiveAgentAssistant.submit();
+ harness.input.value = "把露营模式的空调温度变成25度";
+ await harness.sandbox.CognitiveAgentAssistant.submit();
+
+ const sameMode = JSON.parse(harness.requestCalls[1].options.body);
+ const question = JSON.parse(harness.requestCalls[2].options.body);
+ const anotherMode = JSON.parse(harness.requestCalls[3].options.body);
+ assert.deepEqual(
+ Object.keys(sameMode.mode_profile_draft_context).sort(),
+ ["name", "source_request_id", "vehicle_id"],
+ );
+ assert.equal(sameMode.mode_profile_draft_context.name, "午休模式");
+ assert.equal(sameMode.mode_profile_draft_context.source_request_id, "req-draft-change-into");
+ assert.equal(sameMode.mode_profile_draft_context.vehicle_id, "demo-vehicle");
+ assert.equal(Object.hasOwn(question, "mode_profile_draft_context"), false);
+ assert.equal(Object.hasOwn(anotherMode, "mode_profile_draft_context"), false);
+});
+
+test("shade position rows explain the target opening at semantic boundaries", async () => {
+ const positions = [0, 30, 50, 100];
+ const events = [
+ { type: "message.accepted", route: "vehicle_control", engine: "n7web-mode-builder" },
+ {
+ type: "action.completed",
+ success: true,
+ payload: {
+ status: "succeeded",
+ execution_mode: "dry_run",
+ orchestration_route: "mode_profile_create_preview",
+ mode_profile_draft: {
+ name: "遮阳测试模式",
+ instruction: "遮阳帘目标开度测试",
+ operation_count: positions.length,
+ source_request_id: "req-draft-shade-positions",
+ vehicle_id: "demo-vehicle",
+ },
+ actions: positions.map((position) => ({
+ domain: "roof",
+ capability: "roof.set_shade_position",
+ target: { zone: "vehicle" },
+ parameters: { position_pct: position },
+ })),
+ },
+ },
+ { type: "answer.completed", route: "vehicle_control", answer: "模式草稿已生成" },
+ ];
+ const harness = createHarness(events);
+ harness.input.value = "构建一个遮阳测试模式";
+
+ await harness.sandbox.CognitiveAgentAssistant.submit();
+
+ const rows = harness.messages.querySelectorAll(".assistant-mode-draft-row");
+ assert.equal(rows.length, 4);
+ assert.deepEqual(
+ rows.map((row) => ({
+ label: row.children[1].textContent,
+ value: row.children[2].textContent,
+ })),
+ [
+ { label: "遮阳帘", value: "完全关闭(目标开度 0%)" },
+ { label: "遮阳帘", value: "目标开度 30%" },
+ { label: "遮阳帘", value: "半开(目标开度 50%)" },
+ { label: "遮阳帘", value: "完全打开(目标开度 100%)" },
+ ],
+ );
+});
+
test("ordinary questions and updates for another mode do not carry the draft reference", async () => {
const events = modeDraftEvents("req-draft-scoped", 24);
const harness = createHarness(events);
diff --git a/tests/unit/test_assistant_frontend_contract.py b/tests/unit/test_assistant_frontend_contract.py
index d5eb664..8262dd9 100644
--- a/tests/unit/test_assistant_frontend_contract.py
+++ b/tests/unit/test_assistant_frontend_contract.py
@@ -13,15 +13,15 @@ def test_console_contains_ai_assistant_surface():
assert 'aria-label="AI助手对话"' in html
assert 'placeholder="向 AI助手提问或交代工作"' in html
assert 'aria-label="发送消息"' in html
- assert 'href="/assistant-chat.css?v=codex-assistant-23"' in html
- assert 'src="/assistant-chat.js?v=codex-assistant-23"' in html
+ assert 'href="/assistant-chat.css?v=codex-assistant-24"' in html
+ assert 'src="/assistant-chat.js?v=codex-assistant-24"' 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-v272"' in service_worker
- assert '"/assistant-chat.css?v=codex-assistant-23"' in service_worker
- assert '"/assistant-chat.js?v=codex-assistant-23"' in service_worker
+ assert '"/assistant-chat.css?v=codex-assistant-24"' in service_worker
+ assert '"/assistant-chat.js?v=codex-assistant-24"' in service_worker
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
diff --git a/tests/unit/test_can1_semantic_whitelist.py b/tests/unit/test_can1_semantic_whitelist.py
index 6d38d79..7cdf8da 100644
--- a/tests/unit/test_can1_semantic_whitelist.py
+++ b/tests/unit/test_can1_semantic_whitelist.py
@@ -349,6 +349,9 @@ class Can1SemanticWhitelistTests(unittest.TestCase):
("停止遮阳帘", ["shade stop"]),
("遮阳帘开到20%", ["shade position 20"]),
("遮阳帘打开30%", ["shade position 30"]),
+ ("遮阳帘调整为40%", ["shade position 40"]),
+ ("遮阳帘变成50%", ["shade position 50"]),
+ ("把遮阳帘换成100%", ["shade position 100"]),
)
for index, (utterance, expected) in enumerate(cases):
with self.subTest(utterance=utterance):
diff --git a/tests/unit/test_codex_assistant_service.py b/tests/unit/test_codex_assistant_service.py
index 843f4b5..c95e885 100644
--- a/tests/unit/test_codex_assistant_service.py
+++ b/tests/unit/test_codex_assistant_service.py
@@ -468,6 +468,49 @@ def test_pending_mode_draft_update_uses_fast_configuration_route_without_codex()
assert "调整待保存" in result["answer"]
+def test_pending_mode_draft_change_into_percentage_is_a_configuration_update() -> None:
+ context = {
+ "name": "观影模式",
+ "source_request_id": "req-viewing-preview",
+ "vehicle_id": "denza_n7",
+ }
+
+ update = _authorize_vehicle_request(
+ "把观影模式的遮阳帘变成100%",
+ CONTROL_TERMS,
+ context,
+ )
+ question = _authorize_vehicle_request(
+ "把观影模式的遮阳帘变成100%吗",
+ CONTROL_TERMS,
+ context,
+ )
+
+ assert update.reason == "explicit_mode_profile_draft_update"
+ assert question.reason == "mode_profile_query"
+
+
+def test_pending_mode_draft_noop_answer_says_target_was_already_set() -> None:
+ result = _sample_mode_preview_result(
+ "观影模式",
+ generation_source="draft_revision",
+ )
+ draft = result["mode_profile_draft"]
+ assert isinstance(draft, dict)
+ draft.update(
+ {
+ "requested_operation_count": 1,
+ "changed_operation_count": 0,
+ }
+ )
+
+ answer = CodexAssistantService._mode_profile_preview_answer(result, True)
+
+ assert "该项原本已是目标状态" in answer
+ assert "已保持不变并重新校验" in answer
+ assert "已调整待保存" not in answer
+
+
def test_active_draft_context_does_not_hijack_an_ordinary_question() -> None:
client = FakeCodexClient()
assistant = CodexAssistantService(client)
@@ -1037,6 +1080,8 @@ def test_safe_vehicle_result_exposes_mode_preview_without_command_flow() -> None
"vehicle_id": "denza_n7",
"domains": ["roof", "interior_light", "ambient_light"],
"operation_count": 4,
+ "requested_operation_count": 1,
+ "changed_operation_count": 0,
"command_flow": {"raw_can": "must-not-cross-tool-boundary"},
},
"intent_ir": {
@@ -1065,6 +1110,8 @@ def test_safe_vehicle_result_exposes_mode_preview_without_command_flow() -> None
"vehicle_id": "denza_n7",
"domains": ["roof", "interior_light", "ambient_light"],
"operation_count": 4,
+ "requested_operation_count": 1,
+ "changed_operation_count": 0,
}
assert safe["actions"] == [
{
diff --git a/tests/unit/test_energy_monitor_frontend_contract.py b/tests/unit/test_energy_monitor_frontend_contract.py
index 65ceb11..41f2711 100644
--- a/tests/unit/test_energy_monitor_frontend_contract.py
+++ b/tests/unit/test_energy_monitor_frontend_contract.py
@@ -36,7 +36,7 @@ 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-23") < html.index(
+ assert html.index("/assistant-chat.js?v=codex-assistant-24") < html.index(
"/energy-monitor.js?v=energy-monitor-4"
)
assert 'const CACHE_VERSION = "cognitive-agent-pad-v272"' in service_worker