feat(k1): add local connection matrix
This commit is contained in:
@@ -88,12 +88,12 @@ def test_repository_catalog_exposes_xgrids_model() -> None:
|
||||
item for item in plugins if item["metadata"]["id"] == "nodedc.device.xgrids-lixelkity-k1"
|
||||
)
|
||||
assert plugin["apiVersion"] == "missioncore.nodedc/v1alpha2"
|
||||
assert plugin["metadata"]["version"] == "0.5.0"
|
||||
assert plugin["metadata"]["version"] == "0.6.0"
|
||||
assert plugin["spec"]["hostApiRange"] == "v1alpha2"
|
||||
assert plugin["spec"]["compatibilityProfiles"] == [
|
||||
{
|
||||
"profileId": "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1",
|
||||
"path": "profiles/fw-3.0.2/direct-lan.v1.json",
|
||||
"profileId": "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||||
"path": "profiles/fw-3.0.2/local-network.v2.json",
|
||||
"modelId": "xgrids.lixelkity-k1",
|
||||
}
|
||||
]
|
||||
@@ -125,14 +125,14 @@ def test_repository_catalog_exposes_xgrids_model() -> None:
|
||||
} <= action_ids
|
||||
assert next(item for item in models if item["id"] == "xgrids.lixelkity-k1") == {
|
||||
"pluginId": "nodedc.device.xgrids-lixelkity-k1",
|
||||
"pluginVersion": "0.5.0",
|
||||
"pluginVersion": "0.6.0",
|
||||
"id": "xgrids.lixelkity-k1",
|
||||
"vendor": "XGRIDS",
|
||||
"displayName": "XGRIDS LixelKity K1",
|
||||
"category": "Мобильный лидарный сканер",
|
||||
"description": (
|
||||
"Проверенный локальный профиль: BLE-настройка Wi-Fi, MQTT-приём, "
|
||||
"облако точек, поза и raw-first запись."
|
||||
"Локальный профиль Bridge, Quick Connect и Direct Connect: "
|
||||
"BLE/CoreWLAN, MQTT, камеры и raw-first запись."
|
||||
),
|
||||
"verified": True,
|
||||
"capabilities": [
|
||||
@@ -141,6 +141,10 @@ def test_repository_catalog_exposes_xgrids_model() -> None:
|
||||
"id": "device.provisioning.wifi-over-ble",
|
||||
"label": "Wi-Fi через BLE",
|
||||
},
|
||||
{
|
||||
"id": "host.network.wifi-associate-local",
|
||||
"label": "Подключение к точке доступа K1",
|
||||
},
|
||||
{"id": "spatial.point-cloud.live", "label": "Облако точек"},
|
||||
{"id": "spatial.pose.live", "label": "Траектория"},
|
||||
{"id": "device.modeling.live", "label": "Метрики маршрута"},
|
||||
|
||||
@@ -33,6 +33,16 @@ ATTESTATION = CompatibilityAttestationRequest(
|
||||
topology="direct-lan",
|
||||
verification="live-device-info",
|
||||
)
|
||||
QUICK_CONNECT_ATTESTATION = CompatibilityAttestationRequest(
|
||||
firmware_version="3.0.2",
|
||||
topology="device-ap",
|
||||
verification="live-device-info",
|
||||
)
|
||||
DIRECT_CONNECT_ATTESTATION = CompatibilityAttestationRequest(
|
||||
firmware_version="3.0.2",
|
||||
topology="controller-hotspot",
|
||||
verification="live-device-info",
|
||||
)
|
||||
PRIMARY_TEST_CREDENTIAL = "x" * 24
|
||||
SECONDARY_TEST_CREDENTIAL = "y" * 24
|
||||
PROJECT_NAME = "K1 lifecycle test"
|
||||
@@ -225,15 +235,48 @@ def test_project_name_is_normalized_and_control_characters_are_rejected() -> Non
|
||||
host="192.168.1.20",
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
def test_only_physically_accepted_configuration_values_are_admitted() -> None:
|
||||
|
||||
|
||||
def test_connection_modes_require_their_exact_topology_attestation() -> None:
|
||||
assert ConnectRequest(
|
||||
device_id="synthetic-device",
|
||||
ssid="synthetic-network",
|
||||
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
||||
connection_mode="quick-connect",
|
||||
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
||||
).connection_mode == "quick-connect"
|
||||
assert ConnectRequest(
|
||||
device_id="synthetic-device",
|
||||
ssid="synthetic-network",
|
||||
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
||||
connection_mode="direct-connect",
|
||||
compatibility_attestation=DIRECT_CONNECT_ATTESTATION,
|
||||
).connection_mode == "direct-connect"
|
||||
with pytest.raises(ValidationError):
|
||||
ConnectRequest(
|
||||
device_id="synthetic-device",
|
||||
ssid="synthetic-network",
|
||||
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
||||
connection_mode="quick-connect", # type: ignore[arg-type]
|
||||
connection_mode="quick-connect",
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
with pytest.raises(ValidationError, match="32 UTF-8 bytes"):
|
||||
ConnectRequest(
|
||||
device_id="synthetic-device",
|
||||
ssid="🛰️" * 9,
|
||||
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
with pytest.raises(ValidationError, match="64 UTF-8 bytes"):
|
||||
ConnectRequest(
|
||||
device_id="synthetic-device",
|
||||
ssid="synthetic-network",
|
||||
password=SecretStr("🔒" * 17),
|
||||
compatibility_attestation=ATTESTATION,
|
||||
)
|
||||
|
||||
|
||||
def test_only_physically_accepted_mount_and_gnss_values_are_admitted() -> None:
|
||||
with pytest.raises(ValidationError):
|
||||
PrepareAcquisitionRequest(
|
||||
project_name=PROJECT_NAME,
|
||||
@@ -1372,15 +1415,15 @@ def test_exact_profile_is_inactive_until_selected_for_live_device_info_verificat
|
||||
)
|
||||
|
||||
|
||||
def test_prepare_rejects_device_ap_fallback_as_direct_lan_target(tmp_path: Path) -> None:
|
||||
def test_prepare_rejects_device_ap_without_completed_quick_connect(tmp_path: Path) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
|
||||
with pytest.raises(ValueError, match="точки доступа"):
|
||||
with pytest.raises(ValueError, match="connection flow"):
|
||||
service.prepare_acquisition(
|
||||
PrepareAcquisitionRequest(
|
||||
project_name=PROJECT_NAME,
|
||||
host="192.168.56.1",
|
||||
compatibility_attestation=ATTESTATION,
|
||||
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1812,6 +1855,165 @@ def test_network_provisioning_is_single_flight_and_secret_is_unwrapped_only_at_b
|
||||
assert {item["status"] for item in provision_operations} == {"succeeded", "failed"}
|
||||
|
||||
|
||||
def test_quick_connect_associates_the_host_without_a_ble_write(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
service._devices = [{"device_id": "k1-a"}] # noqa: SLF001
|
||||
association_calls: list[tuple[Path, str, str]] = []
|
||||
|
||||
def fake_associate(
|
||||
helper_path: Path,
|
||||
ssid: str,
|
||||
password: str,
|
||||
**_: object,
|
||||
) -> dict[str, Any]:
|
||||
association_calls.append((helper_path, ssid, password))
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"adapter": "CoreWLAN",
|
||||
"outcome": "associated",
|
||||
"already_associated": False,
|
||||
}
|
||||
|
||||
async def forbidden_ble_write(*_: object, **__: object) -> dict[str, Any]:
|
||||
raise AssertionError("Quick Connect must not provision the K1 over BLE")
|
||||
|
||||
monkeypatch.setattr(facade_module, "associate_with_wifi_once", fake_associate)
|
||||
monkeypatch.setattr(facade_module, "provision_wifi_once", forbidden_ble_write)
|
||||
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
||||
|
||||
state = asyncio.run(
|
||||
service.connect(
|
||||
ConnectRequest(
|
||||
device_id="k1-a",
|
||||
ssid="XGR-TEST",
|
||||
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
||||
connection_mode="quick-connect",
|
||||
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assert len(association_calls) == 1
|
||||
assert association_calls[0][0].name == "associate_wifi.swift"
|
||||
assert association_calls[0][1:] == ("XGR-TEST", PRIMARY_TEST_CREDENTIAL)
|
||||
assert state["connection_mode"] == "quick-connect"
|
||||
assert state["k1_ip"] == "192.168.56.1"
|
||||
assert state["compatibility"]["attestation"]["topology"] == "device-ap"
|
||||
quick_sessions = sorted(service.evidence_root.glob("*viewer_k1_ap_association*"))
|
||||
assert len(quick_sessions) == 1
|
||||
assert not (quick_sessions[0] / "provisioning.sensitive.json").exists()
|
||||
redacted_manifest = (quick_sessions[0] / "manifest.redacted.json").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert PRIMARY_TEST_CREDENTIAL not in redacted_manifest
|
||||
assert "XGR-TEST" not in redacted_manifest
|
||||
assert service._camera_target_for_session( # noqa: SLF001
|
||||
state["device_session"]["device_session_id"]
|
||||
) == "192.168.56.1"
|
||||
|
||||
prepared = service.prepare_acquisition(
|
||||
PrepareAcquisitionRequest(
|
||||
project_name=PROJECT_NAME,
|
||||
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
||||
)
|
||||
)
|
||||
assert prepared["acquisition"]["target_host"] == "192.168.56.1"
|
||||
|
||||
|
||||
def test_direct_connect_reuses_the_reviewed_ble_provisioning_frame(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
service._devices = [{"device_id": "k1-a"}] # noqa: SLF001
|
||||
provisioning_calls: list[tuple[str, str, str]] = []
|
||||
|
||||
async def fake_provision(
|
||||
device_id: str,
|
||||
ssid: str,
|
||||
password: str,
|
||||
**_: object,
|
||||
) -> dict[str, Any]:
|
||||
provisioning_calls.append((device_id, ssid, password))
|
||||
return {
|
||||
"started_at_utc": "2026-07-19T01:00:00Z",
|
||||
"completed_at_utc": "2026-07-19T01:00:01Z",
|
||||
"profile_id": "xgrids-k1-fw3-wifi-v1",
|
||||
"outcome": "lan_address_observed",
|
||||
"observations": [{"status": {"ipv4": "172.20.10.2"}}],
|
||||
}
|
||||
|
||||
def forbidden_host_association(*_: object, **__: object) -> dict[str, Any]:
|
||||
raise AssertionError("Direct Connect must not switch the host Wi-Fi network")
|
||||
|
||||
monkeypatch.setattr(facade_module, "provision_wifi_once", fake_provision)
|
||||
monkeypatch.setattr(
|
||||
facade_module,
|
||||
"associate_with_wifi_once",
|
||||
forbidden_host_association,
|
||||
)
|
||||
monkeypatch.setattr(facade_module, "_target_is_local_ipv4", lambda _target: False)
|
||||
|
||||
state = asyncio.run(
|
||||
service.connect(
|
||||
ConnectRequest(
|
||||
device_id="k1-a",
|
||||
ssid="controller-hotspot",
|
||||
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
||||
connection_mode="direct-connect",
|
||||
compatibility_attestation=DIRECT_CONNECT_ATTESTATION,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
assert provisioning_calls == [
|
||||
("k1-a", "controller-hotspot", PRIMARY_TEST_CREDENTIAL)
|
||||
]
|
||||
assert state["connection_mode"] == "direct-connect"
|
||||
assert state["k1_ip"] == "172.20.10.2"
|
||||
assert state["compatibility"]["attestation"]["topology"] == (
|
||||
"controller-hotspot"
|
||||
)
|
||||
|
||||
|
||||
def test_failed_connection_change_revokes_the_previous_route(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
service, _ = service_with_fake_runtime(tmp_path)
|
||||
service._devices = [{"device_id": "k1-a"}] # noqa: SLF001
|
||||
service._selected_device_id = "previous-k1" # noqa: SLF001
|
||||
service._k1_ip = "192.168.1.20" # noqa: SLF001
|
||||
service._connection_mode = "bridge" # noqa: SLF001
|
||||
|
||||
def failed_association(*_: object, **__: object) -> dict[str, Any]:
|
||||
raise RuntimeError("offline association fixture failed")
|
||||
|
||||
monkeypatch.setattr(facade_module, "associate_with_wifi_once", failed_association)
|
||||
|
||||
with pytest.raises(RuntimeError, match="offline association fixture failed"):
|
||||
asyncio.run(
|
||||
service.connect(
|
||||
ConnectRequest(
|
||||
device_id="k1-a",
|
||||
ssid="XGR-OFFLINE",
|
||||
password=SecretStr(PRIMARY_TEST_CREDENTIAL),
|
||||
connection_mode="quick-connect",
|
||||
compatibility_attestation=QUICK_CONNECT_ATTESTATION,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
state = service.state()
|
||||
assert state["selected_device_id"] is None
|
||||
assert state["k1_ip"] is None
|
||||
assert state["connection_mode"] is None
|
||||
assert state["compatibility"]["attestation"] is None
|
||||
|
||||
|
||||
def test_network_provisioning_rejects_an_ipv4_owned_by_the_local_host(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
|
||||
@@ -275,6 +275,15 @@ def test_acceptance_transport_rejects_the_k1_access_point_fallback() -> None:
|
||||
ReviewedApplicationMqttTransport("192.168.56.1")
|
||||
|
||||
|
||||
def test_acceptance_transport_admits_the_k1_ap_only_with_an_explicit_gate() -> None:
|
||||
transport = ReviewedApplicationMqttTransport(
|
||||
"192.168.56.1",
|
||||
allow_device_ap=True,
|
||||
)
|
||||
|
||||
assert transport.snapshot().state == "new"
|
||||
|
||||
|
||||
def test_retained_control_subscription_batches_remain_exact_and_separate_from_points() -> None:
|
||||
assert [len(group) for group in CONTROL_SUBSCRIPTION_GROUPS] == [9, 5, 42]
|
||||
assert CONTROL_SUBSCRIPTION_GROUPS[0][-1] == (DEVICE_INFO_RESPONSE_TOPIC, 0)
|
||||
|
||||
@@ -45,15 +45,29 @@ def _boolean_write_flags(value: Any) -> list[bool]:
|
||||
def test_xgrids_compatibility_profile_loads_exact_firmware_and_sources() -> None:
|
||||
profile = LOADER.load_compatibility_profile()
|
||||
|
||||
assert profile["profile_id"] == "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1"
|
||||
assert profile["profile_id"] == "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2"
|
||||
assert profile["scope"]["vendor"] == "XGRIDS"
|
||||
assert profile["scope"]["model"] == "LixelKity K1"
|
||||
assert profile["scope"]["platform_type"] == "A4"
|
||||
assert profile["scope"]["firmware"] == {"match": "exact", "version": "3.0.2"}
|
||||
assert profile["scope"]["topology"] == "direct-lan"
|
||||
assert profile["scope"]["topology"] == "local-network-matrix"
|
||||
modes = _by_id(profile["scope"]["connection_modes"])
|
||||
assert {
|
||||
mode_id: mode["topology"] for mode_id, mode in modes.items()
|
||||
} == {
|
||||
"bridge": "direct-lan",
|
||||
"direct-connect": "controller-hotspot",
|
||||
"quick-connect": "device-ap",
|
||||
}
|
||||
assert LOADER.matches_target(profile, firmware="3.0.2", topology="direct-lan")
|
||||
assert LOADER.matches_target(profile, firmware="3.0.2", topology="device-ap")
|
||||
assert LOADER.matches_target(
|
||||
profile,
|
||||
firmware="3.0.2",
|
||||
topology="controller-hotspot",
|
||||
)
|
||||
assert not LOADER.matches_target(profile, firmware="3.0.3", topology="direct-lan")
|
||||
assert not LOADER.matches_target(profile, firmware="3.0.2", topology="device-ap")
|
||||
assert not LOADER.matches_target(profile, firmware="3.0.2", topology="usb")
|
||||
|
||||
for source in profile["evidence_sources"]:
|
||||
assert (REPOSITORY_ROOT / source["path"]).is_file()
|
||||
@@ -119,7 +133,7 @@ def test_xgrids_compatibility_profile_declares_only_reviewed_transports() -> Non
|
||||
assert ble["evidence"]["physical_verified"] is True
|
||||
assert ble["evidence"]["write_enabled"] is False
|
||||
|
||||
mqtt = transports["mqtt.direct-lan.fw3.v1"]
|
||||
mqtt = transports["mqtt.local-ipv4.fw3.v1"]
|
||||
assert mqtt["protocol"] == "MQTT 3.1.1"
|
||||
assert mqtt["network"]["transport"] == "TCP"
|
||||
assert mqtt["network"]["port"] == 1883
|
||||
@@ -205,6 +219,18 @@ def test_xgrids_compatibility_profile_rejects_vendor_write_promotion() -> None:
|
||||
LOADER.validate_compatibility_profile(modified)
|
||||
|
||||
|
||||
def test_xgrids_compatibility_profile_rejects_connection_matrix_drift() -> None:
|
||||
profile = LOADER.load_compatibility_profile()
|
||||
modified = copy.deepcopy(profile)
|
||||
modes = _by_id(modified["scope"]["connection_modes"])
|
||||
modes["quick-connect"]["device_network_action"] = (
|
||||
"unreviewed-ble-credential-read"
|
||||
)
|
||||
|
||||
with pytest.raises(LOADER.CompatibilityProfileError, match="differs"):
|
||||
LOADER.validate_compatibility_profile(modified)
|
||||
|
||||
|
||||
def test_xgrids_compatibility_profile_rejects_noncanonical_modeling_header() -> None:
|
||||
profile = LOADER.load_compatibility_profile()
|
||||
modified = copy.deepcopy(profile)
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.device_plugins.xgrids_k1 import macos_wifi
|
||||
|
||||
TEST_PASSWORD = "fixture-only-network-secret"
|
||||
|
||||
|
||||
def _helper(tmp_path: Path) -> Path:
|
||||
helper = tmp_path / "associate_wifi.swift"
|
||||
helper.write_text("// offline fixture\n", encoding="utf-8")
|
||||
return helper
|
||||
|
||||
|
||||
def test_association_passes_secret_only_through_stdin(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.setattr(macos_wifi.sys, "platform", "darwin")
|
||||
calls: list[dict[str, object]] = []
|
||||
live_inputs: list[bytearray] = []
|
||||
|
||||
def fake_runner(argv: list[str], **kwargs: object) -> subprocess.CompletedProcess[bytes]:
|
||||
assert isinstance(kwargs["input"], bytearray)
|
||||
live_inputs.append(kwargs["input"])
|
||||
calls.append(
|
||||
{
|
||||
"argv": argv,
|
||||
**kwargs,
|
||||
"input": bytes(kwargs["input"]),
|
||||
}
|
||||
)
|
||||
return subprocess.CompletedProcess(
|
||||
argv,
|
||||
0,
|
||||
stdout=b'{"ok":true,"already_associated":false}',
|
||||
stderr=b"",
|
||||
)
|
||||
|
||||
result = macos_wifi.associate_with_wifi_once(
|
||||
_helper(tmp_path),
|
||||
"XGR-OFFLINE",
|
||||
TEST_PASSWORD,
|
||||
runner=fake_runner,
|
||||
)
|
||||
|
||||
assert result == {
|
||||
"schema_version": 1,
|
||||
"adapter": "CoreWLAN",
|
||||
"outcome": "associated",
|
||||
"already_associated": False,
|
||||
}
|
||||
assert len(calls) == 1
|
||||
call = calls[0]
|
||||
assert call["argv"][:2] == ["/usr/bin/xcrun", "swift"]
|
||||
assert TEST_PASSWORD not in " ".join(call["argv"])
|
||||
request = json.loads(bytes(call["input"]).decode("utf-8"))
|
||||
assert request == {"ssid": "XGR-OFFLINE", "password": TEST_PASSWORD}
|
||||
assert call["check"] is False
|
||||
assert call["timeout"] == 45.0
|
||||
assert live_inputs and not any(live_inputs[0])
|
||||
|
||||
|
||||
def test_association_reports_only_sanitized_helper_reason(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
monkeypatch.setattr(macos_wifi.sys, "platform", "darwin")
|
||||
|
||||
def fake_runner(argv: list[str], **_: object) -> subprocess.CompletedProcess[bytes]:
|
||||
return subprocess.CompletedProcess(
|
||||
argv,
|
||||
1,
|
||||
stdout=b'{"ok":false,"reason_code":"network-not-found"}',
|
||||
stderr=f"private diagnostic {TEST_PASSWORD}".encode(),
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
macos_wifi.HostWifiAssociationError,
|
||||
match="network-not-found",
|
||||
) as raised:
|
||||
macos_wifi.associate_with_wifi_once(
|
||||
_helper(tmp_path),
|
||||
"XGR-OFFLINE",
|
||||
TEST_PASSWORD,
|
||||
runner=fake_runner,
|
||||
)
|
||||
|
||||
assert TEST_PASSWORD not in str(raised.value)
|
||||
|
||||
|
||||
def test_association_is_macos_only(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(macos_wifi.sys, "platform", "linux")
|
||||
|
||||
with pytest.raises(
|
||||
macos_wifi.HostWifiAssociationError,
|
||||
match="unsupported-platform",
|
||||
):
|
||||
macos_wifi.associate_with_wifi_once(
|
||||
_helper(tmp_path),
|
||||
"XGR-OFFLINE",
|
||||
TEST_PASSWORD,
|
||||
)
|
||||
Reference in New Issue
Block a user