feat(n7web): refine spatial UI and NFC-gated extension routing

This commit is contained in:
Miayui
2026-09-16 16:14:10 +08:00
parent 0e4328f548
commit 75ce391312
16 changed files with 925 additions and 106 deletions
+27 -1
View File
@@ -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-v315-vue-vite"'
'const CACHE_VERSION = "cognitive-agent-pad-v317-nfc-auth-routing"'
in service_worker
)
assert '"/assistant-chat.css?v=codex-assistant-29"' in service_worker
@@ -120,6 +120,32 @@ 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"
)
vue_ui = (ROOT / "apps/web-console/vue-ui.js").read_text(encoding="utf-8")
assert "function focusLatestDispatchedControls(container)" in assistant
assert 'transcript.querySelectorAll(".assistant-dispatched-controls")' in assistant
assert "card.open = card === activeCard" in assistant
assert "focusLatestDispatchedControls(host)" in assistant
assert "focusLatestDispatchedControls(card)" in assistant
assert 'querySelectorAll(".assistant-dispatched-controls[open]")' in assistant
assert 'querySelectorAll(".assistant-dispatched-controls[open]")' in vue_ui
assert "if (card !== event.currentTarget) card.open = false" in vue_ui
def test_saved_mode_execution_opens_with_pending_flow_before_http_completion():
app = (ROOT / "apps/web-console/app.js").read_text(encoding="utf-8")
flow = app.split(
@@ -41,7 +41,7 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
"/energy-monitor.js?v=energy-monitor-4"
)
assert (
'const CACHE_VERSION = "cognitive-agent-pad-v315-vue-vite"'
'const CACHE_VERSION = "cognitive-agent-pad-v316-sidebar-accordion"'
in service_worker
)
+17 -1
View File
@@ -52,7 +52,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
assert source_asset in main
assert f'"{cached_asset}"' in service_worker
assert 'import "./styles/design-system.css"' in main
assert 'const CACHE_VERSION = "cognitive-agent-pad-v315-vue-vite"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v316-sidebar-accordion"' in service_worker
def test_reference_glass_theme_covers_the_complete_workspace() -> None:
@@ -247,6 +247,22 @@ def test_command_fields_use_distinct_semantic_colors() -> None:
assert mapping in console
def test_sidebar_command_and_transport_status_use_theme_semantics() -> None:
console = source("console-v2.js")
design_system = source("src/styles/design-system.css")
assert '"v2-sidebar-footer v2-control-link-card"' in console
assert 'transport === "wireless_mqtt" ? "无线 CAN" : "有线 CAN"' in console
assert 'MQTT ${transportOnline ? "已连接" : "未连接"}' in console
assert 'footer.dataset.state = presentationState' in console
assert 'attributeFilter: ["aria-pressed", "class", "data-connection-state", "title"]' in console
assert ".v2-control-link-card" in design_system
assert 'data-state="online"' in design_system
assert 'data-state="offline"' in design_system
assert ".v2-can-command-actual code" in design_system
assert "color: var(--card-foreground) !important" in design_system
def test_function_modes_use_an_inline_accessible_collapsible() -> None:
html = source("index.html")
app = source("app.js")
+78
View File
@@ -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(
@@ -133,6 +133,22 @@ def test_compact_sidebar_overrides_cannot_break_shared_geometry() -> None:
assert "white-space: nowrap" in design_system
def test_runtime_log_starts_collapsed_with_an_accessible_disclosure() -> None:
console = source("console-v2.js")
design_system = source("src/styles/design-system.css")
assert 'class="v2-sync-log" aria-labelledby="v2SyncLogTitle" data-collapsed="true"' in console
assert 'id="v2SyncLogList" aria-live="polite" hidden' in console
assert 'id="v2SyncLogToggle"' in console
assert 'aria-controls="v2SyncLogList"' in console
assert 'aria-expanded="false"' in console
assert "function setSyncLogCollapsed(collapsed)" in console
assert "setSyncLogCollapsed(true)" in console
assert '.v2-sync-log[data-collapsed="true"]' in design_system
assert '.v2-sync-log[data-collapsed="true"] #v2LogFilter' in design_system
assert '#v2SyncLogList[hidden]' in design_system
def test_assistant_composer_hint_stays_inside_narrow_input() -> None:
styles = source("ui-consistency.css")
+72
View File
@@ -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] = []
+1 -1
View File
@@ -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-v315-vue-vite"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v317-nfc-auth-routing"' in service_worker
assert "unpkg.com" not in html
assert "cdn.jsdelivr.net" not in html