feat(x4): add per-camera manual wake and hide power action when connected

This commit is contained in:
DCCONSTRUCTIONS
2026-09-10 16:27:48 +03:00
parent db14ee5435
commit 99f29218d7
27 changed files with 992 additions and 126 deletions
@@ -0,0 +1,88 @@
"""Synthetic owner installer checks: no actual APT, systemd or camera calls."""
import json
import sys
import unittest
from pathlib import Path
from unittest.mock import Mock, patch
sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "apps/node-agent/packaging"))
import install_owner_release as owner # noqa: E402
PROFILE = {"version": "0.1.3-8", "revision": "a" * 24}
class OwnerModelUpdateTests(unittest.TestCase):
def test_clean_host_does_not_install_optional_model_implicitly(self):
run = Mock()
with patch.object(owner.subprocess, "run", return_value=Mock(returncode=1, stdout="")):
self.assertEqual(owner.update_installed_x4(run), {"state": "not-installed"})
run.assert_not_called()
def test_current_installed_model_is_not_restarted(self):
run = Mock(return_value=PROFILE["version"])
with (
patch.object(
owner.subprocess,
"run",
return_value=Mock(returncode=0, stdout="install ok installed"),
),
patch.object(owner.Path, "read_text", return_value=json.dumps(PROFILE)),
):
self.assertEqual(owner.update_installed_x4(run)["state"], "current")
self.assertEqual(run.call_count, 1)
def test_offline_model_update_uses_only_fixed_profile_and_checks_result(self):
run = Mock(side_effect=["0.1.3-7", "", PROFILE["version"]])
with (
patch.object(
owner.subprocess,
"run",
return_value=Mock(returncode=0, stdout="install ok installed"),
),
patch.object(
owner.Path,
"read_text",
side_effect=[
json.dumps(PROFILE),
json.dumps({"state": "complete", "revision": PROFILE["revision"]}),
],
),
):
result = owner.update_installed_x4(run)
self.assertEqual(
result,
{
"state": "updated",
"version": PROFILE["version"],
"camera_verification": "not-performed",
},
)
self.assertEqual(
run.call_args_list[1].args[1],
["/usr/bin/systemctl", "start", "mission-core-node-insta360-x4-profile.service"],
)
self.assertIsNone(run.call_args_list[1].kwargs["timeout"])
def test_stale_profile_completion_cannot_qualify_updated_model(self):
run = Mock(side_effect=["0.1.3-7", "", PROFILE["version"]])
with (
patch.object(
owner.subprocess,
"run",
return_value=Mock(returncode=0, stdout="install ok installed"),
),
patch.object(
owner.Path,
"read_text",
side_effect=[
json.dumps(PROFILE),
json.dumps({"state": "complete", "revision": "b" * 24}),
],
),self.assertRaises(RuntimeError)
):
owner.update_installed_x4(run)
if __name__ == "__main__":
unittest.main()
+110
View File
@@ -1,5 +1,6 @@
"""Synthetic recovery/privilege-boundary tests. No USB, Bluetooth or service changes."""
import io
import json
import sys
import tempfile
@@ -149,6 +150,72 @@ class RecoveryTests(unittest.TestCase):
with self.assertRaises(ValueError):
parameters("recovery.configure", value)
def test_registry_retains_offline_wake_identity_without_automatic_intent(self):
with patch.object(recovery, "request", return_value={"items": [A, B]}):
self.value.sync_registry()
self.assertTrue(self.value.view(A)["wake_available"])
self.assertFalse(self.value.view(A)["enabled"])
self.assertEqual(self.value.observe([], 900), [])
self.assertEqual(len(self.value.augment([])), 2)
def test_manual_wake_is_durable_once_and_does_not_enable_policy_or_preview(self):
self.value.states[A] = self.value.default()
session = self.value.states[A]["session_id"]
value = command(A, session, "power.wake")
with patch.object(recovery, "request", return_value={"state": "connected"}) as call:
self.assertEqual(self.value.operation(A, value)["state"], "complete")
self.assertEqual(self.value.operation(A, value)["state"], "complete")
call.assert_called_once_with(
recovery.HELPER,
"/wake-once",
{
"device_id": A,
"attempt": value["operation_id"][3:],
},
timeout=52,
)
self.assertFalse(self.value.view(A)["enabled"])
self.assertFalse(self.value.view(A)["preview_wanted"])
with self.assertRaises(ValueError):
parameters("power.wake", {"serial": "ARBITRARY"})
def test_manual_wake_failure_does_not_schedule_automatic_retry(self):
self.value.states[A] = self.value.default()
value = command(A, self.value.states[A]["session_id"], "power.wake")
with (
patch.object(recovery, "request", return_value={"state": "unavailable"}) as call,
patch.object(recovery.time, "monotonic", side_effect=[0, 53]),
):
result = self.value.operation(A, value)
self.assertEqual(self.value.operation(A, value), result)
self.assertEqual(result["state"], "error")
call.assert_called_once()
self.assertEqual(self.value.observe([], 1000), [])
def test_manual_wake_waits_for_late_sdk_return_without_another_radio_command(self):
with (
patch.object(recovery, "request", return_value={"state": "unavailable"}) as call,
patch.object(recovery.time, "monotonic", side_effect=[0, 22, 37]),
patch.object(self.value.stopping, "wait") as wait,
patch.object(self.broker, "snapshots", side_effect=[[snapshot(B)], [snapshot(A)]]),
):
result = self.value.wake_once(A, command(A, "x4_one", "power.wake"))
self.assertEqual(result["state"], "complete")
call.assert_called_once()
wait.assert_called_once_with(1)
self.assertFalse(self.value.view(A)["enabled"])
def test_manual_wake_does_not_wait_for_other_camera_or_retry_after_deadline(self):
with (
patch.object(recovery, "request", return_value={"state": "unavailable"}) as call,
patch.object(recovery.time, "monotonic", side_effect=[0, 22, 53]),
patch.object(self.value.stopping, "wait"),
patch.object(self.broker, "snapshots", return_value=[snapshot(B)]),
):
result = self.value.wake_once(A, command(A, "x4_one", "power.wake"))
self.assertEqual(result["state"], "error")
call.assert_called_once()
class WakeServiceTests(unittest.TestCase):
def setUp(self):
@@ -231,6 +298,49 @@ class WakeServiceTests(unittest.TestCase):
self.service.advertiser.release()
self.assertEqual(list(self.root.iterdir()), [])
def test_registry_enrolls_usb_without_enabling_recovery_and_keeps_it_when_unplugged(self):
with patch.object(wake_service, "connected", return_value=[self.binding]):
self.assertEqual(self.service.registry(), {"items": [A]})
self.assertFalse(self.service.load(A)["enabled"])
self.assertEqual(self.service.load(A)["attempts"], [])
with patch.object(wake_service, "connected", return_value=[]):
self.assertEqual(self.service.registry(), {"items": [A]})
def test_registry_never_enrolls_ambiguous_usb_identity(self):
with patch.object(wake_service, "connected", return_value=[self.binding, self.binding]):
self.assertEqual(self.service.registry(), {"items": []})
def test_manual_wake_works_while_disabled_and_shares_the_persisted_attempt_budget(self):
self.enable()
self.service.control(A, False)
def start(*args, **kwargs):
kwargs["stdout"].write(b'{"sdk_usb_returned": true}')
kwargs["stdout"].flush()
return Mock(stdin=io.BytesIO(), returncode=0)
with (
patch.object(wake_service, "connected", return_value=[]),
patch.object(
wake_service.subprocess,
"Popen",
side_effect=start,
) as spawn,
):
self.assertEqual(self.service.wake_locked(A, "1" * 32), {"state": "disabled"})
self.assertEqual(
self.service.wake_locked(A, "1" * 32, manual=True), {"state": "connected"}
)
self.assertEqual(
self.service.wake_locked(A, "1" * 32, manual=True), {"state": "already_attempted"}
)
self.assertEqual(
self.service.wake_locked(B, "2" * 32, manual=True), {"state": "disabled"}
)
spawn.assert_called_once()
self.assertFalse(self.service.load(A)["enabled"])
self.assertEqual(len(self.service.load(A)["attempts"]), 1)
if __name__ == "__main__":
unittest.main()