from __future__ import annotations from pathlib import Path REPOSITORY_ROOT = Path(__file__).resolve().parents[2] SOURCE_PATH = REPOSITORY_ROOT / "apps" / "web-console" / "self-extension.js" def _function_source(source: str, signature: str, next_signature: str) -> str: start = source.index(signature) end = source.index(next_signature, start) return source[start:end] def test_nfc_binding_waits_for_durable_catalog_before_success() -> None: source = SOURCE_PATH.read_text(encoding="utf-8") bind = _function_source( source, " async function bindCurrentNfcToExisting() {", "\n async function unbindCurrentNfc()", ) persistence = bind.index("await bindCardToDevice(device, cardUid);") success = bind.index("NFC 已关联设备 ID") assert persistence < success assert "NFC 设备授权保存失败" in bind assert "void bindCardToDevice" not in bind def test_peripheral_catalog_sync_is_serialized_and_tracks_backend_revision() -> None: source = SOURCE_PATH.read_text(encoding="utf-8") sync = _function_source( source, " async function syncPeripheralCatalog(", "\n async function bindCardToDevice", ) assert "peripheralCatalogSyncQueue" in sync assert "const nextCatalog = peripheralCatalog();" in sync assert "catalog_revision: peripheralCatalogRevision" in sync assert "result.catalog_revision" in sync assert "applyDurablePeripheralCatalog(" in sync assert "if (throwOnError) throw error;" in sync def test_backend_catalog_is_authoritative_for_persistent_device_cards() -> None: source = SOURCE_PATH.read_text(encoding="utf-8") hydrate = _function_source( source, " async function hydratePeripheralCatalog() {", "\n function vehicleCatalog", ) bootstrap = source[source.index(" void hydratePeripheralCatalog()") :] assert "applyDurablePeripheralCatalog(" in hydrate assert "if (!catalog.length)" in hydrate assert "renderAll();" in hydrate assert ".finally(() => syncPeripheralCatalog())" not in bootstrap assert "return syncPeripheralCatalog();" in bootstrap def test_bound_card_can_be_reassigned_to_a_new_device() -> None: source = SOURCE_PATH.read_text(encoding="utf-8") render = _function_source( source, " function renderNfcDialog(", "\n function buildUnifiedNfcDialog", ) bind = _function_source( source, " async function bindCurrentNfcToExisting() {", "\n async function unbindCurrentNfc()", ) assert "alternativeDeviceGroups" in render assert "item.deviceId !== linkedDevice.id" in render assert "state.devices.find((device) => device.id === item.deviceId)?.cardUid" in render assert "data-nfc-bind-existing" in render assert "将当前 NFC 改绑到新设备 ID" in render assert "const previousDevice = linkedDeviceForCard(cardUid);" in bind assert "NFC 已从设备 ID" in bind assert "if (!capabilities.length)" in bind def test_device_card_surfaces_extension_toy_mqtt_delivery() -> None: source = SOURCE_PATH.read_text(encoding="utf-8") apply_toy = _function_source( source, " function applyExtensionToySnapshot(", "\n function applyNfcSnapshot", ) apply_nfc = _function_source( source, " function applyNfcSnapshot(", "\n async function refreshNfcStatus", ) assert 'state === "burst_complete"' in apply_toy assert 'state === "burst_failed"' in apply_toy assert "burst_published_count" in apply_toy assert "extensionToySnapshot.last_error" in apply_toy assert "applyExtensionToySnapshot(value.extension_toy" in apply_nfc assert "正在准备 extension/toy 消息" in apply_nfc