fix: harden conversational mode draft revisions
This commit is contained in:
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:
|
||||
|
||||
Reference in New Issue
Block a user