1872 lines
63 KiB
Python
1872 lines
63 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import queue
|
|
import threading
|
|
import time
|
|
from collections.abc import Callable, Mapping
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from copy import deepcopy
|
|
|
|
import pytest
|
|
|
|
from cognitive_agent.application.codex_assistant import (
|
|
ActiveCodexTurn,
|
|
CodexAssistantService,
|
|
_mode_profile_create_parts,
|
|
)
|
|
from cognitive_agent.application.scene_composer import (
|
|
scene_required_native_fallbacks,
|
|
scene_semantic_guidance,
|
|
)
|
|
|
|
CONTROL_TERMS = ("空调", "座椅", "车窗", "遮阳帘", "氛围灯", "阅读灯", "出风口")
|
|
|
|
|
|
@pytest.mark.parametrize(("message", "mode_name"), [
|
|
("构建一个观影模式", "观影模式"),
|
|
("为主驾构建一个观影模式", "主驾观影模式"),
|
|
])
|
|
def test_local_first_callbacks_do_not_intercept_scoped_mode_draft_creation(message, mode_name):
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
events = []
|
|
control_calls = []
|
|
|
|
def execute_control(instruction, _progress, plan):
|
|
control_calls.append((instruction, plan))
|
|
return _preview_result(mode_name)
|
|
|
|
result = assistant.stream(
|
|
message, conversation_id="conv-merged-scene-router", principal="test",
|
|
history=[], emit=events.append, execute_control=execute_control,
|
|
preview_control=lambda *_args: pytest.fail("mode previews must not partially execute via local-first controls"),
|
|
supplement_control=lambda *_args: pytest.fail("mode previews must not become unresolved control supplements"),
|
|
control_terms=CONTROL_TERMS,
|
|
capability_snapshot=_catalog_with_templates(_scene_template()),
|
|
scene_capability_context=_scene_context(),
|
|
)
|
|
assert result["success"] is True
|
|
assert result["route"] == "scene_template"
|
|
assert [instruction for instruction, _ in control_calls] == [message]
|
|
assert not client.structured_calls and not client.legacy_calls
|
|
|
|
|
|
def test_local_first_callbacks_preserve_schedule_stop_route_and_answer():
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
events = []
|
|
result = assistant.stream(
|
|
"停止动态模式", conversation_id="conv-merged-schedule-stop", principal="test",
|
|
history=[], emit=events.append,
|
|
execute_control=lambda instruction, _progress: {
|
|
"request_id": "req-stop-merged", "utterance": instruction,
|
|
"status": "succeeded", "execution_mode": "dry_run",
|
|
"orchestration_route": "mode_schedule_stop",
|
|
"schedule_execution": {"status": "idle"},
|
|
},
|
|
preview_control=lambda *_args: pytest.fail("schedule stop must retain its scheduler route"),
|
|
supplement_control=lambda *_args: pytest.fail("schedule stop is not an unresolved control"),
|
|
control_terms=CONTROL_TERMS,
|
|
)
|
|
assert result["success"] is True
|
|
assert "已模拟停止动态模式" in result["answer"]
|
|
assert not client.structured_calls and not client.legacy_calls
|
|
|
|
|
|
def _structured_plan(
|
|
*,
|
|
temperature: float = 24,
|
|
shade_position: int = 100,
|
|
) -> dict[str, object]:
|
|
return {
|
|
"requests": [
|
|
{
|
|
"capability": "climate.set_temperature",
|
|
"target_scope": "vehicle",
|
|
"parameters": [{"name": "temperature_c", "value": temperature}],
|
|
},
|
|
{
|
|
"capability": "roof.set_shade_position",
|
|
"target_scope": "vehicle",
|
|
"parameters": [{"name": "position_pct", "value": shade_position}],
|
|
},
|
|
],
|
|
"ambiguities": [],
|
|
"confidence": 0.94,
|
|
}
|
|
|
|
|
|
def _legacy_plan(*, temperature: float = 24) -> dict[str, object]:
|
|
structured = _structured_plan(temperature=temperature)
|
|
requests = []
|
|
domains = {"climate.set_temperature": "climate", "roof.set_shade_position": "roof"}
|
|
for item in structured["requests"]:
|
|
requests.append(
|
|
{
|
|
"domain": domains[item["capability"]],
|
|
"capability": item["capability"],
|
|
"target_scope": item["target_scope"],
|
|
"operation": "set",
|
|
"duration_ms": None,
|
|
"parameters": item["parameters"],
|
|
"reason": "旧链路候选动作。",
|
|
}
|
|
)
|
|
return {
|
|
"detected_domains": ["climate", "roof"],
|
|
"requests": requests,
|
|
"ambiguities": [],
|
|
"decision_summary": ["旧链路生成候选计划。"],
|
|
"confidence": 0.94,
|
|
}
|
|
|
|
|
|
def _completed_turn(
|
|
thread_id: str,
|
|
turn_id: str,
|
|
answer: str,
|
|
) -> ActiveCodexTurn:
|
|
turn = ActiveCodexTurn(thread_id=thread_id, turn_id=turn_id, events=queue.Queue())
|
|
_finish_turn(turn, answer)
|
|
return turn
|
|
|
|
|
|
def _finish_turn(turn: ActiveCodexTurn, answer: str) -> None:
|
|
item_id = f"answer-{turn.turn_id}"
|
|
turn.events.put(
|
|
{
|
|
"method": "item/started",
|
|
"params": {
|
|
"item": {
|
|
"id": item_id,
|
|
"type": "agentMessage",
|
|
"text": "",
|
|
"phase": "final_answer",
|
|
}
|
|
},
|
|
}
|
|
)
|
|
turn.events.put(
|
|
{
|
|
"method": "item/agentMessage/delta",
|
|
"params": {"itemId": item_id, "delta": answer},
|
|
}
|
|
)
|
|
turn.events.put(
|
|
{
|
|
"method": "item/completed",
|
|
"params": {
|
|
"item": {
|
|
"id": item_id,
|
|
"type": "agentMessage",
|
|
"text": answer,
|
|
"phase": "final_answer",
|
|
}
|
|
},
|
|
}
|
|
)
|
|
turn.events.put(
|
|
{
|
|
"method": "turn/completed",
|
|
"params": {
|
|
"turn": {
|
|
"id": turn.turn_id,
|
|
"status": "completed",
|
|
"items": [],
|
|
"error": None,
|
|
}
|
|
},
|
|
}
|
|
)
|
|
|
|
|
|
class StructuredSceneClient:
|
|
def __init__(self) -> None:
|
|
self.structured_calls: list[dict[str, object]] = []
|
|
self.legacy_calls: list[dict[str, object]] = []
|
|
self.thread_instructions: list[str] = []
|
|
self.finished: list[ActiveCodexTurn] = []
|
|
self.interrupted: list[ActiveCodexTurn] = []
|
|
self.unsubscribed: list[str] = []
|
|
self.structured_answers: queue.Queue[str | BaseException] = queue.Queue()
|
|
self.on_structured_start: Callable[[], None] | None = None
|
|
self.structured_turn_factory: Callable[[str, str], ActiveCodexTurn] | None = None
|
|
self.on_legacy_start: Callable[[str, ActiveCodexTurn], None] | None = None
|
|
self._lock = threading.Lock()
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
return True
|
|
|
|
@property
|
|
def status(self) -> str:
|
|
return "ready"
|
|
|
|
def start(self, _callback_url: str, _callback_token: str) -> None:
|
|
return None
|
|
|
|
def close(self) -> None:
|
|
return None
|
|
|
|
def start_thread(
|
|
self,
|
|
*,
|
|
developer_instructions: str,
|
|
ephemeral: bool = True,
|
|
replace_developer_instructions: bool = False,
|
|
timeout: float | None = None,
|
|
) -> str:
|
|
assert ephemeral is True
|
|
del replace_developer_instructions, timeout
|
|
with self._lock:
|
|
self.thread_instructions.append(developer_instructions)
|
|
return f"thread-{len(self.thread_instructions)}"
|
|
|
|
def start_structured_turn(
|
|
self,
|
|
thread_id: str,
|
|
text: str,
|
|
*,
|
|
application_context: Mapping[str, object] | None = None,
|
|
output_schema: Mapping[str, object],
|
|
timeout: float | None = None,
|
|
) -> ActiveCodexTurn:
|
|
del timeout
|
|
with self._lock:
|
|
call_index = len(self.structured_calls) + 1
|
|
self.structured_calls.append(
|
|
{
|
|
"thread_id": thread_id,
|
|
"text": text,
|
|
"application_context": deepcopy(dict(application_context or {})),
|
|
"output_schema": deepcopy(dict(output_schema)),
|
|
}
|
|
)
|
|
if self.on_structured_start is not None:
|
|
self.on_structured_start()
|
|
turn_id = f"structured-{call_index}"
|
|
if self.structured_turn_factory is not None:
|
|
return self.structured_turn_factory(thread_id, turn_id)
|
|
try:
|
|
answer = self.structured_answers.get_nowait()
|
|
except queue.Empty:
|
|
answer = json.dumps(_structured_plan(), ensure_ascii=False)
|
|
if isinstance(answer, BaseException):
|
|
raise answer
|
|
return _completed_turn(thread_id, turn_id, answer)
|
|
|
|
def start_turn(
|
|
self,
|
|
thread_id: str,
|
|
text: str,
|
|
request_token: str,
|
|
*,
|
|
application_context: Mapping[str, object] | None = None,
|
|
) -> ActiveCodexTurn:
|
|
with self._lock:
|
|
call_index = len(self.legacy_calls) + 1
|
|
self.legacy_calls.append(
|
|
{
|
|
"thread_id": thread_id,
|
|
"text": text,
|
|
"request_token": request_token,
|
|
"application_context": deepcopy(dict(application_context or {})),
|
|
}
|
|
)
|
|
turn = _completed_turn(thread_id, f"legacy-{call_index}", "旧链路已生成模式。")
|
|
if self.on_legacy_start is not None:
|
|
self.on_legacy_start(request_token, turn)
|
|
return turn
|
|
|
|
def finish_turn(
|
|
self,
|
|
turn: ActiveCodexTurn,
|
|
timeout: float | None = None,
|
|
) -> None:
|
|
del timeout
|
|
self.finished.append(turn)
|
|
|
|
def interrupt(
|
|
self,
|
|
turn: ActiveCodexTurn,
|
|
timeout: float | None = None,
|
|
) -> None:
|
|
del timeout
|
|
self.interrupted.append(turn)
|
|
|
|
def unsubscribe_thread(
|
|
self,
|
|
thread_id: str,
|
|
timeout: float | None = None,
|
|
) -> None:
|
|
del timeout
|
|
self.unsubscribed.append(thread_id)
|
|
|
|
|
|
def _scene_context(
|
|
revision: str = "scene-contract-a",
|
|
*,
|
|
vehicle_id: str = "denza_n7",
|
|
) -> dict[str, object]:
|
|
return {
|
|
"schema_version": "n7web-scene-composition/v1",
|
|
"composition_revision": revision,
|
|
"vehicle": {"id": vehicle_id, "name": "腾势 N7"},
|
|
"capabilities": [
|
|
{
|
|
"id": "climate.set_temperature",
|
|
"domain": "climate",
|
|
"target_scopes": ["vehicle", "driver", "passenger"],
|
|
"parameters": [
|
|
{
|
|
"name": "temperature_c",
|
|
"type": "number",
|
|
"required": True,
|
|
"minimum": 16,
|
|
"maximum": 32,
|
|
}
|
|
],
|
|
},
|
|
{
|
|
"id": "roof.set_shade_position",
|
|
"domain": "roof",
|
|
"target_scopes": ["vehicle"],
|
|
"parameters": [
|
|
{
|
|
"name": "position_pct",
|
|
"type": "integer",
|
|
"required": True,
|
|
"minimum": 0,
|
|
"maximum": 100,
|
|
}
|
|
],
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def _catalog(*, vehicle_id: str = "denza_n7") -> dict[str, object]:
|
|
return {
|
|
"catalog_revision": "full-runtime-revision",
|
|
"vehicle": {"id": vehicle_id, "name": "腾势 N7"},
|
|
"modes": [],
|
|
"source_status": {"modes": "ready"},
|
|
}
|
|
|
|
|
|
def _scene_template(
|
|
*,
|
|
template_id: str = "viewing",
|
|
name: str = "观影模式",
|
|
aliases: list[str] | None = None,
|
|
) -> dict[str, object]:
|
|
revision = f"revision-{template_id}"
|
|
plan = _legacy_plan()
|
|
plan["metadata"] = {
|
|
"source": "scene_template",
|
|
"template_id": template_id,
|
|
"template_revision": revision,
|
|
}
|
|
return {
|
|
"id": template_id,
|
|
"name": name,
|
|
"aliases": aliases or ["电影模式"],
|
|
"assistant_callable": True,
|
|
"template_revision": revision,
|
|
"plan": plan,
|
|
}
|
|
|
|
|
|
def _catalog_with_templates(
|
|
*templates: Mapping[str, object],
|
|
modes: list[dict[str, object]] | None = None,
|
|
) -> dict[str, object]:
|
|
catalog = _catalog()
|
|
catalog["modes"] = deepcopy(modes or [])
|
|
catalog["scene_templates"] = [deepcopy(dict(template)) for template in templates]
|
|
catalog["source_status"]["scene_templates"] = "ready"
|
|
return catalog
|
|
|
|
|
|
def _preview_result(
|
|
mode_name: str,
|
|
*,
|
|
operation_count: int = 2,
|
|
success: bool = True,
|
|
) -> dict[str, object]:
|
|
if not success:
|
|
return {
|
|
"request_id": "req-failed",
|
|
"status": "failed",
|
|
"execution_mode": "dry_run",
|
|
"hardware_execution_allowed": False,
|
|
"orchestration_route": "mode_profile_create_preview",
|
|
"validation": [{"code": "invalid_scene", "message": "invalid"}],
|
|
}
|
|
return {
|
|
"request_id": f"req-{mode_name}",
|
|
"status": "succeeded",
|
|
"execution_mode": "dry_run",
|
|
"execution_outcome": "simulated",
|
|
"hardware_execution_allowed": False,
|
|
"orchestration_route": "mode_profile_create_preview",
|
|
"validation": [],
|
|
"intent_ir": {"spec": {"actions": [{}, {}][:operation_count]}},
|
|
"mode_profile_draft": {
|
|
"name": mode_name,
|
|
"instruction": f"进入{mode_name}",
|
|
"source_request_id": f"req-{mode_name}",
|
|
"vehicle_id": "denza_n7",
|
|
"domains": ["climate", "roof"],
|
|
"operation_count": operation_count,
|
|
"generation_source": "codex_scene_composition",
|
|
},
|
|
}
|
|
|
|
|
|
def _invoke(
|
|
assistant: CodexAssistantService,
|
|
message: str,
|
|
executor: Callable[..., dict[str, object]],
|
|
*,
|
|
conversation_id: str,
|
|
principal: str = "alice",
|
|
scene_context: Mapping[str, object] | None = None,
|
|
catalog: Mapping[str, object] | None = None,
|
|
emit_handler: Callable[[dict[str, object]], bool | None] | None = None,
|
|
) -> tuple[dict[str, object], list[dict[str, object]]]:
|
|
events: list[dict[str, object]] = []
|
|
|
|
def emit(event: dict[str, object]) -> bool | None:
|
|
events.append(event)
|
|
return emit_handler(event) if emit_handler is not None else None
|
|
|
|
result = assistant.stream(
|
|
message,
|
|
conversation_id=conversation_id,
|
|
principal=principal,
|
|
history=[],
|
|
emit=emit,
|
|
execute_control=executor,
|
|
capability_snapshot=catalog or _catalog(),
|
|
capability_context={"general": "context"},
|
|
scene_capability_context=scene_context or _scene_context(),
|
|
control_terms=CONTROL_TERMS,
|
|
)
|
|
return result, events
|
|
|
|
|
|
def test_exact_scene_template_hit_skips_codex_and_previews_a_fresh_plan() -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
template = _scene_template()
|
|
observed: list[dict[str, object]] = []
|
|
|
|
def execute(
|
|
instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
assert instruction == "构建一个观影模式"
|
|
observed.append(deepcopy(dict(assistant_plan)))
|
|
# A service callback must not be able to mutate the catalog-owned plan.
|
|
assistant_plan["requests"][0]["parameters"][0]["value"] = 31
|
|
return _preview_result("观影模式")
|
|
|
|
result, events = _invoke(
|
|
assistant,
|
|
"构建一个观影模式",
|
|
execute,
|
|
conversation_id="conv-scene-template-hit",
|
|
catalog=_catalog_with_templates(template),
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert result["route"] == "scene_template"
|
|
assert client.structured_calls == []
|
|
assert client.legacy_calls == []
|
|
assert len(observed) == 1
|
|
assert observed[0]["metadata"] == template["plan"]["metadata"]
|
|
assert template["plan"]["requests"][0]["parameters"][0]["value"] == 24
|
|
assert events[0]["engine"] == "n7web-scene-template"
|
|
|
|
|
|
def test_exact_scene_template_keeps_native_base_and_adds_only_selected_ir_ref() -> None:
|
|
client = StructuredSceneClient()
|
|
capability_ref = "ir:0123456789abcdef01234567"
|
|
client.structured_answers.put(
|
|
json.dumps(
|
|
{
|
|
"requests": [{"capability_ref": capability_ref}],
|
|
"ambiguities": [],
|
|
"confidence": 0.93,
|
|
},
|
|
ensure_ascii=False,
|
|
)
|
|
)
|
|
assistant = CodexAssistantService(client)
|
|
template = _scene_template()
|
|
context = _scene_context()
|
|
context["peripheral_actions"] = [
|
|
{
|
|
"ref": capability_ref,
|
|
"device_label": "氛围灯带",
|
|
"action_label": "设置为红色",
|
|
"semantic_tags": ["浪漫", "红色", "灯光"],
|
|
}
|
|
]
|
|
context["device_inventory"] = [
|
|
{
|
|
"kind": "learned_infrared",
|
|
"id": "romance-light",
|
|
"label": "氛围灯带",
|
|
"action": "设置为红色",
|
|
"availability": "available",
|
|
}
|
|
]
|
|
observed: list[dict[str, object]] = []
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
observed.append(deepcopy(dict(assistant_plan)))
|
|
return _preview_result("观影模式")
|
|
|
|
result, _events = _invoke(
|
|
assistant,
|
|
"构建一个观影模式",
|
|
execute,
|
|
conversation_id="conv-scene-template-ir-augmentation",
|
|
catalog=_catalog_with_templates(template),
|
|
scene_context=context,
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert result["route"] == "scene_template"
|
|
assert len(client.structured_calls) == 1
|
|
assert not client.legacy_calls
|
|
assert observed[0]["requests"][:2] == template["plan"]["requests"]
|
|
assert observed[0]["requests"][2] == {
|
|
"domain": "self_extension",
|
|
"capability": "infrared.emit",
|
|
"target_scope": capability_ref,
|
|
"operation": "emit",
|
|
"duration_ms": None,
|
|
"parameters": [{"name": "capability_ref", "value": capability_ref}],
|
|
"reason": (
|
|
"Scene Composer selected a server-issued learned-device reference; "
|
|
"runtime binding resolution is still required."
|
|
),
|
|
}
|
|
assert "learned-device augmentation" in client.thread_instructions[0]
|
|
|
|
|
|
def _scene_context_with_flashing_light() -> dict[str, object]:
|
|
context = _scene_context()
|
|
context["capabilities"].append(
|
|
{
|
|
"id": "ambient_light.set_power",
|
|
"domain": "ambient_light",
|
|
"target_scopes": ["vehicle"],
|
|
"parameters": [
|
|
{
|
|
"name": "state",
|
|
"type": "string",
|
|
"allowed_values": ["on", "off"],
|
|
"required": True,
|
|
}
|
|
],
|
|
}
|
|
)
|
|
context["peripheral_actions"] = [
|
|
{
|
|
"ref": "ir:flash-open-000000000001",
|
|
"device_label": "闪烁灯",
|
|
"object_names": ["闪烁灯", "闪光灯"],
|
|
"action_label": "打开",
|
|
"action_names": ["打开", "开启", "启动"],
|
|
"semantic_tags": ["闪烁灯", "灯光"],
|
|
},
|
|
{
|
|
"ref": "ir:flash-red-0000000000002",
|
|
"device_label": "闪烁灯",
|
|
"object_names": ["闪烁灯", "闪光灯"],
|
|
"action_label": "切换为红色",
|
|
"action_names": ["切换为红色", "设置为红色"],
|
|
"semantic_tags": ["闪烁灯", "红色", "灯光"],
|
|
},
|
|
{
|
|
"ref": "ir:flash-close-00000000003",
|
|
"device_label": "闪烁灯",
|
|
"object_names": ["闪烁灯", "闪光灯"],
|
|
"action_label": "关闭",
|
|
"action_names": ["关闭", "熄灭"],
|
|
"semantic_tags": ["闪烁灯", "灯光"],
|
|
},
|
|
{
|
|
"ref": "ir:strip-open-000000000001",
|
|
"device_label": "灯带",
|
|
"object_names": ["灯带", "LED 灯条"],
|
|
"action_label": "打开",
|
|
"action_names": ["打开", "开启"],
|
|
"semantic_tags": ["灯带", "灯光"],
|
|
},
|
|
{
|
|
"ref": "ir:strip-red-0000000000002",
|
|
"device_label": "灯带",
|
|
"object_names": ["灯带", "LED 灯条"],
|
|
"action_label": "切换为红色",
|
|
"action_names": ["切换为红色", "设置为红色"],
|
|
"semantic_tags": ["灯带", "红色", "灯光"],
|
|
},
|
|
{
|
|
"ref": "ir:ambient-open-0000000001",
|
|
"device_label": "外设氛围灯",
|
|
"object_names": ["外设氛围灯", "氛围灯"],
|
|
"action_label": "打开",
|
|
"action_names": ["打开", "开启"],
|
|
"semantic_tags": ["氛围灯", "灯光"],
|
|
},
|
|
]
|
|
context["device_inventory"] = [
|
|
{
|
|
"kind": "learned_infrared",
|
|
"id": "toy-light-001",
|
|
"label": "闪烁灯",
|
|
"availability": "available",
|
|
}
|
|
]
|
|
return context
|
|
|
|
|
|
def _peripheral_refs(plan: Mapping[str, object]) -> list[str]:
|
|
return [
|
|
str(parameter.get("value") or "")
|
|
for request in plan.get("requests", [])
|
|
if isinstance(request, Mapping)
|
|
and request.get("domain") == "self_extension"
|
|
for parameter in request.get("parameters", [])
|
|
if isinstance(parameter, Mapping)
|
|
and parameter.get("name") == "capability_ref"
|
|
]
|
|
|
|
|
|
def test_romantic_template_always_adds_authorized_decorative_lights() -> None:
|
|
client = StructuredSceneClient()
|
|
client.structured_answers.put(
|
|
json.dumps(
|
|
{"requests": [], "ambiguities": [], "confidence": 0.82},
|
|
ensure_ascii=False,
|
|
)
|
|
)
|
|
assistant = CodexAssistantService(client)
|
|
template = _scene_template(
|
|
template_id="romantic",
|
|
name="浪漫模式",
|
|
aliases=["约会模式"],
|
|
)
|
|
observed: list[dict[str, object]] = []
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
observed.append(deepcopy(dict(assistant_plan)))
|
|
return _preview_result("浪漫模式")
|
|
|
|
result, _events = _invoke(
|
|
assistant,
|
|
"创建浪漫模式",
|
|
execute,
|
|
conversation_id="conv-romantic-required-flashing-light",
|
|
catalog=_catalog_with_templates(template),
|
|
scene_context=_scene_context_with_flashing_light(),
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert observed[0]["requests"][:2] == template["plan"]["requests"]
|
|
assert any(
|
|
request.get("domain") == "ambient_light"
|
|
for request in observed[0]["requests"]
|
|
)
|
|
assert _peripheral_refs(observed[0]) == [
|
|
"ir:flash-open-000000000001",
|
|
"ir:ambient-open-0000000001",
|
|
"ir:strip-open-000000000001",
|
|
]
|
|
assert "ir:flash-close-00000000003" not in _peripheral_refs(observed[0])
|
|
|
|
|
|
def test_mid_autumn_cloud_composition_adds_required_native_and_external_controls() -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
observed: list[dict[str, object]] = []
|
|
scene_context = _scene_context_with_flashing_light()
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
observed.append(deepcopy(dict(assistant_plan)))
|
|
return _preview_result("中秋欢快模式")
|
|
|
|
result, _events = _invoke(
|
|
assistant,
|
|
"创建中秋欢快模式",
|
|
execute,
|
|
conversation_id="conv-mid-autumn-cloud-peripherals",
|
|
catalog=_catalog_with_templates(_scene_template()),
|
|
scene_context=scene_context,
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert result["route"] == "scene_composer"
|
|
call_context = client.structured_calls[0]["application_context"]
|
|
assert len(call_context["capabilities"]) == len(scene_context["capabilities"])
|
|
assert len(call_context["peripheral_actions"]) == 6
|
|
assert call_context["scene_semantics"]["family"] == "festival_celebration"
|
|
assert call_context["scene_semantics"]["peripheral_policy"][
|
|
"required_refs"
|
|
] == [
|
|
"ir:flash-open-000000000001",
|
|
"ir:ambient-open-0000000001",
|
|
"ir:strip-open-000000000001",
|
|
]
|
|
assert "as many relevant" in client.thread_instructions[0]
|
|
assert _peripheral_refs(observed[0]) == [
|
|
"ir:flash-open-000000000001",
|
|
"ir:ambient-open-0000000001",
|
|
"ir:strip-open-000000000001",
|
|
]
|
|
|
|
|
|
def test_scene_template_miss_falls_through_to_structured_scene_composer() -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
observed: list[dict[str, object]] = []
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
observed.append(deepcopy(dict(assistant_plan)))
|
|
return _preview_result("星夜模式")
|
|
|
|
result, events = _invoke(
|
|
assistant,
|
|
"构建一个星夜模式",
|
|
execute,
|
|
conversation_id="conv-scene-template-miss",
|
|
catalog=_catalog_with_templates(_scene_template()),
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert len(client.structured_calls) == 1
|
|
assert client.legacy_calls == []
|
|
assert len(observed) == 1
|
|
assert "metadata" not in observed[0]
|
|
assert events[0]["engine"] == "codex-app-server-scene-composer"
|
|
|
|
|
|
def test_learning_mode_uses_grounded_free_composition_without_device_names() -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
observed: list[dict[str, object]] = []
|
|
|
|
def execute(
|
|
instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
assert instruction == "创建学习模式"
|
|
observed.append(deepcopy(dict(assistant_plan)))
|
|
return _preview_result("学习模式")
|
|
|
|
result, _events = _invoke(
|
|
assistant,
|
|
"创建学习模式",
|
|
execute,
|
|
conversation_id="conv-learning-mode-free-composition",
|
|
catalog=_catalog_with_templates(_scene_template()),
|
|
scene_context=_scene_context(),
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert result["route"] == "scene_composer"
|
|
assert len(client.structured_calls) == 1
|
|
context = client.structured_calls[0]["application_context"]
|
|
semantics = context["scene_semantics"]
|
|
assert semantics["family"] == "focused_learning"
|
|
assert semantics["grounded"] is True
|
|
assert "climate.set_temperature" in semantics["preferred_capability_ids"]
|
|
output_schema = client.structured_calls[0]["output_schema"]
|
|
assert output_schema["properties"]["requests"]["minItems"] == 1
|
|
assert output_schema["properties"]["ambiguities"]["maxItems"] == 0
|
|
assert "学习/自习/专注" in client.thread_instructions[0]
|
|
assert observed and observed[0]["requests"]
|
|
|
|
|
|
def test_registered_mode_match_takes_precedence_over_same_named_template() -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
call_arity: list[int] = []
|
|
|
|
def execute(*args: object) -> dict[str, object]:
|
|
call_arity.append(len(args))
|
|
return _preview_result("观影模式")
|
|
|
|
result, events = _invoke(
|
|
assistant,
|
|
"构建一个观影模式",
|
|
execute,
|
|
conversation_id="conv-scene-template-registered-priority",
|
|
catalog=_catalog_with_templates(
|
|
_scene_template(),
|
|
modes=[
|
|
{
|
|
"id": "viewing-registered",
|
|
"name": "观影模式",
|
|
"aliases": ["电影模式"],
|
|
"assistant_callable": True,
|
|
}
|
|
],
|
|
),
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert call_arity == [2]
|
|
assert client.structured_calls == []
|
|
assert client.legacy_calls == []
|
|
assert events[0]["engine"] == "n7web-mode-builder"
|
|
|
|
|
|
def test_explicit_scene_settings_take_precedence_over_template_defaults() -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
instructions: list[str] = []
|
|
call_arity: list[int] = []
|
|
|
|
def execute(*args: object) -> dict[str, object]:
|
|
call_arity.append(len(args))
|
|
instructions.append(str(args[0]))
|
|
result = _preview_result("观影模式", operation_count=1)
|
|
result["mode_profile_draft"]["generation_source"] = "explicit_scene_requirements"
|
|
return result
|
|
|
|
result, events = _invoke(
|
|
assistant,
|
|
"构建一个观影模式,空调温度调整到25度",
|
|
execute,
|
|
conversation_id="conv-scene-template-explicit-priority",
|
|
catalog=_catalog_with_templates(_scene_template()),
|
|
)
|
|
|
|
assert result["success"] is True
|
|
assert instructions == ["构建一个观影模式,空调温度调整到25度"]
|
|
assert call_arity == [2]
|
|
assert client.structured_calls == []
|
|
assert client.legacy_calls == []
|
|
assert events[0]["engine"] == "n7web-mode-builder"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("message", "requested_name"),
|
|
(
|
|
("我想创建一个观影模式", "观影模式"),
|
|
("我希望构建观影模式呀!", "观影模式"),
|
|
("麻烦帮我新建一下观影模式吧", "观影模式"),
|
|
("请帮忙生成一套“观影模式”,先预览一下。", "观影模式"),
|
|
("可以帮我创建个「观影模式」给我看看", "观影模式"),
|
|
("能不能帮我创建一个影院模式,给我看看呀?", "影院模式"),
|
|
("能帮我创建一个观影模式吗?", "观影模式"),
|
|
("能否帮我创建一个影院模式吗", "影院模式"),
|
|
("可不可以帮我创建观影模式?", "观影模式"),
|
|
("请问能不能帮我创建影院模式吗?", "影院模式"),
|
|
("给我添加一个『电影模式』,预览看看!", "电影模式"),
|
|
("请帮我创建一个'影院模式', 给我看看!", "影院模式"),
|
|
("请 帮 我 创 建 一 个 《观 影 模 式》 吧 !", "观影模式"),
|
|
),
|
|
)
|
|
def test_oral_template_create_variants_keep_the_zero_codex_fast_path(
|
|
message: str,
|
|
requested_name: str,
|
|
) -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
template = _scene_template(aliases=["电影模式", "影院模式"])
|
|
observed: list[tuple[str, dict[str, object]]] = []
|
|
|
|
def execute(
|
|
instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
observed.append((instruction, deepcopy(dict(assistant_plan))))
|
|
return _preview_result(requested_name)
|
|
|
|
result, events = _invoke(
|
|
assistant,
|
|
message,
|
|
execute,
|
|
conversation_id=f"conv-oral-template-{abs(hash(message))}",
|
|
catalog=_catalog_with_templates(template),
|
|
)
|
|
|
|
assert _mode_profile_create_parts(message) == (requested_name, "")
|
|
assert result["success"] is True
|
|
assert result["route"] == "scene_template"
|
|
assert events[0]["route"] == "scene_template"
|
|
assert events[0]["engine"] == "n7web-scene-template"
|
|
action = next(event for event in events if event["type"] == "action.completed")
|
|
assert action["payload"]["mode_profile_draft"]["name"] == requested_name
|
|
assert action["payload"]["execution_mode"] == "dry_run"
|
|
assert action["payload"]["hardware_execution_allowed"] is False
|
|
assert len(observed) == 1
|
|
assert observed[0][1]["metadata"] == template["plan"]["metadata"]
|
|
assert client.thread_instructions == []
|
|
assert client.structured_calls == []
|
|
assert client.legacy_calls == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"message",
|
|
(
|
|
"构建一个观影模式并预览",
|
|
"请创建观影模式,先预览一下",
|
|
"我希望创建观影模式:让我先看一下方案",
|
|
"麻烦帮我生成观影模式,给我看看吧",
|
|
"能不能帮我创建影院模式并让我先看看呀?",
|
|
),
|
|
)
|
|
def test_preview_and_look_suffixes_are_not_misclassified_as_device_settings(
|
|
message: str,
|
|
) -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
template = _scene_template(aliases=["影院模式"])
|
|
executor_arity: list[int] = []
|
|
|
|
def execute(*args: object) -> dict[str, object]:
|
|
executor_arity.append(len(args))
|
|
return _preview_result("影院模式" if "影院" in message else "观影模式")
|
|
|
|
result, events = _invoke(
|
|
assistant,
|
|
message,
|
|
execute,
|
|
conversation_id=f"conv-preview-suffix-{abs(hash(message))}",
|
|
catalog=_catalog_with_templates(template),
|
|
)
|
|
|
|
parts = _mode_profile_create_parts(message)
|
|
assert parts is not None
|
|
assert parts[1] == ""
|
|
assert result["route"] == "scene_template"
|
|
assert executor_arity == [3]
|
|
assert events[0]["engine"] == "n7web-scene-template"
|
|
assert client.structured_calls == []
|
|
assert client.legacy_calls == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("message", "expected_settings"),
|
|
(
|
|
("我想构建一个观影模式,空调温度调整到25度", "空调温度调整到25度"),
|
|
("请帮我创建一个影院模式:遮阳帘设置为100%", "遮阳帘设置为100%"),
|
|
(
|
|
"能不能帮我创建观影模式,把车窗全部关闭、氛围灯调暗",
|
|
"把车窗全部关闭、氛围灯调暗",
|
|
),
|
|
),
|
|
)
|
|
def test_oral_create_with_explicit_device_settings_never_uses_template_defaults(
|
|
message: str,
|
|
expected_settings: str,
|
|
) -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
executor_calls: list[tuple[object, ...]] = []
|
|
|
|
def execute(*args: object) -> dict[str, object]:
|
|
executor_calls.append(args)
|
|
result = _preview_result("观影模式", operation_count=1)
|
|
result["mode_profile_draft"]["generation_source"] = (
|
|
"explicit_scene_requirements"
|
|
)
|
|
return result
|
|
|
|
result, events = _invoke(
|
|
assistant,
|
|
message,
|
|
execute,
|
|
conversation_id=f"conv-explicit-settings-{abs(hash(message))}",
|
|
catalog=_catalog_with_templates(
|
|
_scene_template(aliases=["影院模式"]),
|
|
),
|
|
)
|
|
|
|
parts = _mode_profile_create_parts(message)
|
|
assert parts is not None
|
|
assert parts[1] == expected_settings
|
|
assert result["success"] is True
|
|
assert events[0]["route"] == "vehicle_control"
|
|
assert events[0]["engine"] == "n7web-mode-builder"
|
|
assert len(executor_calls) == 1
|
|
assert len(executor_calls[0]) == 2
|
|
assert executor_calls[0][0] == " ".join(message.strip().split())
|
|
assert client.structured_calls == []
|
|
assert client.legacy_calls == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"message",
|
|
(
|
|
"我想创建一个星空漫游模式",
|
|
"麻烦帮我构建星空漫游模式吧",
|
|
"能不能帮我创建一个星空漫游模式,先给我看看呀?",
|
|
),
|
|
)
|
|
def test_unknown_oral_scene_names_still_fall_through_to_scene_composer(
|
|
message: str,
|
|
) -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
executor_arity: list[int] = []
|
|
|
|
def execute(*args: object) -> dict[str, object]:
|
|
executor_arity.append(len(args))
|
|
return _preview_result("星空漫游模式")
|
|
|
|
result, events = _invoke(
|
|
assistant,
|
|
message,
|
|
execute,
|
|
conversation_id=f"conv-unknown-oral-scene-{abs(hash(message))}",
|
|
catalog=_catalog_with_templates(_scene_template()),
|
|
)
|
|
|
|
parts = _mode_profile_create_parts(message)
|
|
assert parts is not None
|
|
assert parts[1] == ""
|
|
assert result["success"] is True
|
|
assert events[0]["route"] == "scene_composer"
|
|
assert events[0]["engine"] == "codex-app-server-scene-composer"
|
|
assert len(client.structured_calls) == 1
|
|
assert client.legacy_calls == []
|
|
assert executor_arity == [3]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"message",
|
|
(
|
|
"不要创建观影模式",
|
|
"请别执行创建观影模式",
|
|
"先不要构建观影模式,给我解释一下",
|
|
"请解释一下“创建一个观影模式”是什么意思",
|
|
"引用‘构建一个观影模式’,不要执行",
|
|
"复述:请创建一个观影模式",
|
|
"如何创建一个观影模式?",
|
|
"是否支持创建观影模式?",
|
|
"能否创建观影模式?",
|
|
"你可以创建哪些模式?",
|
|
"构建一个观影模式,空调温度是25度还是26度?",
|
|
"构建一个模式",
|
|
"创建一个功能模式",
|
|
"添加一个新模式",
|
|
"预览观影模式",
|
|
),
|
|
)
|
|
def test_negative_quoted_explanatory_and_capability_queries_never_execute(
|
|
message: str,
|
|
) -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
executor_calls = 0
|
|
|
|
def execute(*_args: object) -> dict[str, object]:
|
|
nonlocal executor_calls
|
|
executor_calls += 1
|
|
return _preview_result("观影模式")
|
|
|
|
result, events = _invoke(
|
|
assistant,
|
|
message,
|
|
execute,
|
|
conversation_id=f"conv-safe-scene-language-{abs(hash(message))}",
|
|
catalog=_catalog_with_templates(_scene_template()),
|
|
)
|
|
|
|
assert _mode_profile_create_parts(message) is None
|
|
assert result["route"] == "agent"
|
|
assert events[0]["route"] == "agent"
|
|
assert executor_calls == 0
|
|
assert client.structured_calls == []
|
|
assert len(client.legacy_calls) == 1
|
|
|
|
|
|
def test_unknown_mode_uses_one_structured_turn_then_local_dry_run() -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
observed: list[tuple[str, dict[str, object]]] = []
|
|
|
|
def execute(
|
|
instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
observed.append((instruction, deepcopy(dict(assistant_plan))))
|
|
return _preview_result("星夜模式")
|
|
|
|
result, events = _invoke(
|
|
assistant,
|
|
"构建一个星夜模式",
|
|
execute,
|
|
conversation_id="conv-scene-fast-success",
|
|
)
|
|
|
|
assert len(client.structured_calls) == 1
|
|
assert client.legacy_calls == []
|
|
assert observed[0][0] == "构建一个星夜模式"
|
|
assert [item["capability"] for item in observed[0][1]["requests"]] == [
|
|
"climate.set_temperature",
|
|
"roof.set_shade_position",
|
|
]
|
|
call = client.structured_calls[0]
|
|
expected_context = _scene_context()
|
|
expected_context["seat_scope_policy"] = {
|
|
"selected_scopes": ["passenger"],
|
|
"default_scope": "passenger",
|
|
"applies_to": "seat capabilities in every phase",
|
|
}
|
|
expected_context["natural_language_routing"] = {
|
|
"original_utterance": "构建一个星夜模式",
|
|
"parsed_clauses": [],
|
|
"resolved_clauses": [],
|
|
"unresolved_clauses": ["构建一个星夜模式"],
|
|
"cloud_input_utterance": "构建一个星夜模式",
|
|
}
|
|
expected_semantics = scene_semantic_guidance("星夜模式")
|
|
required_native = scene_required_native_fallbacks(
|
|
"星夜模式",
|
|
expected_context,
|
|
include_default_bundle=True,
|
|
)
|
|
expected_semantics["required_native_domains"] = ["climate", "roof"]
|
|
expected_semantics["required_native_controls"] = [
|
|
{
|
|
"capability": str(item["capability"]),
|
|
"target_scope": str(item["target_scope"]),
|
|
}
|
|
for item in required_native
|
|
]
|
|
expected_semantics["native_fallback_requests"] = required_native
|
|
expected_semantics["default_control_bundle_applied"] = True
|
|
expected_semantics["peripheral_policy"] = {
|
|
"available_action_count": 0,
|
|
"required_refs": [],
|
|
"selection_goal": (
|
|
"Include as many semantically relevant scene-safe peripheral devices "
|
|
"as are useful; normally choose activation plus at most one stable "
|
|
"state action per device."
|
|
),
|
|
"required_reason": "No peripheral action is mandatory for this name.",
|
|
}
|
|
expected_context["scene_semantics"] = expected_semantics
|
|
assert call["application_context"] == expected_context
|
|
assert call["output_schema"]["type"] == "object"
|
|
assert call["output_schema"]["additionalProperties"] is False
|
|
assert "requests" in call["output_schema"]["required"]
|
|
assert result["success"] is True
|
|
assert "预览" in result["answer"]
|
|
assert "2项设备动作" in result["answer"]
|
|
assert any(event["type"] == "action.completed" for event in events)
|
|
assert all("真实下发" not in str(event) for event in events)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"bad_answer",
|
|
(
|
|
"不是 JSON",
|
|
"{}",
|
|
'{"requests":"not-a-list"}',
|
|
),
|
|
)
|
|
def test_malformed_structured_output_falls_back_to_existing_agent_path(
|
|
bad_answer: str,
|
|
) -> None:
|
|
client = StructuredSceneClient()
|
|
client.structured_answers.put(bad_answer)
|
|
assistant = CodexAssistantService(client)
|
|
fallback_plan = _legacy_plan(temperature=23)
|
|
calls: list[dict[str, object]] = []
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
calls.append(deepcopy(dict(assistant_plan)))
|
|
return _preview_result("阅读模式")
|
|
|
|
def legacy(request_token: str, _turn: ActiveCodexTurn) -> None:
|
|
assistant.execute_cockpit_request(request_token, fallback_plan)
|
|
|
|
client.on_legacy_start = legacy
|
|
result, _events = _invoke(
|
|
assistant,
|
|
"构建一个阅读模式",
|
|
execute,
|
|
conversation_id=f"conv-scene-fallback-{abs(hash(bad_answer))}",
|
|
)
|
|
|
|
assert len(client.structured_calls) == 1
|
|
assert len(client.legacy_calls) == 1
|
|
assert len(calls) == 1
|
|
assert [item["capability"] for item in calls[0]["requests"]] == [
|
|
"climate.set_temperature",
|
|
"roof.set_shade_position",
|
|
]
|
|
assert calls[0]["requests"][0]["parameters"] == [{"name": "temperature_c", "value": 23}]
|
|
assert result["route"] == "agent"
|
|
assert "预览" in result["answer"]
|
|
|
|
|
|
def test_same_normalized_scene_and_revision_reuses_plan_but_revalidates_every_request() -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
observed: list[dict[str, object]] = []
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
observed.append(deepcopy(dict(assistant_plan)))
|
|
return _preview_result("静心模式")
|
|
|
|
first, _ = _invoke(
|
|
assistant,
|
|
"创建一个静心模式",
|
|
execute,
|
|
conversation_id="conv-scene-cache-first",
|
|
)
|
|
second, _ = _invoke(
|
|
assistant,
|
|
"构建一个静心模式",
|
|
execute,
|
|
conversation_id="conv-scene-cache-second",
|
|
)
|
|
|
|
assert first["success"] is second["success"] is True
|
|
assert len(client.structured_calls) == 1
|
|
assert len(observed) == 2
|
|
assert observed[0] == observed[1]
|
|
|
|
|
|
@pytest.mark.parametrize("changed_dimension", ("revision", "vehicle"))
|
|
def test_cache_is_invalidated_when_scene_contract_or_vehicle_changes(
|
|
changed_dimension: str,
|
|
) -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
_assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
return _preview_result("接待模式")
|
|
|
|
_invoke(
|
|
assistant,
|
|
"构建一个接待模式",
|
|
execute,
|
|
conversation_id="conv-scene-invalidate-first",
|
|
)
|
|
context = (
|
|
_scene_context("scene-contract-b")
|
|
if changed_dimension == "revision"
|
|
else _scene_context(vehicle_id="another_vehicle")
|
|
)
|
|
catalog = _catalog(
|
|
vehicle_id="another_vehicle" if changed_dimension == "vehicle" else "denza_n7"
|
|
)
|
|
_invoke(
|
|
assistant,
|
|
"构建一个接待模式",
|
|
execute,
|
|
conversation_id="conv-scene-invalidate-second",
|
|
scene_context=context,
|
|
catalog=catalog,
|
|
)
|
|
|
|
assert len(client.structured_calls) == 2
|
|
|
|
|
|
def test_failed_local_validation_is_never_cached() -> None:
|
|
client = StructuredSceneClient()
|
|
client.structured_answers.put(json.dumps(_structured_plan(temperature=18), ensure_ascii=False))
|
|
client.structured_answers.put(json.dumps(_structured_plan(temperature=25), ensure_ascii=False))
|
|
assistant = CodexAssistantService(client)
|
|
attempts = 0
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
_assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
nonlocal attempts
|
|
attempts += 1
|
|
return _preview_result("派对模式", success=attempts > 1)
|
|
|
|
first, _ = _invoke(
|
|
assistant,
|
|
"构建一个派对模式",
|
|
execute,
|
|
conversation_id="conv-scene-invalid-first",
|
|
)
|
|
second, _ = _invoke(
|
|
assistant,
|
|
"构建一个派对模式",
|
|
execute,
|
|
conversation_id="conv-scene-invalid-second",
|
|
)
|
|
|
|
assert first["success"] is False
|
|
assert second["success"] is True
|
|
assert len(client.structured_calls) == 2
|
|
|
|
|
|
def test_concurrent_identical_cold_requests_share_one_planner_turn() -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
planner_entered = threading.Event()
|
|
release_planner = threading.Event()
|
|
executor_calls = 0
|
|
executor_lock = threading.Lock()
|
|
|
|
def hold_planner() -> None:
|
|
planner_entered.set()
|
|
assert release_planner.wait(timeout=3)
|
|
|
|
client.on_structured_start = hold_planner
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
_assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
nonlocal executor_calls
|
|
with executor_lock:
|
|
executor_calls += 1
|
|
return _preview_result("露营模式")
|
|
|
|
def run(index: int) -> dict[str, object]:
|
|
result, _ = _invoke(
|
|
assistant,
|
|
"构建一个露营模式",
|
|
execute,
|
|
conversation_id=f"conv-scene-singleflight-{index}",
|
|
)
|
|
return result
|
|
|
|
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
first = pool.submit(run, 1)
|
|
assert planner_entered.wait(timeout=2)
|
|
second = pool.submit(run, 2)
|
|
time.sleep(0.15)
|
|
assert len(client.structured_calls) == 1
|
|
release_planner.set()
|
|
results = [first.result(timeout=3), second.result(timeout=3)]
|
|
|
|
assert all(result["success"] is True for result in results)
|
|
assert len(client.structured_calls) == 1
|
|
# A plan cache never authorizes execution: both requests still traverse the
|
|
# current local validation/dry-run callback independently.
|
|
assert executor_calls == 2
|
|
|
|
|
|
def test_cached_plan_is_copied_before_each_validation() -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
seen_temperatures: list[float] = []
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
requests = assistant_plan["requests"]
|
|
assert isinstance(requests, list)
|
|
temperature = requests[0]["parameters"][0]["value"]
|
|
seen_temperatures.append(temperature)
|
|
requests[0]["parameters"][0]["value"] = 31
|
|
return _preview_result("专注模式")
|
|
|
|
for suffix in ("first", "second"):
|
|
_invoke(
|
|
assistant,
|
|
"构建一个专注模式",
|
|
execute,
|
|
conversation_id=f"conv-scene-copy-{suffix}",
|
|
)
|
|
|
|
assert len(client.structured_calls) == 1
|
|
assert seen_temperatures == [24, 24]
|
|
|
|
|
|
def test_scene_preview_never_reports_or_requests_hardware_execution() -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
calls = 0
|
|
|
|
def execute(
|
|
instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
nonlocal calls
|
|
calls += 1
|
|
assert instruction == "构建一个观景模式"
|
|
assert assistant_plan["requests"]
|
|
return _preview_result("观景模式")
|
|
|
|
result, events = _invoke(
|
|
assistant,
|
|
"构建一个观景模式",
|
|
execute,
|
|
conversation_id="conv-scene-no-hardware",
|
|
)
|
|
|
|
assert calls == 1
|
|
assert result["success"] is True
|
|
action_completed = next(event for event in events if event["type"] == "action.completed")
|
|
payload = action_completed["payload"]
|
|
assert payload["execution_mode"] == "dry_run"
|
|
assert payload["hardware_execution_allowed"] is False
|
|
assert "approval_token" not in json.dumps(events, ensure_ascii=False)
|
|
assert "command_plans" not in json.dumps(events, ensure_ascii=False)
|
|
|
|
|
|
def test_validated_scene_plan_cache_is_shared_across_principals() -> None:
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
executor_calls = 0
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
_assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
nonlocal executor_calls
|
|
executor_calls += 1
|
|
return _preview_result("露营模式")
|
|
|
|
first, _ = _invoke(
|
|
assistant,
|
|
"构建一个露营模式",
|
|
execute,
|
|
conversation_id="conv-scene-global-cache-a",
|
|
principal="alice",
|
|
)
|
|
second, _ = _invoke(
|
|
assistant,
|
|
"创建一个露营模式",
|
|
execute,
|
|
conversation_id="conv-scene-global-cache-b",
|
|
principal="bob",
|
|
)
|
|
|
|
assert first["success"] is second["success"] is True
|
|
assert len(client.structured_calls) == 1
|
|
# Cross-user reuse applies only to the context-bound candidate plan. Each
|
|
# user's request still enters the current local dry-run validation chain.
|
|
assert executor_calls == 2
|
|
|
|
|
|
def test_structured_planner_timeout_returns_explicit_failure_without_general_fallback() -> None:
|
|
client = StructuredSceneClient()
|
|
hanging_turns: list[ActiveCodexTurn] = []
|
|
|
|
def hanging_turn(thread_id: str, turn_id: str) -> ActiveCodexTurn:
|
|
turn = ActiveCodexTurn(thread_id=thread_id, turn_id=turn_id, events=queue.Queue())
|
|
hanging_turns.append(turn)
|
|
return turn
|
|
|
|
client.structured_turn_factory = hanging_turn
|
|
assistant = CodexAssistantService(client)
|
|
assistant._scene_turn_timeout = 0.05
|
|
executor_calls = 0
|
|
|
|
def execute(*_args: object) -> dict[str, object]:
|
|
nonlocal executor_calls
|
|
executor_calls += 1
|
|
return _preview_result("超时模式")
|
|
|
|
result, events = _invoke(
|
|
assistant,
|
|
"构建一个超时模式",
|
|
execute,
|
|
conversation_id="conv-scene-planner-timeout",
|
|
)
|
|
|
|
assert len(hanging_turns) == 1
|
|
assert client.interrupted == hanging_turns
|
|
assert client.legacy_calls == []
|
|
assert executor_calls == 0
|
|
assert result["route"] == "scene_composer"
|
|
assert result["success"] is False
|
|
assert "超过" in result["answer"]
|
|
assert any(event["type"] == "work.completed" and event["success"] is False for event in events)
|
|
assert any(
|
|
event["type"] == "answer.completed"
|
|
and event["route"] == "scene_composer"
|
|
and "超过" in event["answer"]
|
|
for event in events
|
|
)
|
|
assert any("超时" in str(event.get("display_text") or "") for event in events)
|
|
assert all("切换完整理解链路" not in str(event) for event in events)
|
|
|
|
|
|
def test_scene_planner_concurrency_limit_rejects_excess_cold_scene_without_queueing(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
monkeypatch.setenv("CODEX_ASSISTANT_SCENE_MAX_CONCURRENT_PLANS", "1")
|
|
client = StructuredSceneClient()
|
|
assistant = CodexAssistantService(client)
|
|
first_planner_entered = threading.Event()
|
|
release_planners = threading.Event()
|
|
callback_lock = threading.Lock()
|
|
active_planners = 0
|
|
max_active_planners = 0
|
|
|
|
def hold_planner() -> None:
|
|
nonlocal active_planners, max_active_planners
|
|
with callback_lock:
|
|
active_planners += 1
|
|
max_active_planners = max(max_active_planners, active_planners)
|
|
first_planner_entered.set()
|
|
try:
|
|
assert release_planners.wait(timeout=3)
|
|
finally:
|
|
with callback_lock:
|
|
active_planners -= 1
|
|
|
|
client.on_structured_start = hold_planner
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
_assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
return _preview_result("并发模式")
|
|
|
|
def run(index: int, mode_name: str) -> dict[str, object]:
|
|
result, _ = _invoke(
|
|
assistant,
|
|
f"构建一个{mode_name}",
|
|
execute,
|
|
conversation_id=f"conv-scene-concurrency-{index}",
|
|
principal=f"user-{index}",
|
|
)
|
|
return result
|
|
|
|
with ThreadPoolExecutor(max_workers=1) as pool:
|
|
first = pool.submit(run, 1, "森林模式")
|
|
assert first_planner_entered.wait(timeout=2)
|
|
second_result, second_events = _invoke(
|
|
assistant,
|
|
"构建一个阅读模式",
|
|
execute,
|
|
conversation_id="conv-scene-concurrency-2",
|
|
principal="user-2",
|
|
)
|
|
calls_before_release = len(client.structured_calls)
|
|
max_active_before_release = max_active_planners
|
|
release_planners.set()
|
|
first_result = first.result(timeout=3)
|
|
|
|
assert first_result["success"] is True
|
|
assert second_result["route"] == "scene_composer"
|
|
assert second_result["success"] is False
|
|
assert "任务较多" in second_result["answer"]
|
|
assert any(
|
|
event["type"] == "work.completed" and event["success"] is False for event in second_events
|
|
)
|
|
assert calls_before_release == 1
|
|
assert max_active_before_release == 1
|
|
assert len(client.structured_calls) == 1
|
|
assert client.legacy_calls == []
|
|
assert max_active_planners == 1
|
|
|
|
|
|
def test_singleflight_wait_timeout_fails_without_starting_a_second_agent_turn() -> None:
|
|
client = StructuredSceneClient()
|
|
active_ready = threading.Event()
|
|
active_turns: list[ActiveCodexTurn] = []
|
|
|
|
def hanging_turn(thread_id: str, turn_id: str) -> ActiveCodexTurn:
|
|
turn = ActiveCodexTurn(thread_id=thread_id, turn_id=turn_id, events=queue.Queue())
|
|
active_turns.append(turn)
|
|
active_ready.set()
|
|
return turn
|
|
|
|
client.structured_turn_factory = hanging_turn
|
|
assistant = CodexAssistantService(client)
|
|
assistant._scene_turn_timeout = 2
|
|
executor_calls = 0
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
_assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
nonlocal executor_calls
|
|
executor_calls += 1
|
|
return _preview_result("亲子模式")
|
|
|
|
def run_leader() -> dict[str, object]:
|
|
result, _ = _invoke(
|
|
assistant,
|
|
"构建一个亲子模式",
|
|
execute,
|
|
conversation_id="conv-scene-wait-leader",
|
|
principal="leader",
|
|
)
|
|
return result
|
|
|
|
with ThreadPoolExecutor(max_workers=1) as pool:
|
|
leader = pool.submit(run_leader)
|
|
assert active_ready.wait(timeout=2)
|
|
assistant._scene_turn_timeout = 0.05
|
|
waiter_result, waiter_events = _invoke(
|
|
assistant,
|
|
"创建一个亲子模式",
|
|
execute,
|
|
conversation_id="conv-scene-wait-timeout",
|
|
principal="waiter",
|
|
)
|
|
structured_calls_before_completion = len(client.structured_calls)
|
|
legacy_calls_before_completion = list(client.legacy_calls)
|
|
executor_calls_before_completion = executor_calls
|
|
assistant._scene_turn_timeout = 2
|
|
_finish_turn(
|
|
active_turns[0],
|
|
json.dumps(_structured_plan(), ensure_ascii=False),
|
|
)
|
|
leader_result = leader.result(timeout=3)
|
|
|
|
assert structured_calls_before_completion == 1
|
|
assert legacy_calls_before_completion == []
|
|
assert executor_calls_before_completion == 0
|
|
assert waiter_result["route"] == "scene_composer"
|
|
assert waiter_result["success"] is False
|
|
assert "超过" in waiter_result["answer"]
|
|
assert any(
|
|
event["type"] == "work.completed" and event["success"] is False for event in waiter_events
|
|
)
|
|
assert any("超时" in str(event.get("display_text") or "") for event in waiter_events)
|
|
assert leader_result["success"] is True
|
|
assert executor_calls == 1
|
|
assert client.interrupted == []
|
|
|
|
|
|
def test_cancelled_singleflight_waiter_does_not_cancel_or_execute_leader_plan() -> None:
|
|
client = StructuredSceneClient()
|
|
active_ready = threading.Event()
|
|
active_turns: list[ActiveCodexTurn] = []
|
|
|
|
def hanging_turn(thread_id: str, turn_id: str) -> ActiveCodexTurn:
|
|
turn = ActiveCodexTurn(thread_id=thread_id, turn_id=turn_id, events=queue.Queue())
|
|
active_turns.append(turn)
|
|
active_ready.set()
|
|
return turn
|
|
|
|
client.structured_turn_factory = hanging_turn
|
|
assistant = CodexAssistantService(client)
|
|
assistant._scene_turn_timeout = 2
|
|
executor_calls = 0
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
_assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
nonlocal executor_calls
|
|
executor_calls += 1
|
|
return _preview_result("陪伴模式")
|
|
|
|
def run_leader() -> dict[str, object]:
|
|
result, _ = _invoke(
|
|
assistant,
|
|
"构建一个陪伴模式",
|
|
execute,
|
|
conversation_id="conv-scene-cancel-leader",
|
|
principal="leader",
|
|
)
|
|
return result
|
|
|
|
def cancel_on_singleflight_wait(event: dict[str, object]) -> bool | None:
|
|
if event.get("type") == "work.progress" and "相同场景" in str(
|
|
event.get("display_text") or ""
|
|
):
|
|
return False
|
|
return None
|
|
|
|
with ThreadPoolExecutor(max_workers=1) as pool:
|
|
leader = pool.submit(run_leader)
|
|
assert active_ready.wait(timeout=2)
|
|
waiter_result, _ = _invoke(
|
|
assistant,
|
|
"创建一个陪伴模式",
|
|
execute,
|
|
conversation_id="conv-scene-cancel-waiter",
|
|
principal="waiter",
|
|
emit_handler=cancel_on_singleflight_wait,
|
|
)
|
|
assert waiter_result == {"cancelled": True}
|
|
assert len(client.structured_calls) == 1
|
|
assert client.legacy_calls == []
|
|
assert executor_calls == 0
|
|
assert client.interrupted == []
|
|
|
|
_finish_turn(
|
|
active_turns[0],
|
|
json.dumps(_structured_plan(), ensure_ascii=False),
|
|
)
|
|
leader_result = leader.result(timeout=3)
|
|
|
|
assert leader_result["success"] is True
|
|
assert executor_calls == 1
|
|
assert client.interrupted == []
|
|
|
|
|
|
def test_continuous_delta_stream_still_detects_browser_cancellation() -> None:
|
|
client = StructuredSceneClient()
|
|
active_turns: list[ActiveCodexTurn] = []
|
|
|
|
def busy_turn(thread_id: str, turn_id: str) -> ActiveCodexTurn:
|
|
turn = ActiveCodexTurn(thread_id=thread_id, turn_id=turn_id, events=queue.Queue())
|
|
for _ in range(10_000):
|
|
turn.events.put(
|
|
{
|
|
"method": "item/agentMessage/delta",
|
|
"params": {"delta": "x"},
|
|
}
|
|
)
|
|
active_turns.append(turn)
|
|
return turn
|
|
|
|
client.structured_turn_factory = busy_turn
|
|
assistant = CodexAssistantService(client)
|
|
assistant._scene_progress_interval = 0.001
|
|
executor_calls = 0
|
|
|
|
def execute(*_args: object) -> dict[str, object]:
|
|
nonlocal executor_calls
|
|
executor_calls += 1
|
|
return _preview_result("持续输出模式")
|
|
|
|
def disconnect_on_heartbeat(event: dict[str, object]) -> bool | None:
|
|
if event.get("type") == "work.progress" and event.get("display_text") == (
|
|
"小迪正在生成结构化模式方案……"
|
|
):
|
|
return False
|
|
return None
|
|
|
|
result, _events = _invoke(
|
|
assistant,
|
|
"构建一个持续输出模式",
|
|
execute,
|
|
conversation_id="conv-scene-continuous-cancel",
|
|
emit_handler=disconnect_on_heartbeat,
|
|
)
|
|
|
|
assert result == {"cancelled": True}
|
|
assert client.interrupted == active_turns
|
|
assert active_turns[0].events.qsize() > 0
|
|
assert client.legacy_calls == []
|
|
assert executor_calls == 0
|
|
|
|
|
|
def test_cancelled_singleflight_leader_allows_a_follower_to_replan() -> None:
|
|
client = StructuredSceneClient()
|
|
first_planner_entered = threading.Event()
|
|
follower_waiting = threading.Event()
|
|
release_first_planner = threading.Event()
|
|
starts = 0
|
|
starts_lock = threading.Lock()
|
|
executor_calls = 0
|
|
|
|
def hold_only_first_planner() -> None:
|
|
nonlocal starts
|
|
with starts_lock:
|
|
starts += 1
|
|
current = starts
|
|
if current == 1:
|
|
first_planner_entered.set()
|
|
assert release_first_planner.wait(timeout=3)
|
|
|
|
client.on_structured_start = hold_only_first_planner
|
|
assistant = CodexAssistantService(client)
|
|
|
|
def execute(
|
|
_instruction: str,
|
|
_progress: Callable[[dict[str, object]], None],
|
|
_assistant_plan: Mapping[str, object],
|
|
) -> dict[str, object]:
|
|
nonlocal executor_calls
|
|
executor_calls += 1
|
|
return _preview_result("接续模式")
|
|
|
|
def cancel_leader(event: dict[str, object]) -> bool | None:
|
|
if event.get("type") == "work.progress" and "小迪已开始" in str(
|
|
event.get("display_text") or ""
|
|
):
|
|
return False
|
|
return None
|
|
|
|
def observe_waiter(event: dict[str, object]) -> bool | None:
|
|
if event.get("type") == "work.progress" and "相同场景" in str(
|
|
event.get("display_text") or ""
|
|
):
|
|
follower_waiting.set()
|
|
return None
|
|
|
|
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
leader = pool.submit(
|
|
_invoke,
|
|
assistant,
|
|
"构建一个接续模式",
|
|
execute,
|
|
conversation_id="conv-scene-replan-leader",
|
|
principal="leader",
|
|
emit_handler=cancel_leader,
|
|
)
|
|
assert first_planner_entered.wait(timeout=2)
|
|
follower = pool.submit(
|
|
_invoke,
|
|
assistant,
|
|
"创建一个接续模式",
|
|
execute,
|
|
conversation_id="conv-scene-replan-follower",
|
|
principal="follower",
|
|
emit_handler=observe_waiter,
|
|
)
|
|
assert follower_waiting.wait(timeout=2)
|
|
release_first_planner.set()
|
|
leader_result, _leader_events = leader.result(timeout=3)
|
|
follower_result, _follower_events = follower.result(timeout=3)
|
|
|
|
assert leader_result == {"cancelled": True}
|
|
assert follower_result["success"] is True
|
|
assert len(client.structured_calls) == 2
|
|
assert client.legacy_calls == []
|
|
assert executor_calls == 1
|