fix: harden conversational mode draft revisions
This commit is contained in:
@@ -168,11 +168,11 @@
|
|||||||
if (!compact || !context?.name) return false;
|
if (!compact || !context?.name) return false;
|
||||||
const politePrefix = "(?:(?:请|麻烦)(?:帮我|帮忙|给我)?|帮我|帮忙|给我)?";
|
const politePrefix = "(?:(?:请|麻烦)(?:帮我|帮忙|给我)?|帮我|帮忙|给我)?";
|
||||||
const prefixUpdateVerb = "(?:修改一下|修改|调整|更改|更新|改一下|变更)";
|
const prefixUpdateVerb = "(?:修改一下|修改|调整|更改|更新|改一下|变更)";
|
||||||
const changePredicate = "(?:修改|改成|改为|更改|更新|变更|变为|换成|调整|调节|调高|调低|调到|调成|调为|调至|设置|设为|设成|设到|设至|打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|复位|半开|开到|关到|拉开|拉上|上锁|解锁)";
|
const changePredicate = "(?:修改|改成|改为|更改|更新|变更|变为|变成|换成|调整|调节|调高|调低|调到|调成|调为|调至|设置|设为|设成|设到|设至|打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|复位|半开|开到|关到|拉开|拉上|上锁|解锁)";
|
||||||
const modeName = "([^,。!?!?、;;::]{1,64}?模式)";
|
const modeName = "([^,。!?!?、;;::]{1,64}?模式)";
|
||||||
const modePossessiveBoundary = "(?:中的|里的|内的|之中的|的|中|里|内)";
|
const modePossessiveBoundary = "(?:中的|里的|内的|之中的|的|中|里|内)";
|
||||||
const modeMemberBoundary = `(?:${modePossessiveBoundary}|[,,::])`;
|
const modeMemberBoundary = `(?:${modePossessiveBoundary}|[,,::])`;
|
||||||
const question = /为什么|什么|怎么|如何|介绍|解释|作用|区别|原理|哪些|多少|几(?:度|档|级|个|项)?|是否|能否|可不可以|合适吗|合理吗|会怎样|列出|告诉我|[??]|吗[。.!!]?$/;
|
const question = /为什么|什么|怎么|如何|介绍|解释|作用|区别|原理|哪些|多少|几(?:度|档|级|个|项)?|是否|能否|可不可以|合适吗|合理吗|会怎样|列出|告诉我|[??]|(?:吗|么|呢)[。.!!]?$/;
|
||||||
const candidates = [
|
const candidates = [
|
||||||
{
|
{
|
||||||
pattern: new RegExp(`^${politePrefix}${prefixUpdateVerb}${modeName}${modeMemberBoundary}[,,::]?(.*)$`),
|
pattern: new RegExp(`^${politePrefix}${prefixUpdateVerb}${modeName}${modeMemberBoundary}[,,::]?(.*)$`),
|
||||||
@@ -469,6 +469,18 @@
|
|||||||
on: "开启",
|
on: "开启",
|
||||||
off: "关闭",
|
off: "关闭",
|
||||||
}[String(value)] || String(value || "已配置"));
|
}[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) => {
|
actions.forEach((action, index) => {
|
||||||
if (consumed.has(index)) return;
|
if (consumed.has(index)) return;
|
||||||
const capability = String(action.capability || "");
|
const capability = String(action.capability || "");
|
||||||
@@ -482,7 +494,9 @@
|
|||||||
|
|
||||||
if (capability === "roof.set_shade_motion" || capability === "roof.set_shade_position") {
|
if (capability === "roof.set_shade_motion" || capability === "roof.set_shade_position") {
|
||||||
label = "遮阳帘";
|
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";
|
icon = "blinds";
|
||||||
order = instructionOrder("遮阳帘", "天幕帘");
|
order = instructionOrder("遮阳帘", "天幕帘");
|
||||||
} else if (domain === "window") {
|
} else if (domain === "window") {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-65" />
|
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-65" />
|
||||||
<link rel="stylesheet" href="/self-extension.css?v=gscu-tcp-43" />
|
<link rel="stylesheet" href="/self-extension.css?v=gscu-tcp-43" />
|
||||||
<link rel="stylesheet" href="/speech-input.css?v=sensevoice-7" />
|
<link rel="stylesheet" href="/speech-input.css?v=sensevoice-7" />
|
||||||
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-23" />
|
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-24" />
|
||||||
<link rel="stylesheet" href="/energy-monitor.css?v=energy-monitor-4" />
|
<link rel="stylesheet" href="/energy-monitor.css?v=energy-monitor-4" />
|
||||||
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
|
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
|
||||||
<script src="/vendor/three/0.147.0/three.min.js" defer></script>
|
<script src="/vendor/three/0.147.0/three.min.js" defer></script>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
<script src="/pwa.js?v=android-pad-3" defer></script>
|
<script src="/pwa.js?v=android-pad-3" defer></script>
|
||||||
<script src="/console-v2.js?v=delivery-state-62" defer></script>
|
<script src="/console-v2.js?v=delivery-state-62" defer></script>
|
||||||
<script src="/self-extension.js?v=gscu-tcp-43" defer></script>
|
<script src="/self-extension.js?v=gscu-tcp-43" defer></script>
|
||||||
<script src="/assistant-chat.js?v=codex-assistant-23" defer></script>
|
<script src="/assistant-chat.js?v=codex-assistant-24" defer></script>
|
||||||
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
|
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="console-v2">
|
<body class="console-v2">
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ const APP_SHELL = [
|
|||||||
"/speech-input.css?v=sensevoice-7",
|
"/speech-input.css?v=sensevoice-7",
|
||||||
"/speech-input.js?v=sensevoice-12",
|
"/speech-input.js?v=sensevoice-12",
|
||||||
"/speech-audio-worklet.js?v=sensevoice-5",
|
"/speech-audio-worklet.js?v=sensevoice-5",
|
||||||
"/assistant-chat.css?v=codex-assistant-23",
|
"/assistant-chat.css?v=codex-assistant-24",
|
||||||
"/assistant-chat.js?v=codex-assistant-23",
|
"/assistant-chat.js?v=codex-assistant-24",
|
||||||
"/energy-monitor.css?v=energy-monitor-4",
|
"/energy-monitor.css?v=energy-monitor-4",
|
||||||
"/energy-monitor.js?v=energy-monitor-4",
|
"/energy-monitor.js?v=energy-monitor-4",
|
||||||
"/assets/pets/self-extension-cat.png?v=3",
|
"/assets/pets/self-extension-cat.png?v=3",
|
||||||
|
|||||||
@@ -983,6 +983,22 @@ class CodexAssistantService:
|
|||||||
if draft.get("existing_mode_id")
|
if draft.get("existing_mode_id")
|
||||||
else "确认后可保存为功能模式。"
|
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 (
|
return (
|
||||||
f"已调整待保存的“{name}”模式,并重新校验全部"
|
f"已调整待保存的“{name}”模式,并重新校验全部"
|
||||||
f"{operation_count}项组合动作。原草稿未被保存或执行,"
|
f"{operation_count}项组合动作。原草稿未被保存或执行,"
|
||||||
@@ -1839,6 +1855,8 @@ class CodexAssistantService:
|
|||||||
"generation_source",
|
"generation_source",
|
||||||
"draft_revision",
|
"draft_revision",
|
||||||
"revises_source_request_id",
|
"revises_source_request_id",
|
||||||
|
"requested_operation_count",
|
||||||
|
"changed_operation_count",
|
||||||
)
|
)
|
||||||
if key in mode_profile_draft
|
if key in mode_profile_draft
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ _CONTEXTUAL_NAME_FIRST_UPDATE = re.compile(
|
|||||||
r"(?P<change>.+?)[。.!!]?$"
|
r"(?P<change>.+?)[。.!!]?$"
|
||||||
)
|
)
|
||||||
MODE_PROFILE_CHANGE_PREDICATE = re.compile(
|
MODE_PROFILE_CHANGE_PREDICATE = re.compile(
|
||||||
r"修改|改成|改为|更改|更新|变更|变为|换成|"
|
r"修改|改成|改为|更改|更新|变更|变成|变为|换成|"
|
||||||
r"调整|调节|调高|调低|调到|调成|调为|调至|"
|
r"调整|调节|调高|调低|调到|调成|调为|调至|"
|
||||||
r"设置|设为|设成|设到|设至|"
|
r"设置|设为|设成|设到|设至|"
|
||||||
r"打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|"
|
r"打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|"
|
||||||
@@ -40,7 +40,7 @@ MODE_PROFILE_QUERY_CUE = re.compile(
|
|||||||
r"什么|哪些|多少|几(?:度|档|级|个|项)?|是否|是不是|"
|
r"什么|哪些|多少|几(?:度|档|级|个|项)?|是否|是不是|"
|
||||||
r"合适(?:吗)?|合理(?:吗)?|会(?:怎么样|怎样|如何)|"
|
r"合适(?:吗)?|合理(?:吗)?|会(?:怎么样|怎样|如何)|"
|
||||||
r"怎么|如何|列(?:出|一下)|介绍(?:一下)?|说明(?:一下)?|"
|
r"怎么|如何|列(?:出|一下)|介绍(?:一下)?|说明(?:一下)?|"
|
||||||
r"告诉我|查询|查看|有什么作用|是何作用|还是|[??]"
|
r"告诉我|查询|查看|有什么作用|是何作用|还是|[??]|(?:吗|么|呢)$"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1349,7 +1349,8 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
|
|||||||
shade_text = _scoped_text(compact, r"遮阳帘|天幕帘")
|
shade_text = _scoped_text(compact, r"遮阳帘|天幕帘")
|
||||||
if shade_text and _allows_vehicle_wide_target(shade_text):
|
if shade_text and _allows_vehicle_wide_target(shade_text):
|
||||||
position = re.search(
|
position = re.search(
|
||||||
r"(?:位置|开度|调整到|调到|设为|设置为|开到|打开(?:到)?|开启(?:到)?)(\d{1,3})%",
|
r"(?:位置|开度|调整到|调整为|调到|调为|改为|改成|变为|变成|"
|
||||||
|
r"换成|设为|设置为|开到|打开(?:到)?|开启(?:到)?)(\d{1,3})%",
|
||||||
shade_text,
|
shade_text,
|
||||||
)
|
)
|
||||||
if position and int(position[1]) <= 100:
|
if position and int(position[1]) <= 100:
|
||||||
|
|||||||
@@ -4309,7 +4309,7 @@ class SeatDemoService:
|
|||||||
if not change_text:
|
if not change_text:
|
||||||
raise ValueError("请说明要调整的设备和明确目标值。")
|
raise ValueError("请说明要调整的设备和明确目标值。")
|
||||||
parseable_change_text = re.sub(
|
parseable_change_text = re.sub(
|
||||||
r"改为|改成|变更为|换成|变为",
|
r"改为|改成|变更为|换成|变成|变为",
|
||||||
"调整为",
|
"调整为",
|
||||||
change_text,
|
change_text,
|
||||||
)
|
)
|
||||||
@@ -4387,6 +4387,22 @@ class SeatDemoService:
|
|||||||
): item
|
): item
|
||||||
for item in normalized_changes
|
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]] = []
|
merged: list[dict[str, Any]] = []
|
||||||
for operation in base_operations:
|
for operation in base_operations:
|
||||||
key = (
|
key = (
|
||||||
@@ -4397,7 +4413,7 @@ class SeatDemoService:
|
|||||||
merged.append(deepcopy(change_by_key.pop(key, operation)))
|
merged.append(deepcopy(change_by_key.pop(key, operation)))
|
||||||
merged.extend(deepcopy(list(change_by_key.values())))
|
merged.extend(deepcopy(list(change_by_key.values())))
|
||||||
|
|
||||||
return self._process_mode_profile_create(
|
revised_result = self._process_mode_profile_create(
|
||||||
utterance,
|
utterance,
|
||||||
"local",
|
"local",
|
||||||
conversation_id,
|
conversation_id,
|
||||||
@@ -4410,6 +4426,26 @@ class SeatDemoService:
|
|||||||
assistant_plan=self._mode_profile_draft_plan(merged),
|
assistant_plan=self._mode_profile_draft_plan(merged),
|
||||||
source_draft=source_draft,
|
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:
|
except ValueError as exc:
|
||||||
result["status"] = "needs_confirmation"
|
result["status"] = "needs_confirmation"
|
||||||
result["execution_mode"] = "dry_run"
|
result["execution_mode"] = "dry_run"
|
||||||
@@ -7180,6 +7216,20 @@ class SeatDemoService:
|
|||||||
allow_scene_composition: bool = False,
|
allow_scene_composition: bool = False,
|
||||||
) -> tuple[dict[str, Any], str, str | None]:
|
) -> tuple[dict[str, Any], str, str | None]:
|
||||||
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
|
||||||
|
if assistant_plan is not None and allow_scene_composition:
|
||||||
|
# ``utterance`` is a synthesized ``进入<mode name>`` 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)
|
clause_results = self._local_clause_results(utterance, context)
|
||||||
|
|
||||||
trusted_local = self._merge_parsed_results(
|
trusted_local = self._merge_parsed_results(
|
||||||
|
|||||||
@@ -770,6 +770,37 @@ class TestCodexAssistantWebApi:
|
|||||||
assert action["payload"]["hardware_execution_allowed"] is False
|
assert action["payload"]["hardware_execution_allowed"] is False
|
||||||
assert events[0]["engine"] == "n7web-mode-builder"
|
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(
|
def test_active_draft_reference_is_not_forwarded_for_an_unrelated_question(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -656,7 +656,7 @@ operations:
|
|||||||
) as response:
|
) as response:
|
||||||
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
||||||
with self.opener.open(
|
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:
|
) as response:
|
||||||
assistant_script = response.read().decode("utf-8")
|
assistant_script = response.read().decode("utf-8")
|
||||||
with self.opener.open(
|
with self.opener.open(
|
||||||
@@ -718,8 +718,8 @@ operations:
|
|||||||
self.assertNotIn("滚动查看更多", html)
|
self.assertNotIn("滚动查看更多", html)
|
||||||
self.assertIn('aria-label="发送消息"', html)
|
self.assertIn('aria-label="发送消息"', html)
|
||||||
self.assertIn('id="assistantTranscript"', html)
|
self.assertIn('id="assistantTranscript"', html)
|
||||||
self.assertIn('href="/assistant-chat.css?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-23"', 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('href="/energy-monitor.css?v=energy-monitor-4"', html)
|
||||||
self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html)
|
self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html)
|
||||||
self.assertNotIn('"admin"', console_script)
|
self.assertNotIn('"admin"', console_script)
|
||||||
@@ -995,8 +995,8 @@ operations:
|
|||||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v272"', service_worker)
|
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.css?v=delivery-state-65', service_worker)
|
||||||
self.assertIn('/console-v2.js?v=delivery-state-62', 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.css?v=codex-assistant-24', service_worker)
|
||||||
self.assertIn('/assistant-chat.js?v=codex-assistant-23', 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.css?v=energy-monitor-4', service_worker)
|
||||||
self.assertIn('/energy-monitor.js?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)
|
self.assertIn('"/app.js?v=dual-seat-127"', service_worker)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -95,6 +95,51 @@ LUNCH_BREAK_SCENE_PLAN = {
|
|||||||
"confidence": 0.92,
|
"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
|
@pytest.fixture
|
||||||
def vehicle_service(tmp_path: Path) -> SeatDemoService:
|
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]]:
|
def _intent_actions(result: dict[str, object]) -> list[dict[str, object]]:
|
||||||
intent_ir = result.get("intent_ir")
|
intent_ir = result.get("intent_ir")
|
||||||
assert isinstance(intent_ir, dict)
|
assert isinstance(intent_ir, dict)
|
||||||
@@ -455,6 +514,250 @@ def test_pending_draft_can_be_revised_repeatedly_from_latest_server_artifact(
|
|||||||
) == 26
|
) == 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(
|
def test_pending_draft_reference_is_bound_to_conversation_name_and_vehicle(
|
||||||
vehicle_service: SeatDemoService,
|
vehicle_service: SeatDemoService,
|
||||||
) -> None:
|
) -> 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(
|
def test_codex_scene_composition_still_rejects_capabilities_outside_the_registry(
|
||||||
vehicle_service: SeatDemoService,
|
vehicle_service: SeatDemoService,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -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);
|
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 () => {
|
test("ordinary questions and updates for another mode do not carry the draft reference", async () => {
|
||||||
const events = modeDraftEvents("req-draft-scoped", 24);
|
const events = modeDraftEvents("req-draft-scoped", 24);
|
||||||
const harness = createHarness(events);
|
const harness = createHarness(events);
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ def test_console_contains_ai_assistant_surface():
|
|||||||
assert 'aria-label="AI助手对话"' in html
|
assert 'aria-label="AI助手对话"' in html
|
||||||
assert 'placeholder="向 AI助手提问或交代工作"' in html
|
assert 'placeholder="向 AI助手提问或交代工作"' in html
|
||||||
assert 'aria-label="发送消息"' in html
|
assert 'aria-label="发送消息"' in html
|
||||||
assert 'href="/assistant-chat.css?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-23"' 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():
|
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")
|
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 'const CACHE_VERSION = "cognitive-agent-pad-v272"' in service_worker
|
||||||
assert '"/assistant-chat.css?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-23"' 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():
|
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
|
||||||
|
|||||||
@@ -349,6 +349,9 @@ class Can1SemanticWhitelistTests(unittest.TestCase):
|
|||||||
("停止遮阳帘", ["shade stop"]),
|
("停止遮阳帘", ["shade stop"]),
|
||||||
("遮阳帘开到20%", ["shade position 20"]),
|
("遮阳帘开到20%", ["shade position 20"]),
|
||||||
("遮阳帘打开30%", ["shade position 30"]),
|
("遮阳帘打开30%", ["shade position 30"]),
|
||||||
|
("遮阳帘调整为40%", ["shade position 40"]),
|
||||||
|
("遮阳帘变成50%", ["shade position 50"]),
|
||||||
|
("把遮阳帘换成100%", ["shade position 100"]),
|
||||||
)
|
)
|
||||||
for index, (utterance, expected) in enumerate(cases):
|
for index, (utterance, expected) in enumerate(cases):
|
||||||
with self.subTest(utterance=utterance):
|
with self.subTest(utterance=utterance):
|
||||||
|
|||||||
@@ -468,6 +468,49 @@ def test_pending_mode_draft_update_uses_fast_configuration_route_without_codex()
|
|||||||
assert "调整待保存" in result["answer"]
|
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:
|
def test_active_draft_context_does_not_hijack_an_ordinary_question() -> None:
|
||||||
client = FakeCodexClient()
|
client = FakeCodexClient()
|
||||||
assistant = CodexAssistantService(client)
|
assistant = CodexAssistantService(client)
|
||||||
@@ -1037,6 +1080,8 @@ def test_safe_vehicle_result_exposes_mode_preview_without_command_flow() -> None
|
|||||||
"vehicle_id": "denza_n7",
|
"vehicle_id": "denza_n7",
|
||||||
"domains": ["roof", "interior_light", "ambient_light"],
|
"domains": ["roof", "interior_light", "ambient_light"],
|
||||||
"operation_count": 4,
|
"operation_count": 4,
|
||||||
|
"requested_operation_count": 1,
|
||||||
|
"changed_operation_count": 0,
|
||||||
"command_flow": {"raw_can": "must-not-cross-tool-boundary"},
|
"command_flow": {"raw_can": "must-not-cross-tool-boundary"},
|
||||||
},
|
},
|
||||||
"intent_ir": {
|
"intent_ir": {
|
||||||
@@ -1065,6 +1110,8 @@ def test_safe_vehicle_result_exposes_mode_preview_without_command_flow() -> None
|
|||||||
"vehicle_id": "denza_n7",
|
"vehicle_id": "denza_n7",
|
||||||
"domains": ["roof", "interior_light", "ambient_light"],
|
"domains": ["roof", "interior_light", "ambient_light"],
|
||||||
"operation_count": 4,
|
"operation_count": 4,
|
||||||
|
"requested_operation_count": 1,
|
||||||
|
"changed_operation_count": 0,
|
||||||
}
|
}
|
||||||
assert safe["actions"] == [
|
assert safe["actions"] == [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
|
|||||||
):
|
):
|
||||||
assert asset in html
|
assert asset in html
|
||||||
assert f'"{asset}"' in service_worker
|
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"
|
"/energy-monitor.js?v=energy-monitor-4"
|
||||||
)
|
)
|
||||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v272"' in service_worker
|
assert 'const CACHE_VERSION = "cognitive-agent-pad-v272"' in service_worker
|
||||||
|
|||||||
Reference in New Issue
Block a user