fix: harden conversational mode draft revisions

This commit is contained in:
vee
2026-09-08 00:55:45 +08:00
parent f0a248ede3
commit 5e228e12ae
16 changed files with 1862 additions and 23 deletions
+31
View File
@@ -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:
+5 -5
View File
@@ -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)
File diff suppressed because it is too large Load Diff
@@ -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:
@@ -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);
@@ -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():
@@ -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):
@@ -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"] == [
{
@@ -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