feat(n7web): add pet NFC workflow and glass UI
This commit is contained in:
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user