feat(n7web): add pet NFC workflow and glass UI

This commit is contained in:
Miayui
2026-09-14 15:16:02 +08:00
parent 812e67422d
commit aa6f59d3a6
20 changed files with 2724 additions and 102 deletions
+130 -9
View File
@@ -15,6 +15,7 @@ from unittest.mock import patch
from cognitive_agent.application.codex_assistant import CodexAssistantService
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.pet_cards import PetCardService
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
from cognitive_agent.application.self_extension import (
GatewayError,
@@ -64,6 +65,18 @@ class SeatWebApiTests(unittest.TestCase):
cls.extension_temporary_directory.name, "gscu-ir-catalog.sqlite3"
),
)
cls.original_pets = SeatDemoHandler.pets
SeatDemoHandler.pets = PetCardService(
database_path=Path(
cls.extension_temporary_directory.name,
"pet-cards.sqlite3",
),
nfc_provider=lambda: SeatDemoHandler.nfc.snapshot(),
publisher=lambda _payload: {
"delivery_status": "queued",
"message_id": 1,
},
)
cls.original_auth = SeatDemoHandler.auth
SeatDemoHandler.auth = AuthRuntime(
AuthConfig(
@@ -94,6 +107,8 @@ class SeatWebApiTests(unittest.TestCase):
cls.thread.join(timeout=2)
SeatDemoHandler.service = cls.original_service
SeatDemoHandler.extension = cls.original_extension
SeatDemoHandler.pets.close()
SeatDemoHandler.pets = cls.original_pets
SeatDemoHandler.auth = cls.original_auth
cls.extension_temporary_directory.cleanup()
cls.temporary_directory.cleanup()
@@ -161,6 +176,105 @@ class SeatWebApiTests(unittest.TestCase):
unauthenticated.open(control_request, timeout=3)
self.assertEqual(control_denied.exception.code, 401)
def test_pet_card_crud_and_binding_accept_only_current_reader_uid(self) -> None:
create = urllib.request.Request(
f"{self.base_url}/api/extension/pets",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(
{"pet_name": "团团", "action": "摇尾巴"},
ensure_ascii=False,
).encode("utf-8"),
)
with self.opener.open(create, timeout=3) as response:
created = json.loads(response.read().decode("utf-8"))
self.assertEqual(response.status, 201)
pet_id = created["card"]["id"]
self.assertIsNone(created["card"]["nfc_id"])
self.assertFalse(created["card"]["saved"])
forged = urllib.request.Request(
f"{self.base_url}/api/extension/pets/{pet_id}",
method="PUT",
headers={"Content-Type": "application/json"},
data=json.dumps(
{
"pet_name": "团团",
"action": "摇尾巴",
"nfc_id": "DEADBEEF",
}
).encode("utf-8"),
)
with self.assertRaises(urllib.error.HTTPError) as rejected:
self.opener.open(forged, timeout=3)
self.assertEqual(rejected.exception.code, 400)
save = urllib.request.Request(
f"{self.base_url}/api/extension/pets/{pet_id}",
method="PUT",
headers={"Content-Type": "application/json"},
data=json.dumps(
{"pet_name": "团团", "action": "摇尾巴"},
ensure_ascii=False,
).encode("utf-8"),
)
with self.opener.open(save, timeout=3) as response:
saved = json.loads(response.read().decode("utf-8"))
self.assertTrue(saved["card"]["saved"])
original_nfc = SeatDemoHandler.nfc
SeatDemoHandler.nfc = SimpleNamespace(
snapshot=lambda: {
"revision": 8,
"reader": {"online": True, "state": "ready"},
"card": {"uid": "e1:13-b2 03", "action": "present"},
}
)
try:
bind = urllib.request.Request(
f"{self.base_url}/api/extension/pets/{pet_id}/bind-current-nfc",
method="POST",
headers={"Content-Type": "application/json"},
data=b"{}",
)
with self.opener.open(bind, timeout=3) as response:
bound = json.loads(response.read().decode("utf-8"))
self.assertEqual(bound["card"]["nfc_id"], "E113B203")
with self.opener.open(
f"{self.base_url}/api/extension/nfc/status",
timeout=3,
) as response:
nfc_status = json.loads(response.read().decode("utf-8"))
self.assertEqual(nfc_status["pet_cards"]["cards"][0]["nfc_id"], "E113B203")
self.assertEqual(nfc_status["pet_cards"]["publisher"]["topic"], "n7/pet")
finally:
SeatDemoHandler.nfc = original_nfc
update = urllib.request.Request(
f"{self.base_url}/api/extension/pets/{pet_id}",
method="PUT",
headers={"Content-Type": "application/json"},
data=json.dumps(
{"pet_name": "团团 2", "action": "趴下"},
ensure_ascii=False,
).encode("utf-8"),
)
with self.opener.open(update, timeout=3) as response:
updated = json.loads(response.read().decode("utf-8"))
self.assertEqual(updated["card"]["pet_name"], "团团 2")
self.assertEqual(updated["card"]["action"], "趴下")
self.assertEqual(updated["card"]["nfc_id"], "E113B203")
delete = urllib.request.Request(
f"{self.base_url}/api/extension/pets/{pet_id}",
method="DELETE",
)
with self.opener.open(delete, timeout=3) as response:
deleted = json.loads(response.read().decode("utf-8"))
self.assertEqual(deleted["deleted"]["id"], pet_id)
self.assertEqual(deleted["count"], 0)
def test_n7_mode_scene_catalog_is_public_and_read_only(self) -> None:
unauthenticated = urllib.request.build_opener()
@@ -693,7 +807,7 @@ operations:
def test_static_console_and_runtime_endpoint(self) -> None:
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
html = response.read().decode("utf-8")
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-135", timeout=3) as response:
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-136", timeout=3) as response:
app_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
@@ -720,11 +834,11 @@ operations:
) as response:
speech_worklet = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/self-extension.js?v=gscu-tcp-44", timeout=3
f"{self.base_url}/self-extension.js?v=pet-acrylic-48", timeout=3
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/self-extension.css?v=gscu-tcp-43", timeout=3
f"{self.base_url}/self-extension.css?v=pet-acrylic-48", timeout=3
) as response:
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
with self.opener.open(
@@ -739,6 +853,10 @@ operations:
f"{self.base_url}/energy-monitor.css?v=energy-monitor-4", timeout=3
) as response:
energy_styles = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/codepen-glass-theme.css?v=creative-cloud-1", timeout=3
) as response:
glass_theme = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/service-worker.js", timeout=3
) as response:
@@ -769,7 +887,7 @@ operations:
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
self.assertNotIn("透明车体 · 实体设备", html)
self.assertIn('src="/app.js?v=dual-seat-135"', html)
self.assertIn('src="/app.js?v=dual-seat-136"', html)
self.assertIn('href="/styles.css?v=dual-seat-23"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-9"', html)
self.assertNotIn('class="vehicle-system-hud"', html)
@@ -782,8 +900,8 @@ operations:
self.assertIn('<body class="console-v2">', html)
self.assertIn('id="v2BootScreen"', html)
self.assertIn('正在加载车机控制与实时状态', html)
self.assertIn('src="/self-extension.js?v=gscu-tcp-44"', html)
self.assertIn('href="/self-extension.css?v=gscu-tcp-43"', html)
self.assertIn('src="/self-extension.js?v=pet-acrylic-48"', html)
self.assertIn('href="/self-extension.css?v=pet-acrylic-48"', html)
self.assertNotIn("self-calibration", html)
self.assertNotIn("self-repair", html)
self.assertNotIn("self-evolution", html)
@@ -794,6 +912,9 @@ operations:
self.assertIn('src="/assistant-chat.js?v=codex-assistant-32"', html)
self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html)
self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html)
self.assertIn('href="/codepen-glass-theme.css?v=creative-cloud-1"', html)
self.assertIn("--glass-shell:", glass_theme)
self.assertIn("backdrop-filter: blur(22px) saturate(1.25)", glass_theme)
self.assertNotIn('"admin"', console_script)
self.assertNotIn("v2RoleSelect", console_script)
self.assertIn('sidebarToggle.id = "v2SidebarToggle"', console_script)
@@ -1073,14 +1194,14 @@ operations:
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
self.assertIn('"cognitive-agent:module-change"', app_script)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v290"', service_worker)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v298"', service_worker)
self.assertIn('/console-v2.css?v=delivery-state-69', service_worker)
self.assertIn('/console-v2.js?v=local-first-70', service_worker)
self.assertIn('/assistant-chat.css?v=codex-assistant-28', service_worker)
self.assertIn('/assistant-chat.js?v=codex-assistant-32', service_worker)
self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker)
self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker)
self.assertIn('"/app.js?v=dual-seat-135"', service_worker)
self.assertIn('"/app.js?v=dual-seat-136"', service_worker)
self.assertIn('"/speech-input.js?v=sensevoice-13"', service_worker)
self.assertIn('"/speech-input.css?v=sensevoice-7"', service_worker)
self.assertIn('"/speech-audio-worklet.js?v=sensevoice-5"', service_worker)
@@ -1537,7 +1658,7 @@ operations:
) as response:
extension_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/app.js?v=dual-seat-135", timeout=3
f"{self.base_url}/app.js?v=dual-seat-136", timeout=3
) as response:
app_script = response.read().decode("utf-8")