merge: sync feature/N7Web into feature/N7Web-Tech
# Conflicts: # apps/web-console/service-worker.js # tests/e2e/test_seat_web_api.py # tests/unit/test_assistant_frontend_contract.py # tests/unit/test_vue_frontend_contract.py
This commit is contained in:
@@ -1363,7 +1363,7 @@ operations:
|
||||
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
|
||||
self.assertIn('"cognitive-agent:module-change"', app_script)
|
||||
self.assertIn(
|
||||
'const CACHE_VERSION = "cognitive-agent-pad-v316-sidebar-accordion"',
|
||||
'const CACHE_VERSION = "cognitive-agent-pad-v318-tech-nfc-routing"',
|
||||
service_worker,
|
||||
)
|
||||
self.assertIn('/console-v2.css?v=delivery-state-69', service_worker)
|
||||
@@ -2116,6 +2116,96 @@ operations:
|
||||
finally:
|
||||
SeatDemoHandler.extension = original_extension
|
||||
|
||||
def test_new_tcp_slot_is_immediately_parsed_but_blocked_until_nfc_binding(
|
||||
self,
|
||||
) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
original_nfc = SeatDemoHandler.nfc
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(directory, "unbound-cactus.sqlite3"),
|
||||
)
|
||||
SeatDemoHandler.extension = extension
|
||||
SeatDemoHandler.nfc = SimpleNamespace(
|
||||
snapshot=lambda: {
|
||||
"revision": 1,
|
||||
"reader": {"online": True},
|
||||
"card": None,
|
||||
}
|
||||
)
|
||||
save_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/extension/gscu/slots/11",
|
||||
method="PUT",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"device_id": "toy-cacti-002",
|
||||
"object_names": ["仙人掌"],
|
||||
"action_names": ["打开", "开启"],
|
||||
"source": "tcp",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
control_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/requests",
|
||||
method="POST",
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/x-ndjson",
|
||||
},
|
||||
data=json.dumps(
|
||||
{
|
||||
"utterance": "打开仙人掌",
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-unbound-cactus-e2e",
|
||||
"execution_mode": "hardware",
|
||||
"client_type": "web_console",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
).encode("utf-8"),
|
||||
)
|
||||
try:
|
||||
with self.opener.open(save_request, timeout=3) as response:
|
||||
self.assertEqual(response.status, 200)
|
||||
with patch.object(
|
||||
extension,
|
||||
"execute_gateway",
|
||||
side_effect=AssertionError("unbound IR command must not be emitted"),
|
||||
):
|
||||
with self.opener.open(control_request, timeout=3) as response:
|
||||
events = [
|
||||
json.loads(line)
|
||||
for line in response.read().decode("utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
finally:
|
||||
SeatDemoHandler.extension = original_extension
|
||||
SeatDemoHandler.nfc = original_nfc
|
||||
|
||||
self.assertEqual(events[-1]["type"], "result")
|
||||
result = events[-1]["payload"]
|
||||
self.assertEqual(result["status"], "blocked")
|
||||
self.assertLess(result["local_control_elapsed_ms"], 1000)
|
||||
self.assertEqual(result["parsed_clauses"], ["打开仙人掌"])
|
||||
self.assertEqual(result["unresolved_clauses"], [])
|
||||
self.assertEqual(
|
||||
result["denied_peripheral_actions"][0]["reason"],
|
||||
"nfc_binding_required",
|
||||
)
|
||||
operation = next(
|
||||
operation
|
||||
for config in result["function_configs"].values()
|
||||
for operation in config["spec"]["operations"]
|
||||
)
|
||||
self.assertEqual(operation["target"]["device"], "toy-cacti-002")
|
||||
self.assertEqual(operation["parameters"], {"action": "打开", "slot": 11})
|
||||
self.assertEqual(result["operation_results"][0]["status"], "blocked")
|
||||
self.assertFalse(
|
||||
result["operation_results"][0]["telemetry"]["transport_submitted"]
|
||||
)
|
||||
|
||||
def test_main_request_routes_durable_nfc_peripheral_after_card_removal(self) -> None:
|
||||
original_extension = SeatDemoHandler.extension
|
||||
original_nfc = SeatDemoHandler.nfc
|
||||
|
||||
@@ -27,7 +27,7 @@ def test_console_contains_ai_assistant_surface():
|
||||
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-v316-sidebar-accordion"'
|
||||
'const CACHE_VERSION = "cognitive-agent-pad-v318-tech-nfc-routing"'
|
||||
in service_worker
|
||||
)
|
||||
assert '"/assistant-chat.css?v=codex-assistant-29"' in service_worker
|
||||
@@ -120,6 +120,16 @@ def test_validated_controls_are_mirrored_into_a_compact_assistant_card():
|
||||
assert ".assistant-dispatched-controls[open]" in css
|
||||
|
||||
|
||||
def test_explicit_nfc_authorization_block_is_rendered_as_safety_blocked():
|
||||
console = (ROOT / "apps/web-console/console-v2.js").read_text(encoding="utf-8")
|
||||
|
||||
assert 'executionStatus === "blocked"' in console
|
||||
assert "Boolean(telemetry.authorization_reason)" in console
|
||||
assert 'deliveryState = aborted' in console
|
||||
assert '? "blocked"' in console
|
||||
assert '? "安全阻止"' in console
|
||||
|
||||
|
||||
def test_latest_dispatched_control_group_is_the_only_group_expanded():
|
||||
assistant = (ROOT / "apps/web-console/assistant-chat.js").read_text(
|
||||
encoding="utf-8"
|
||||
|
||||
@@ -696,6 +696,84 @@ class SelfExtensionTests(unittest.TestCase):
|
||||
self.assertEqual(changed["device_id"], "toy-light-v2")
|
||||
self.assertEqual(service.list_peripheral_capabilities(), [])
|
||||
|
||||
def test_new_structured_slot_is_parseable_before_nfc_authorization(self) -> None:
|
||||
with TemporaryDirectory() as directory:
|
||||
service = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=Path(directory, "infrared-slots.sqlite3"),
|
||||
)
|
||||
service.save_infrared_slot(
|
||||
11,
|
||||
{
|
||||
"device_id": "toy-cacti-002",
|
||||
"object_names": ["仙人掌"],
|
||||
"action_names": ["打开", "开启"],
|
||||
"source": "tcp",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(service.list_peripheral_capabilities(), [])
|
||||
routing = service.list_peripheral_routing_capabilities()
|
||||
self.assertEqual(len(routing), 1)
|
||||
self.assertEqual(routing[0]["id"], "ir-slot:toy-cacti-002:11")
|
||||
self.assertEqual(routing[0]["authorization_state"], "nfc_binding_required")
|
||||
self.assertTrue(routing[0]["parseable"])
|
||||
self.assertFalse(routing[0]["execution_authorized"])
|
||||
|
||||
partition = partition_active_peripheral_intent(
|
||||
"开启仙人掌",
|
||||
routing,
|
||||
{"card": None},
|
||||
)
|
||||
self.assertEqual(partition["peripheral_actions"], [])
|
||||
self.assertEqual(partition["residual_utterance"], "")
|
||||
self.assertEqual(partition["unresolved_peripheral_clauses"], [])
|
||||
self.assertEqual(
|
||||
[
|
||||
(
|
||||
item["capability"]["id"],
|
||||
item["capability"]["slot"],
|
||||
item["reason"],
|
||||
)
|
||||
for item in partition["denied_peripheral_actions"]
|
||||
],
|
||||
[("ir-slot:toy-cacti-002:11", 11, "nfc_binding_required")],
|
||||
)
|
||||
self.assertIsNone(
|
||||
resolve_active_peripheral_command(
|
||||
"开启仙人掌",
|
||||
routing,
|
||||
{"card": None},
|
||||
)
|
||||
)
|
||||
|
||||
service.sync_peripheral_catalog(
|
||||
[
|
||||
{
|
||||
**routing[0],
|
||||
"enabled": True,
|
||||
"card_uid": "DBA5B203",
|
||||
}
|
||||
]
|
||||
)
|
||||
authorized_routing = service.list_peripheral_routing_capabilities()
|
||||
self.assertEqual(
|
||||
authorized_routing[0]["authorization_state"], "authorized"
|
||||
)
|
||||
authorized = partition_active_peripheral_intent(
|
||||
"开启仙人掌",
|
||||
authorized_routing,
|
||||
{"card": None},
|
||||
)
|
||||
self.assertEqual(
|
||||
[item["capability"]["slot"] for item in authorized["peripheral_actions"]],
|
||||
[11],
|
||||
)
|
||||
self.assertEqual(authorized["denied_peripheral_actions"], [])
|
||||
|
||||
service.delete_infrared_slot(11)
|
||||
self.assertEqual(service.list_peripheral_routing_capabilities(), [])
|
||||
|
||||
def test_structured_slot_rejects_invalid_or_missing_device_id(self) -> None:
|
||||
with TemporaryDirectory() as directory:
|
||||
service = SelfExtensionService(
|
||||
|
||||
@@ -5,6 +5,10 @@ from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from cognitive_agent.application.seat_service import SeatDemoService
|
||||
from cognitive_agent.application.self_extension import (
|
||||
SelfExtensionService,
|
||||
SimulationGateway,
|
||||
)
|
||||
from cognitive_agent.server import SeatDemoHandler
|
||||
|
||||
|
||||
@@ -591,6 +595,74 @@ def test_pure_unresolved_peripheral_returns_cloud_ready_local_preview(
|
||||
).is_file()
|
||||
|
||||
|
||||
def test_new_unbound_infrared_device_is_parsed_but_never_dispatched(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
handler = SeatDemoHandler.__new__(SeatDemoHandler)
|
||||
handler.service = _service(tmp_path)
|
||||
handler.extension = SelfExtensionService(
|
||||
SimulationGateway(),
|
||||
catalog_db_path=tmp_path / "infrared-slots.sqlite3",
|
||||
)
|
||||
handler.extension.save_infrared_slot(
|
||||
11,
|
||||
{
|
||||
"device_id": "toy-cacti-002",
|
||||
"object_names": ["仙人掌"],
|
||||
"action_names": ["打开", "开启"],
|
||||
"source": "tcp",
|
||||
},
|
||||
)
|
||||
handler.nfc = SimpleNamespace(snapshot=lambda: {"card": None})
|
||||
validated_events: list[dict[str, object]] = []
|
||||
|
||||
with patch.object(handler.extension, "execute_gateway") as execute_gateway:
|
||||
result = handler._execute_request_with_peripherals(
|
||||
"/api/requests",
|
||||
{
|
||||
"utterance": "打开仙人掌",
|
||||
"provider": "local",
|
||||
"conversation_id": "conv-unbound-cactus",
|
||||
"execution_mode": "hardware",
|
||||
},
|
||||
on_commands_validated=validated_events.append,
|
||||
)
|
||||
|
||||
execute_gateway.assert_not_called()
|
||||
assert result is not None
|
||||
assert result["status"] == "blocked"
|
||||
assert result["phase"] == "authorization_blocked"
|
||||
assert result["success"] is False
|
||||
assert result["local_control_elapsed_ms"] < 1000
|
||||
assert result["natural_language_routing"] == {
|
||||
"original_utterance": "打开仙人掌",
|
||||
"parsed_clauses": ["打开仙人掌"],
|
||||
"resolved_clauses": ["打开仙人掌"],
|
||||
"unresolved_clauses": [],
|
||||
}
|
||||
assert result["denied_peripheral_actions"][0]["reason"] == (
|
||||
"nfc_binding_required"
|
||||
)
|
||||
operation = next(
|
||||
operation
|
||||
for config in result["function_configs"].values()
|
||||
for operation in config["spec"]["operations"]
|
||||
)
|
||||
assert operation["capability"] == "infrared.emit"
|
||||
assert operation["target"]["device"] == "toy-cacti-002"
|
||||
assert operation["parameters"] == {"action": "打开", "slot": 11}
|
||||
assert operation["authorization"] == {
|
||||
"authorized": False,
|
||||
"reason": "nfc_binding_required",
|
||||
}
|
||||
blocked = result["operation_results"][0]
|
||||
assert blocked["status"] == "blocked"
|
||||
assert blocked["telemetry"]["delivery_status"] == "not_submitted"
|
||||
assert blocked["telemetry"]["transport_submitted"] is False
|
||||
assert "尚未完成 NFC 绑定授权" in blocked["message"]
|
||||
assert validated_events == [result]
|
||||
|
||||
|
||||
def test_mixed_request_finishes_all_local_phases_before_cloud_ir() -> None:
|
||||
handler = SeatDemoHandler.__new__(SeatDemoHandler)
|
||||
order: list[str] = []
|
||||
|
||||
@@ -31,7 +31,7 @@ def test_vue_vite_application_is_the_primary_frontend_entry() -> None:
|
||||
assert package["scripts"]["build"] == "vite build"
|
||||
assert 'import vue from "@vitejs/plugin-vue"' in vite
|
||||
assert 'outDir: outputRoot' in vite
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v316-sidebar-accordion"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v318-tech-nfc-routing"' in service_worker
|
||||
assert "unpkg.com" not in html
|
||||
assert "cdn.jsdelivr.net" not in html
|
||||
|
||||
|
||||
Reference in New Issue
Block a user