#!/usr/bin/env python3 import hashlib import importlib.machinery import importlib.util import json import os import subprocess import tarfile import tempfile import unittest from pathlib import Path from unittest import mock SCRIPT_DIR = Path(__file__).resolve().parent PLATFORM_ROOT = SCRIPT_DIR.parent.parent RUNNER_PATH = SCRIPT_DIR / "nodedc-deploy" def load_runner(): loader = importlib.machinery.SourceFileLoader( "nodedc_device_manager_deploy_under_test", str(RUNNER_PATH), ) spec = importlib.util.spec_from_loader(loader.name, loader) module = importlib.util.module_from_spec(spec) loader.exec_module(module) return module RUNNER = load_runner() LAUNCHER_HUB_SERVICE_TRUST_UI_ENTRIES = ( "src/app/LauncherApp.tsx", "src/styles/globals.css", "src/widgets/admin-overlay/AdminOverlay.tsx", ) def healthy_device_plane_inventory(*, include_manager=False): services = [ ("device-control-core", "a"), ("device-gateway", "b"), ("device-postgres", "c"), ] if include_manager: services.append(("device-manager", "d")) return { "schemaVersion": "nodedc.device-plane.runtime-inventory.v1", "composeProject": "nodedc-device-plane", "services": [ { "service": service, "containerId": character * 64, "imageId": f"sha256:{character * 64}", "status": "running", "running": True, "health": "healthy", "restartCount": 0, } for service, character in services ], } def device_manager_release_descriptor( release_id="device-manager-release-unit-001", *, action="activate", predecessor_kind="reconciliation", predecessor_patch="device-manager-reconciliation-unit-001", predecessor_sha="a" * 64, ): return { "schemaVersion": "nodedc.device-plane.device-manager-release.v2", "releaseId": release_id, "action": action, "predecessor": { "kind": predecessor_kind, "patchId": predecessor_patch, "artifactSha256": predecessor_sha, }, **RUNNER.expected_device_plane_manager_release_v2_boundaries(), } def device_manager_release_v1_descriptor( release_id="device-manager-release-v1-unit-001", *, action="upgrade", predecessor_kind="release", predecessor_patch="device-manager-release-v1-unit-000", predecessor_sha="a" * 64, ): return { "schemaVersion": "nodedc.device-plane.device-manager-release.v1", "releaseId": release_id, "action": action, "predecessor": { "kind": predecessor_kind, "patchId": predecessor_patch, "artifactSha256": predecessor_sha, }, **RUNNER.expected_device_plane_manager_release_v1_boundaries(), } def device_manager_release_v3_descriptor( release_id="device-manager-release-v3-unit-001", *, predecessor_patch="device-manager-release-20260811-010", predecessor_sha="d" * 64, ): return { "schemaVersion": "nodedc.device-plane.device-manager-release.v3", "releaseId": release_id, "action": "upgrade", "predecessor": { "kind": "release", "patchId": predecessor_patch, "artifactSha256": predecessor_sha, }, **RUNNER.expected_device_plane_manager_release_v3_boundaries(), } class DeviceManagerControlPlaneArtifactsTest(unittest.TestCase): def build(self, script, patch_id, artifact_dir): environment = os.environ.copy() environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(artifact_dir) completed = subprocess.run( ["node", str(SCRIPT_DIR / script), patch_id], cwd=PLATFORM_ROOT, env=environment, check=True, capture_output=True, text=True, ) return json.loads(completed.stdout) def assert_deterministic_artifact(self, script, patch_id, expected_entries): with tempfile.TemporaryDirectory(prefix="nodedc-device-manager-artifact-") as directory: root = Path(directory) first = self.build(script, patch_id, root / "first") second = self.build(script, patch_id, root / "second") first_artifact = Path(first["artifact"]) second_artifact = Path(second["artifact"]) self.assertEqual(first_artifact.read_bytes(), second_artifact.read_bytes()) self.assertEqual( first["sha256"], hashlib.sha256(first_artifact.read_bytes()).hexdigest(), ) self.assertEqual(tuple(first["entries"]), tuple(expected_entries)) extracted = root / "extracted" extracted.mkdir() manifest, entries, payload = RUNNER.load_artifact(first_artifact, extracted) self.assertEqual(tuple(entries), tuple(expected_entries)) with tarfile.open(first_artifact, "r:gz") as archive: members = archive.getmembers() names = [member.name for member in members] bytes_joined = b"\n".join( archive.extractfile(member).read() for member in members if member.isfile() ) self.assertFalse(any(Path(name).name.startswith("._") for name in names)) self.assertFalse(any("/node_modules/" in name or "/.git/" in name for name in names)) self.assertNotIn(b"-----BEGIN PRIVATE KEY-----", bytes_joined) return manifest, entries, names, first def test_platform_hub_trust_artifact_is_exact_and_build_free(self): manifest, entries, _names, result = self.assert_deterministic_artifact( "build-platform-device-core-hub-trust-artifact.mjs", "platform-device-core-hub-trust-unit-001", RUNNER.PLATFORM_DEVICE_CORE_HUB_TRUST_ENTRIES, ) self.assertEqual(manifest["component"], "platform") self.assertEqual(RUNNER.component_services("platform", entries), ("launcher",)) self.assertEqual(RUNNER.component_builds("platform", entries), ()) self.assertEqual(result["services"], ["launcher"]) def test_launcher_session_artifact_is_exact(self): manifest, entries, _names, result = self.assert_deterministic_artifact( "build-launcher-device-core-artifact.mjs", "launcher-device-core-session-unit-001", RUNNER.LAUNCHER_DEVICE_CORE_SESSION_ENTRIES, ) self.assertEqual(manifest["component"], "launcher") self.assertEqual(RUNNER.component_services("launcher", entries), ("launcher",)) self.assertEqual(len(RUNNER.component_builds("launcher", entries)), 1) self.assertEqual(result["services"], ["launcher"]) def test_launcher_hub_service_trust_ui_artifact_is_exact(self): manifest, entries, _names, result = self.assert_deterministic_artifact( "build-launcher-hub-service-trust-ui-artifact.mjs", "launcher-hub-service-trust-ui-unit-001", LAUNCHER_HUB_SERVICE_TRUST_UI_ENTRIES, ) self.assertEqual(manifest["component"], "launcher") self.assertEqual(RUNNER.component_services("launcher", entries), ("launcher",)) self.assertEqual(len(RUNNER.component_builds("launcher", entries)), 1) self.assertEqual(result["services"], ["launcher"]) def test_device_manager_artifact_selects_only_core_and_manager(self): manifest, entries, names, result = self.assert_deterministic_artifact( "build-device-manager-control-plane-artifact.mjs", "device-manager-control-plane-unit-001", RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES, ) self.assertEqual(manifest["component"], "device-plane") self.assertEqual( RUNNER.component_services("device-plane", entries), ("device-control-core", "device-manager"), ) builds = RUNNER.component_builds("device-plane", entries) self.assertEqual(len(builds), 2) self.assertIn(RUNNER.DEVICE_PLANE_CONTROL_CORE_IMAGE, builds[0][1]) self.assertIn(RUNNER.DEVICE_PLANE_MANAGER_IMAGE, builds[1][1]) self.assertIn("payload/services/device-manager/dist/index.html", names) self.assertIn( "payload/services/device-manager/server/device-manager-server.mjs", names, ) self.assertIn( "payload/services/device-control-core/src/credential-reference.mjs", names, ) self.assertIn( "payload/packages/device-edge-channel-contract/src/index.mjs", names, ) self.assertIn("packages/device-edge-channel-contract", entries) self.assertIn( "payload/services/device-control-core/package-lock.json", names, ) self.assertIn( "payload/services/device-control-core/src/device-gateway-core-runtime.mjs", names, ) self.assertFalse(any( name.startswith("payload/packages/device-adapter-runtime/") or name.startswith("payload/packages/device-adapter-catalog/") or name.startswith("payload/services/device-gateway-core/") for name in names )) core_dockerfile = ( PLATFORM_ROOT / "device-plane/services/device-control-core/Dockerfile" ).read_text(encoding="utf-8") self.assertIn( "COPY services/device-control-core/package.json " "services/device-control-core/package-lock.json ./", core_dockerfile, ) self.assertNotIn("COPY package.json package-lock.json ./", core_dockerfile) self.assertNotIn("COPY services/device-gateway-core", core_dockerfile) self.assertFalse(any(name.endswith(".test.mjs") for name in names)) self.assertEqual(result["services"], ["device-control-core", "device-manager"]) self.assertNotIn("device-postgres", result["services"]) self.assertIn("docker-compose.device-manager.yml", entries) self.assertNotIn("docker-compose.device-plane.yml", entries) compose = ( PLATFORM_ROOT / "device-plane/docker-compose.device-manager.yml" ).read_text(encoding="utf-8") for forbidden in ( "DEVICE_EDGE_CHANNEL_", "device-edge-channel/", "name: nodedc-device-plane-egress", ): self.assertNotIn(forbidden, compose) self.assertNotIn("PRIVATE KEY", compose) checks = RUNNER.component_healthchecks("device-plane", entries, tuple(result["services"])) self.assertEqual(checks[0]["expected_json"]["managementApi"], "enabled") self.assertEqual(checks[0]["expected_json"]["discoveryIngest"], "enabled") def test_device_manager_release_v3_is_exact_typed_and_secret_free(self): patch_id = "device-manager-release-v3-unit-001" manifest, entries, names, result = self.assert_deterministic_artifact( "build-device-manager-control-plane-artifact.mjs", patch_id, RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V3_ENTRIES, ) self.assertEqual(manifest["component"], "device-plane") self.assertEqual( RUNNER.component_services("device-plane", entries), ("device-manager",), ) self.assertEqual(result["services"], ["device-manager"]) self.assertIn( "payload/deployment/device-manager-release-v3.json", names, ) self.assertFalse( any(name.startswith("payload/services/device-control-core/") for name in names) ) self.assertFalse( any(name.startswith("payload/packages/") for name in names) ) self.assertFalse(any(name.endswith(".test.mjs") for name in names)) descriptor = device_manager_release_v3_descriptor(patch_id) self.assertIs( RUNNER.validate_device_plane_manager_release_descriptor( descriptor, schema_version=( "nodedc.device-plane.device-manager-release.v3" ), boundaries=( RUNNER.expected_device_plane_manager_release_v3_boundaries() ), expected_release_id=patch_id, ), descriptor, ) self.assertEqual(descriptor["commandTransport"], "typed-service-ping-v1") self.assertEqual( descriptor["commandCatalog"], "allowlisted-adapter-typed-commands-only", ) self.assertEqual(descriptor["gelios"], "untouched-legacy-only") self.assertTrue( RUNNER.is_device_plane_manager_release_v3_slice( "device-plane", entries, ) ) def test_edge_core_channel_bootstrap_is_core_only_and_secret_free(self): manifest, entries, names, result = self.assert_deterministic_artifact( "build-device-edge-core-channel-bootstrap-artifact.mjs", "device-edge-core-channel-bootstrap-unit-001", RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES, ) self.assertEqual(manifest["component"], "device-plane") self.assertEqual( RUNNER.component_services("device-plane", entries), ("device-control-core",), ) builds = RUNNER.component_builds("device-plane", entries) self.assertEqual(len(builds), 1) self.assertIn(RUNNER.DEVICE_PLANE_CONTROL_CORE_IMAGE, builds[0][1]) self.assertEqual(result["services"], ["device-control-core"]) self.assertFalse(any( name.startswith("payload/services/device-manager/") or name.startswith("payload/services/device-gateway/") for name in names )) self.assertIn( "payload/packages/device-edge-channel-contract/src/index.mjs", names, ) self.assertIn( "payload/docker-compose.device-edge-core-channel.yml", names, ) checks = RUNNER.component_healthchecks( "device-plane", entries, tuple(result["services"]), ) self.assertEqual(checks[0]["expected_json"]["managementApi"], "enabled") self.assertEqual(checks[0]["expected_json"]["commandTransport"], "disabled") def test_edge_core_channel_upgrade_is_core_only_and_pins_bootstrap_018(self): manifest, entries, names, result = self.assert_deterministic_artifact( "build-device-edge-core-channel-bootstrap-artifact.mjs", "device-edge-core-channel-upgrade-unit-001", RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES, ) self.assertEqual(manifest["component"], "device-plane") self.assertEqual( RUNNER.component_services("device-plane", entries), ("device-control-core",), ) self.assertEqual(result["services"], ["device-control-core"]) self.assertIn( "payload/deployment/device-edge-core-channel-upgrade-v1.json", names, ) descriptor = RUNNER.expected_device_plane_edge_core_channel_upgrade_descriptor( "device-edge-core-channel-upgrade-unit-001" ) self.assertEqual(descriptor["action"], "upgrade") self.assertEqual( descriptor["bootstrapPredecessor"]["patchId"], "device-edge-core-channel-bootstrap-20260812-018", ) self.assertEqual( descriptor["endpointPolicy"], "public-ipv4-standard-https-tcp-443-only", ) def test_edge_core_channel_upgrade_v2_is_core_only_and_pins_upgrade_019(self): patch_id = "device-edge-core-channel-upgrade-v2-unit-001" manifest, entries, names, result = self.assert_deterministic_artifact( "build-device-edge-core-channel-bootstrap-artifact.mjs", patch_id, RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES, ) self.assertEqual(manifest["component"], "device-plane") self.assertEqual( RUNNER.component_services("device-plane", entries), ("device-control-core",), ) self.assertEqual(len(RUNNER.component_builds("device-plane", entries)), 1) self.assertEqual(result["services"], ["device-control-core"]) self.assertIn( "payload/deployment/device-edge-core-channel-upgrade-v2.json", names, ) descriptor = ( RUNNER.expected_device_plane_edge_core_channel_upgrade_v2_descriptor( patch_id ) ) self.assertEqual(descriptor["action"], "upgrade") self.assertEqual( descriptor["upgradePredecessor"]["patchId"], "device-edge-core-channel-upgrade-20260812-019", ) self.assertEqual( descriptor["upgradePredecessor"]["artifactSha256"], "8e9a220275959f378c1c4b00be5c7192e79afe2134eaab808a64e515870a8438", ) self.assertEqual(descriptor["edgeRegistrations"], "preserved") self.assertTrue( RUNNER.is_device_plane_edge_core_channel_upgrade_slice( "device-plane", entries, ) ) self.assertTrue( RUNNER.is_device_plane_edge_core_channel_upgrade_v2_slice( "device-plane", entries, ) ) def test_edge_core_channel_upgrade_v4_is_core_only_and_pins_upgrade_021(self): patch_id = "device-edge-core-channel-upgrade-v4-unit-001" manifest, entries, names, result = self.assert_deterministic_artifact( "build-device-edge-core-channel-bootstrap-artifact.mjs", patch_id, RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES, ) self.assertEqual(manifest["component"], "device-plane") self.assertEqual( RUNNER.component_services("device-plane", entries), ("device-control-core",), ) self.assertEqual(len(RUNNER.component_builds("device-plane", entries)), 1) self.assertEqual(result["services"], ["device-control-core"]) self.assertIn( "payload/deployment/device-edge-core-channel-upgrade-v4.json", names, ) self.assertIn( "payload/docker-compose.device-plane.yml", names, ) descriptor = ( RUNNER.expected_device_plane_edge_core_channel_upgrade_v4_descriptor( patch_id ) ) self.assertEqual( descriptor["upgradePredecessor"]["patchId"], "device-edge-core-channel-upgrade-v2-20260812-021", ) self.assertEqual( descriptor["upgradePredecessor"]["artifactSha256"], "e40a6fd24edfecac09e42cd82635a77850541bcf047788db3e9c55d2b9e58867", ) self.assertEqual( descriptor["failedAttempt"]["patchId"], "device-edge-core-channel-upgrade-v3-20260812-022", ) self.assertEqual( descriptor["failedAttempt"]["artifactSha256"], "9e2b409a4b2d19711db434e90d03ac8e3db77bd74949f83cace7949f33caf613", ) self.assertEqual( descriptor["coreNetworks"], ["device-plane-private", "device-plane-egress"], ) self.assertEqual( descriptor["removedCoreNetwork"], "device-plane-control", ) self.assertEqual( descriptor["composeCompatibility"], "synology-compose-v2.20-no-gw-priority", ) self.assertTrue( RUNNER.is_device_plane_edge_core_channel_upgrade_v4_slice( "device-plane", entries, ) ) def test_edge_core_channel_upgrade_v4_rejects_installed_marker(self): with tempfile.TemporaryDirectory( prefix="nodedc-device-edge-upgrade-v4-installed-", ) as directory: root = Path(directory) marker = root / RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL marker.parent.mkdir(parents=True) marker.write_text("{}\n", encoding="utf-8") descriptor = ( RUNNER.expected_device_plane_edge_core_channel_upgrade_v4_descriptor( "device-edge-core-channel-upgrade-v4-unit-002" ) ) with ( mock.patch.object(RUNNER, "DEVICE_PLANE_ROOT", root), mock.patch.object( RUNNER, "validate_device_plane_edge_core_channel_upgrade_v4_payload", return_value=descriptor, ), ): with self.assertRaisesRegex( RUNNER.DeployError, "upgrade v4 is already installed", ): RUNNER.validate_device_plane_edge_core_channel_upgrade_v4_predecessor( root / "payload" ) def test_control_core_release_is_repeatable_core_only_and_compose_free(self): patch_id = "device-control-core-release-unit-001" manifest, entries, names, result = self.assert_deterministic_artifact( "build-device-control-core-release-artifact.mjs", patch_id, RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES, ) self.assertEqual(manifest["component"], "device-plane") self.assertEqual( RUNNER.component_services("device-plane", entries), ("device-control-core",), ) self.assertEqual(len(RUNNER.component_builds("device-plane", entries)), 1) self.assertEqual(result["services"], ["device-control-core"]) self.assertIn( "payload/deployment/device-control-core-release-v1.json", names, ) self.assertFalse(any("docker-compose" in name for name in names)) descriptor = RUNNER.expected_device_plane_control_core_release_descriptor( patch_id, { "kind": "edge-core-channel-upgrade-v4", "patchId": ( RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_FIRST_PREDECESSOR_PATCH_ID ), "artifactSha256": ( RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_FIRST_PREDECESSOR_ARTIFACT_SHA256 ), }, ) self.assertEqual( descriptor["coreNetworks"], ["device-plane-private", "device-plane-egress"], ) self.assertEqual(descriptor["edgeRegistrations"], "preserved") def test_control_core_release_apply_gate_checks_preserved_runtime(self): entries = RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES services = ("device-control-core",) with ( mock.patch.object( RUNNER, "healthcheck_compose_service_with_grace", ) as service_health, mock.patch.object(RUNNER, "healthcheck_url") as url_health, mock.patch.object( RUNNER, "validate_device_manager_control_plane_runtime", ) as runtime_acceptance, ): RUNNER.run_healthchecks("device-plane", entries, services) self.assertEqual( [call.args for call in service_health.call_args_list], [ ("device-plane", "device-control-core"), ("device-plane", "device-manager"), ("device-plane", "device-gateway"), ("device-plane", "device-postgres"), ], ) url_health.assert_called_once_with( RUNNER.component_healthchecks( "device-plane", entries, services, )[0] ) runtime_acceptance.assert_called_once_with( require_edge_channel=True, core_network_mode="private-egress", ) def test_control_core_release_v2_is_typed_core_only(self): patch_id = "device-control-core-release-v2-unit-001" manifest, entries, names, result = self.assert_deterministic_artifact( "build-device-control-core-release-artifact.mjs", patch_id, RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_V2_ENTRIES, ) self.assertEqual(manifest["component"], "device-plane") self.assertEqual(result["services"], ["device-control-core"]) self.assertIn( "payload/deployment/device-control-core-release-v2.json", names, ) self.assertFalse(any("docker-compose" in name for name in names)) self.assertEqual( RUNNER.component_services("device-plane", entries), ("device-control-core",), ) descriptor = RUNNER.expected_device_plane_control_core_release_descriptor( patch_id, { "kind": "release", "patchId": "device-control-core-release-20260812-024", "artifactSha256": ( "a289e909283109642e6bba3d9822a31f63423cfe0bbcd52705979681bd2bc793" ), }, schema_version="v2", ) self.assertEqual(descriptor["commandTransport"], "typed-service-ping-v1") self.assertEqual( descriptor["commandCatalog"], "allowlisted-adapter-typed-commands-only", ) self.assertEqual(descriptor["gelios"], "untouched-legacy-only") def test_control_core_release_builder_supports_release_predecessor(self): with tempfile.TemporaryDirectory( prefix="nodedc-control-core-successor-", ) as directory: result = self.build( "build-device-control-core-release-artifact.mjs", "device-control-core-release-unit-004", Path(directory), ) first_artifact = Path(result["artifact"]) # Rebuild through the CLI's repeatable-release predecessor form. environment = os.environ.copy() successor_dir = Path(directory) / "successor" environment["NODEDC_DEPLOY_ARTIFACT_DIR"] = str(successor_dir) predecessor_id = "device-control-core-release-unit-004" predecessor_sha = hashlib.sha256( first_artifact.read_bytes() ).hexdigest() completed = subprocess.run( [ "node", str( SCRIPT_DIR / "build-device-control-core-release-artifact.mjs" ), "device-control-core-release-unit-005", predecessor_id, predecessor_sha, ], cwd=PLATFORM_ROOT, env=environment, check=True, capture_output=True, text=True, ) successor = json.loads(completed.stdout) extracted = Path(directory) / "extracted-successor" extracted.mkdir() _manifest, _entries, payload = RUNNER.load_artifact( Path(successor["artifact"]), extracted, ) descriptor = json.loads( ( payload / RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_REL ).read_text(encoding="utf-8") ) self.assertEqual( descriptor["predecessor"], { "kind": "release", "patchId": predecessor_id, "artifactSha256": predecessor_sha, }, ) def test_control_core_release_rejects_direct_legacy_predecessor(self): with tempfile.TemporaryDirectory( prefix="nodedc-control-core-release-invalid-", ) as directory: payload = Path(directory) descriptor_path = ( payload / RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_REL ) descriptor_path.parent.mkdir(parents=True) predecessor = { "kind": "release", "patchId": "device-edge-core-channel-upgrade-v4-20260812-023", "artifactSha256": "a" * 64, } descriptor = RUNNER.expected_device_plane_control_core_release_descriptor( "device-control-core-release-unit-002", predecessor, ) descriptor_path.write_text( json.dumps(descriptor), encoding="utf-8", ) with self.assertRaisesRegex( RUNNER.DeployError, "release descriptor mismatch", ): RUNNER.validate_device_plane_control_core_release_payload( payload ) def test_edge_core_channel_upgrade_v2_rejects_installed_marker(self): with tempfile.TemporaryDirectory( prefix="nodedc-device-edge-upgrade-v2-installed-", ) as directory: root = Path(directory) marker = root / RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL marker.parent.mkdir(parents=True) marker.write_text("{}\n", encoding="utf-8") descriptor = ( RUNNER.expected_device_plane_edge_core_channel_upgrade_v2_descriptor( "device-edge-core-channel-upgrade-v2-unit-002" ) ) with ( mock.patch.object(RUNNER, "DEVICE_PLANE_ROOT", root), mock.patch.object( RUNNER, "validate_device_plane_edge_core_channel_upgrade_v2_payload", return_value=descriptor, ), ): with self.assertRaisesRegex( RUNNER.DeployError, "upgrade v2 is already installed", ): RUNNER.validate_device_plane_edge_core_channel_upgrade_v2_predecessor( root / "payload" ) def test_edge_core_channel_upgrade_v2_accepts_exact_applied_019(self): with tempfile.TemporaryDirectory( prefix="nodedc-device-edge-upgrade-v2-predecessor-", ) as directory: root = Path(directory) applied = root / "applied" temporary = root / "tmp" device_plane = root / "device-plane" for path in (applied, temporary, device_plane / "deployment"): path.mkdir(parents=True) predecessor_patch = ( RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_PREDECESSOR_PATCH_ID ) predecessor_sha = ( RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_PREDECESSOR_ARTIFACT_SHA256 ) artifact_name = f"nodedc-device-plane-{predecessor_patch}.tgz" artifact = applied / artifact_name artifact.write_bytes(b"reviewed-upgrade-019") state_file = root / "applied.jsonl" state_file.write_text( json.dumps({ "id": predecessor_patch, "artifact": artifact_name, "component": "device-plane", "sha256": predecessor_sha, "status": "ok", }) + "\n", encoding="utf-8", ) bootstrap = ( RUNNER.expected_device_plane_edge_core_channel_bootstrap_descriptor( RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_PREDECESSOR_PATCH_ID ) ) descriptor = ( RUNNER.expected_device_plane_edge_core_channel_upgrade_v2_descriptor( "device-edge-core-channel-upgrade-v2-unit-003" ) ) expected_source = {"source": "upgrade-019"} with ( mock.patch.object(RUNNER, "APPLIED_DIR", applied), mock.patch.object(RUNNER, "TMP_DIR", temporary), mock.patch.object(RUNNER, "STATE_FILE", state_file), mock.patch.object(RUNNER, "DEVICE_PLANE_ROOT", device_plane), mock.patch.object( RUNNER, "validate_device_plane_edge_core_channel_upgrade_v2_payload", return_value=descriptor, ), mock.patch.object( RUNNER, "sha256_file", return_value=predecessor_sha, ), mock.patch.object( RUNNER, "load_artifact", return_value=( { "id": predecessor_patch, "component": "device-plane", "type": "app-overlay", }, RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES, root / "predecessor-payload", ), ), mock.patch.object( RUNNER, "validate_device_plane_edge_core_channel_upgrade_payload", ) as validate_predecessor, mock.patch.object( RUNNER, "collect_exact_files", side_effect=(expected_source, expected_source), ), mock.patch.object( RUNNER, "read_strict_json", return_value=bootstrap, ), mock.patch.object( RUNNER, "inspect_device_edge_channel_core_identity_state", return_value="valid-reuse-at-apply", ), mock.patch.object( RUNNER, "healthcheck_compose_service", ) as health, ): result = ( RUNNER.validate_device_plane_edge_core_channel_upgrade_v2_predecessor( root / "payload" ) ) self.assertEqual(result["mode"], "edge-core-channel-forward-upgrade-v2") self.assertEqual(result["upgradeArtifact"], artifact) validate_predecessor.assert_called_once_with( root / "predecessor-payload", expected_transition_id=predecessor_patch, ) self.assertEqual( [call.args for call in health.call_args_list], [ ("device-plane", "device-control-core"), ("device-plane", "device-manager"), ("device-plane", "device-gateway"), ("device-plane", "device-postgres"), ], ) def test_edge_core_channel_upgrade_v4_accepts_exact_applied_021(self): with tempfile.TemporaryDirectory( prefix="nodedc-device-edge-upgrade-v4-predecessor-", ) as directory: root = Path(directory) applied = root / "applied" failed = root / "failed" backups = root / "backups" temporary = root / "tmp" device_plane = root / "device-plane" for path in ( applied, failed, backups, temporary, device_plane / "deployment", ): path.mkdir(parents=True) predecessor_patch = ( RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_PATCH_ID ) predecessor_sha = ( RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_ARTIFACT_SHA256 ) artifact_name = f"nodedc-device-plane-{predecessor_patch}.tgz" artifact = applied / artifact_name artifact.write_bytes(b"reviewed-upgrade-021") installed_base = device_plane / "docker-compose.device-plane.yml" installed_base.write_text("services: {}\n", encoding="utf-8") state_file = root / "applied.jsonl" state_file.write_text( json.dumps({ "id": predecessor_patch, "artifact": artifact_name, "component": "device-plane", "sha256": predecessor_sha, "status": "ok", }) + "\n", encoding="utf-8", ) failed_artifact = ( failed / RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_ARTIFACT ) failed_artifact.write_bytes(b"reviewed-failed-upgrade-022") failed_backup = ( backups / RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_BACKUP_ID ) failed_backup.mkdir() failed_state_file = root / "failed.jsonl" failed_state_file.write_text( json.dumps({ "id": RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_PATCH_ID, "artifact": failed_artifact.name, "backup_id": failed_backup.name, "component": "device-plane", "sha256": RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_ARTIFACT_SHA256, "status": "failed", "started_apply": True, "rollback_status": "ok:device-plane-overlay:source+runtime-restored:8", "message": "Device Control Core Edge channel network boundary mismatch", }) + "\n", encoding="utf-8", ) descriptor = ( RUNNER.expected_device_plane_edge_core_channel_upgrade_v4_descriptor( "device-edge-core-channel-upgrade-v4-unit-003" ) ) expected_source = {"source": "upgrade-021"} with ( mock.patch.object(RUNNER, "APPLIED_DIR", applied), mock.patch.object(RUNNER, "FAILED_DIR", failed), mock.patch.object(RUNNER, "BACKUPS_DIR", backups), mock.patch.object(RUNNER, "TMP_DIR", temporary), mock.patch.object(RUNNER, "STATE_FILE", state_file), mock.patch.object( RUNNER, "FAILED_STATE_FILE", failed_state_file, ), mock.patch.object(RUNNER, "DEVICE_PLANE_ROOT", device_plane), mock.patch.object( RUNNER, "validate_device_plane_edge_core_channel_upgrade_v4_payload", return_value=descriptor, ), mock.patch.object( RUNNER, "sha256_file", side_effect=lambda path: ( RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_PREDECESSOR_BASE_COMPOSE_SHA256 if Path(path) == installed_base else RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_FAILED_ARTIFACT_SHA256 if Path(path) == failed_artifact else predecessor_sha ), ), mock.patch.object( RUNNER, "load_artifact", return_value=( { "id": predecessor_patch, "component": "device-plane", "type": "app-overlay", }, RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES, root / "predecessor-payload", ), ), mock.patch.object( RUNNER, "validate_device_plane_edge_core_channel_upgrade_v2_payload", ) as validate_predecessor, mock.patch.object( RUNNER, "collect_exact_files", side_effect=(expected_source, expected_source), ), mock.patch.object( RUNNER, "inspect_device_edge_channel_core_identity_state", return_value="valid-reuse-at-apply", ), mock.patch.object( RUNNER, "healthcheck_compose_service", ) as health, ): result = ( RUNNER.validate_device_plane_edge_core_channel_upgrade_v4_predecessor( root / "payload" ) ) self.assertEqual( result["mode"], "edge-core-channel-private-plus-egress-upgrade-v4", ) self.assertEqual(result["upgradeArtifact"], artifact) validate_predecessor.assert_called_once_with( root / "predecessor-payload", expected_transition_id=predecessor_patch, ) self.assertEqual( [call.args for call in health.call_args_list], [ ("device-plane", "device-control-core"), ("device-plane", "device-manager"), ("device-plane", "device-gateway"), ("device-plane", "device-postgres"), ], ) def test_release_v2_keeps_release_v1_predecessor_contract_immutable(self): predecessor = device_manager_release_v1_descriptor( release_id="device-manager-release-20260811-010", ) candidate = device_manager_release_descriptor( release_id="device-manager-release-20260811-012", action="upgrade", predecessor_kind="release", predecessor_patch="device-manager-release-20260811-010", predecessor_sha="d" * 64, ) self.assertIs( RUNNER.validate_device_plane_manager_release_descriptor( predecessor, schema_version=( "nodedc.device-plane.device-manager-release.v1" ), boundaries=( RUNNER.expected_device_plane_manager_release_v1_boundaries() ), expected_release_id="device-manager-release-20260811-010", ), predecessor, ) self.assertIs( RUNNER.validate_device_plane_manager_release_descriptor( candidate, schema_version=( "nodedc.device-plane.device-manager-release.v2" ), boundaries=( RUNNER.expected_device_plane_manager_release_v2_boundaries() ), expected_release_id="device-manager-release-20260811-012", ), candidate, ) with self.assertRaisesRegex( RUNNER.DeployError, r"schema=.*release\.v2 missing=edgeChannel,edgeChannelEgress," r"edgeChannelIdentity extra=none", ): RUNNER.validate_device_plane_manager_release_descriptor( predecessor, schema_version=( "nodedc.device-plane.device-manager-release.v2" ), boundaries=( RUNNER.expected_device_plane_manager_release_v2_boundaries() ), ) def test_installed_manager_compose_follows_release_generation(self): with tempfile.TemporaryDirectory( prefix="nodedc-device-manager-compose-generation-", ) as directory: root = Path(directory) deployment = root / "deployment" deployment.mkdir() compose = root / RUNNER.DEVICE_PLANE_MANAGER_COMPOSE_REL compose.write_text("services: {}\n", encoding="utf-8") v1 = root / RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL v2 = root / RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V2_REL v1.write_text( json.dumps(device_manager_release_v1_descriptor()), encoding="utf-8", ) with ( mock.patch.object(RUNNER, "DEVICE_PLANE_ROOT", root), mock.patch.dict( RUNNER.COMPONENTS["device-plane"], {"compose_files": ()}, ), mock.patch.object( RUNNER, "sha256_file", return_value=( RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V1_COMPOSE_SHA256 ), ), ): self.assertEqual( RUNNER.component_compose_files("device-plane"), (compose,), ) v2.write_text( json.dumps(device_manager_release_descriptor()), encoding="utf-8", ) with ( mock.patch.object(RUNNER, "DEVICE_PLANE_ROOT", root), mock.patch.dict( RUNNER.COMPONENTS["device-plane"], {"compose_files": ()}, ), mock.patch.object( RUNNER, "sha256_file", return_value=( RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V2_COMPOSE_SHA256 ), ), ): self.assertEqual( RUNNER.component_compose_files("device-plane"), (compose,), ) with ( mock.patch.object(RUNNER, "DEVICE_PLANE_ROOT", root), mock.patch.dict( RUNNER.COMPONENTS["device-plane"], {"compose_files": ()}, ), mock.patch.object( RUNNER, "sha256_file", return_value="0" * 64, ), ): with self.assertRaisesRegex( RUNNER.DeployError, "installed Device Manager Compose drift detected", ): RUNNER.component_compose_files("device-plane") def test_public_route_artifact_is_last_and_proxy_only(self): manifest, entries, _names, result = self.assert_deterministic_artifact( "build-platform-device-manager-route-artifact.mjs", "platform-device-manager-route-unit-001", RUNNER.PLATFORM_DEVICE_MANAGER_PUBLIC_ROUTE_ENTRIES, ) self.assertEqual(manifest["component"], "platform") self.assertEqual(RUNNER.component_services("platform", entries), ("reverse-proxy",)) self.assertEqual(RUNNER.component_builds("platform", entries), ()) self.assertEqual(result["services"], ["reverse-proxy"]) def test_runner_creates_only_file_backed_runtime_secrets(self): with mock.patch.object(RUNNER, "ensure_platform_runtime_secret") as ensure: RUNNER.prepare_component_runtime( "platform", RUNNER.PLATFORM_DEVICE_CORE_HUB_TRUST_ENTRIES, ) self.assertEqual( [call.args[0] for call in ensure.call_args_list], [RUNNER.PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE], ) with ( mock.patch.object( RUNNER, "ensure_platform_runtime_secret", ) as ensure, mock.patch.object( RUNNER, "ensure_device_edge_channel_core_identity", ) as ensure_edge_identity, ): RUNNER.prepare_component_runtime( "device-plane", RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES, ) self.assertEqual( [call.args[0] for call in ensure.call_args_list], [ RUNNER.DEVICE_PLANE_POSTGRES_PASSWORD_FILE, RUNNER.DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE, RUNNER.DEVICE_PLANE_IDENTIFIER_PEPPER_FILE, RUNNER.DEVICE_PLANE_MANAGEMENT_CORE_TOKEN_FILE, RUNNER.PLATFORM_DEVICE_CORE_INTERNAL_TOKEN_FILE, ], ) ensure_edge_identity.assert_not_called() with ( mock.patch.object( RUNNER, "ensure_platform_runtime_secret", ) as ensure, mock.patch.object( RUNNER, "ensure_device_edge_channel_core_identity", ) as ensure_edge_identity, ): RUNNER.prepare_component_runtime( "device-plane", RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_BOOTSTRAP_ENTRIES, ) self.assertEqual( [call.args[0] for call in ensure.call_args_list], [ RUNNER.DEVICE_PLANE_POSTGRES_PASSWORD_FILE, RUNNER.DEVICE_PLANE_GATEWAY_CORE_TOKEN_FILE, RUNNER.DEVICE_PLANE_IDENTIFIER_PEPPER_FILE, RUNNER.DEVICE_PLANE_MANAGEMENT_CORE_TOKEN_FILE, ], ) ensure_edge_identity.assert_called_once_with( allow_invalid_unexported_recovery=True ) with ( mock.patch.object( RUNNER, "ensure_platform_runtime_secret", ), mock.patch.object( RUNNER, "ensure_device_edge_channel_core_identity", ) as ensure_edge_identity, ): RUNNER.prepare_component_runtime( "device-plane", RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_ENTRIES, ) ensure_edge_identity.assert_called_once_with( allow_invalid_unexported_recovery=False ) with ( mock.patch.object( RUNNER, "ensure_platform_runtime_secret", ), mock.patch.object( RUNNER, "ensure_device_edge_channel_core_identity", ) as ensure_edge_identity, ): RUNNER.prepare_component_runtime( "device-plane", RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES, ) ensure_edge_identity.assert_called_once_with( allow_invalid_unexported_recovery=False ) with ( mock.patch.object( RUNNER, "ensure_platform_runtime_secret", ), mock.patch.object( RUNNER, "ensure_device_edge_channel_core_identity", ) as ensure_edge_identity, ): RUNNER.prepare_component_runtime( "device-plane", RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES, ) ensure_edge_identity.assert_called_once_with( allow_invalid_unexported_recovery=False ) def test_apply_gate_checks_exact_services_core_contract_and_runtime_boundary(self): entries = RUNNER.DEVICE_PLANE_MANAGER_RELEASE_V1_SUCCESSOR_ENTRIES services = ("device-control-core", "device-manager") with ( mock.patch.object( RUNNER, "healthcheck_compose_service_with_grace", ) as service_health, mock.patch.object(RUNNER, "healthcheck_url") as url_health, mock.patch.object( RUNNER, "validate_device_manager_control_plane_runtime", ) as runtime_acceptance, ): RUNNER.run_healthchecks("device-plane", entries, services) self.assertEqual( [call.args for call in service_health.call_args_list], [ ("device-plane", "device-control-core"), ("device-plane", "device-manager"), ], ) url_health.assert_called_once_with( RUNNER.component_healthchecks( "device-plane", entries, services, )[0] ) runtime_acceptance.assert_called_once_with() def test_upgrade_v2_apply_gate_checks_preserved_runtime_and_edge_contract(self): entries = RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES services = ("device-control-core",) with ( mock.patch.object( RUNNER, "healthcheck_compose_service_with_grace", ) as service_health, mock.patch.object(RUNNER, "healthcheck_url") as url_health, mock.patch.object( RUNNER, "validate_device_manager_control_plane_runtime", ) as runtime_acceptance, ): RUNNER.run_healthchecks("device-plane", entries, services) self.assertEqual( [call.args for call in service_health.call_args_list], [ ("device-plane", "device-control-core"), ("device-plane", "device-manager"), ("device-plane", "device-gateway"), ("device-plane", "device-postgres"), ], ) url_health.assert_called_once_with( RUNNER.component_healthchecks( "device-plane", entries, services, )[0] ) runtime_acceptance.assert_called_once_with( require_edge_channel=True ) def test_upgrade_v4_apply_gate_checks_preserved_runtime_and_edge_contract(self): entries = RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES services = ("device-control-core",) with ( mock.patch.object( RUNNER, "healthcheck_compose_service_with_grace", ) as service_health, mock.patch.object(RUNNER, "healthcheck_url") as url_health, mock.patch.object( RUNNER, "validate_device_manager_control_plane_runtime", ) as runtime_acceptance, ): RUNNER.run_healthchecks("device-plane", entries, services) self.assertEqual( [call.args for call in service_health.call_args_list], [ ("device-plane", "device-control-core"), ("device-plane", "device-manager"), ("device-plane", "device-gateway"), ("device-plane", "device-postgres"), ], ) url_health.assert_called_once_with( RUNNER.component_healthchecks( "device-plane", entries, services, )[0] ) runtime_acceptance.assert_called_once_with( require_edge_channel=True, core_network_mode="private-egress", ) def test_activation_resolves_predecessor_from_descriptor_and_journal(self): with tempfile.TemporaryDirectory( prefix="nodedc-device-manager-release-predecessor-", ) as directory: root = Path(directory) applied = root / "applied" backups = root / "backups" temporary = root / "tmp" device_plane = root / "device-plane" for path in (applied, backups, temporary, device_plane / "deployment"): path.mkdir(parents=True) predecessor_patch = "device-manager-reconciliation-unit-004" artifact_name = f"nodedc-device-plane-{predecessor_patch}.tgz" artifact = applied / artifact_name artifact_bytes = b"reviewed-reconciliation-artifact" artifact.write_bytes(artifact_bytes) artifact_sha = hashlib.sha256(artifact_bytes).hexdigest() descriptor = device_manager_release_descriptor( predecessor_patch=predecessor_patch, predecessor_sha=artifact_sha, ) state_file = root / "applied.jsonl" state_file.write_text( json.dumps({ "id": predecessor_patch, "artifact": artifact_name, "backup_id": "predecessor-backup", "component": "device-plane", "sha256": artifact_sha, "status": "ok", }) + "\n", encoding="utf-8", ) (backups / "predecessor-backup").mkdir() baseline_backup = root / "failed-backup" predecessor_descriptor = {"marker": "exact"} marker = ( device_plane / RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_REL ) marker.write_text( json.dumps(predecessor_descriptor), encoding="utf-8", ) with ( mock.patch.object(RUNNER, "APPLIED_DIR", applied), mock.patch.object(RUNNER, "BACKUPS_DIR", backups), mock.patch.object(RUNNER, "TMP_DIR", temporary), mock.patch.object(RUNNER, "STATE_FILE", state_file), mock.patch.object( RUNNER, "component_root", return_value=device_plane, ), mock.patch.object( RUNNER, "validate_device_plane_manager_release_payload", return_value=descriptor, ) as payload, mock.patch.object( RUNNER, "load_artifact", return_value=( { "id": predecessor_patch, "component": "device-plane", "type": "app-overlay", }, RUNNER.DEVICE_PLANE_MANAGER_V2_RECONCILIATION_ENTRIES, root / "predecessor-payload", ), ), mock.patch.object( RUNNER, "validate_device_plane_manager_v2_reconciliation_payload", return_value=predecessor_descriptor, ), mock.patch.object( RUNNER, "validate_device_plane_manager_v2_reconciliation_backup", return_value=baseline_backup, ), mock.patch.object( RUNNER, "validate_device_plane_manager_v2_reconciled_baseline", return_value={"accepted": True}, ) as baseline, ): result = ( RUNNER.validate_device_plane_manager_activation_predecessor( root / "payload" ) ) self.assertEqual( result["mode"], "reconciled-manager-forward-activation", ) payload.assert_called_once_with(root / "payload") baseline.assert_called_once_with( baseline_backup, marker_installed=True, ) def test_failed_patch_id_and_sha_are_globally_non_replayable(self): with tempfile.TemporaryDirectory( prefix="nodedc-device-manager-failed-replay-", ) as directory: failed_state = Path(directory) / "failed.jsonl" failed_state.write_text( json.dumps({ "id": "device-manager-release-failed-001", "sha256": "b" * 64, "status": "failed", }) + "\n", encoding="utf-8", ) with mock.patch.object( RUNNER, "FAILED_STATE_FILE", failed_state, ): with self.assertRaisesRegex( RUNNER.DeployError, "SHA is terminal failed", ): RUNNER.reject_failed_artifact_replay( {"id": "different"}, "b" * 64, ) with self.assertRaisesRegex( RUNNER.DeployError, "patch id is terminal failed", ): RUNNER.reject_failed_artifact_replay( {"id": "device-manager-release-failed-001"}, "c" * 64, ) def test_upgrade_accepts_any_current_release_without_runner_patch_ids(self): with tempfile.TemporaryDirectory( prefix="nodedc-device-manager-release-upgrade-", ) as directory: root = Path(directory) applied = root / "applied" backups = root / "backups" temporary = root / "tmp" device_plane = root / "device-plane" for path in (applied, backups, temporary, device_plane / "deployment"): path.mkdir(parents=True) predecessor_patch = "device-manager-release-arbitrary-041" artifact_name = f"nodedc-device-plane-{predecessor_patch}.tgz" artifact = applied / artifact_name artifact_bytes = b"arbitrary-reviewed-device-manager-release" artifact.write_bytes(artifact_bytes) artifact_sha = hashlib.sha256(artifact_bytes).hexdigest() predecessor_descriptor = device_manager_release_v1_descriptor( release_id=predecessor_patch, ) candidate_descriptor = device_manager_release_descriptor( release_id="device-manager-release-arbitrary-042", action="upgrade", predecessor_kind="release", predecessor_patch=predecessor_patch, predecessor_sha=artifact_sha, ) installed = ( device_plane / RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL ) installed.write_text( json.dumps(predecessor_descriptor), encoding="utf-8", ) state_file = root / "applied.jsonl" state_file.write_text( json.dumps({ "id": predecessor_patch, "artifact": artifact_name, "backup_id": "arbitrary-release-backup", "component": "device-plane", "sha256": artifact_sha, "status": "ok", }) + "\n", encoding="utf-8", ) (backups / "arbitrary-release-backup").mkdir() with ( mock.patch.object(RUNNER, "APPLIED_DIR", applied), mock.patch.object(RUNNER, "BACKUPS_DIR", backups), mock.patch.object(RUNNER, "TMP_DIR", temporary), mock.patch.object(RUNNER, "STATE_FILE", state_file), mock.patch.object( RUNNER, "component_root", return_value=device_plane, ), mock.patch.object( RUNNER, "validate_device_plane_manager_release_payload", side_effect=( candidate_descriptor, predecessor_descriptor, ), ), mock.patch.object( RUNNER, "load_artifact", return_value=( { "id": predecessor_patch, "component": "device-plane", "type": "app-overlay", }, RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES, root / "predecessor-payload", ), ), mock.patch.object( RUNNER, "healthcheck_compose_service", ) as health, ): result = ( RUNNER.validate_device_plane_manager_activation_predecessor( root / "candidate-payload" ) ) self.assertEqual(result["mode"], "active-manager-forward-upgrade") self.assertEqual( [call.args for call in health.call_args_list], [ ("device-plane", "device-control-core"), ("device-plane", "device-manager"), ("device-plane", "device-postgres"), ], ) def test_initial_install_rollback_removes_manager_and_restores_core_only(self): entries = RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES missing = { RUNNER.DEVICE_PLANE_MANAGER_COMPOSE_REL, "services/device-manager", RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_REL, } existing = [entry for entry in entries if entry not in missing] with tempfile.TemporaryDirectory( prefix="nodedc-device-manager-rollback-", ) as directory: backup = Path(directory) / "backup" backup.mkdir() (backup / "existing-files.txt").write_text( "\n".join(existing) + "\n", encoding="utf-8", ) (backup / "missing-files.txt").write_text( "\n".join(entry for entry in entries if entry in missing) + "\n", encoding="utf-8", ) (backup / "runtime-before.json").write_text( json.dumps(healthy_device_plane_inventory()), encoding="utf-8", ) with ( mock.patch.object( RUNNER, "stop_and_remove_compose_services", ) as stop, mock.patch.object( RUNNER, "restore_platform_overlay", return_value=len(entries), ) as restore, mock.patch.object( RUNNER, "run_component_runtime", ) as restore_runtime, mock.patch.object( RUNNER, "healthcheck_compose_service_with_grace", ) as restore_health, mock.patch.object( RUNNER, "component_healthchecks", return_value=("core-health",), ), mock.patch.object( RUNNER, "healthcheck_url", ) as restore_url_health, mock.patch.object( RUNNER, "run_healthchecks", ) as generic_health, ): result = RUNNER.rollback_device_plane_apply( Path(directory) / "live", backup, entries, "test-stamp", True, ("device-control-core", "device-manager"), ) stop.assert_called_once_with("device-plane", ("device-manager",)) restore.assert_called_once() restore_runtime.assert_called_once_with( "device-plane", existing, ("device-control-core",), ) restore_health.assert_called_once_with( "device-plane", "device-control-core", ) restore_url_health.assert_called_once_with("core-health") generic_health.assert_not_called() self.assertEqual( result, f"source+runtime-restored:{len(entries)}", ) def test_upgrade_rollback_restores_previous_core_and_manager_release(self): entries = RUNNER.DEVICE_PLANE_MANAGER_CONTROL_PLANE_ENTRIES with tempfile.TemporaryDirectory( prefix="nodedc-device-manager-upgrade-rollback-", ) as directory: backup = Path(directory) / "backup" backup.mkdir() (backup / "existing-files.txt").write_text( "\n".join(entries) + "\n", encoding="utf-8", ) (backup / "missing-files.txt").write_text("", encoding="utf-8") (backup / "runtime-before.json").write_text( json.dumps(healthy_device_plane_inventory(include_manager=True)), encoding="utf-8", ) with ( mock.patch.object( RUNNER, "stop_and_remove_compose_services", ) as stop, mock.patch.object( RUNNER, "restore_platform_overlay", return_value=len(entries), ), mock.patch.object( RUNNER, "run_component_runtime", ) as restore_runtime, mock.patch.object( RUNNER, "healthcheck_compose_service_with_grace", ) as restore_health, mock.patch.object( RUNNER, "component_healthchecks", return_value=(), ), ): result = RUNNER.rollback_device_plane_apply( Path(directory) / "live", backup, entries, "test-stamp", True, ("device-control-core", "device-manager"), ) stop.assert_not_called() restore_runtime.assert_called_once_with( "device-plane", list(entries), ("device-control-core", "device-manager"), ) self.assertEqual( [call.args for call in restore_health.call_args_list], [ ("device-plane", "device-control-core"), ("device-plane", "device-manager"), ], ) self.assertEqual( result, f"source+runtime-restored:{len(entries)}", ) def test_edge_upgrade_v2_rollback_restores_upgrade_019_core_runtime(self): entries = RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_ENTRIES missing = {RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL} existing = [entry for entry in entries if entry not in missing] with tempfile.TemporaryDirectory( prefix="nodedc-device-edge-upgrade-v2-rollback-", ) as directory: backup = Path(directory) / "backup" backup.mkdir() (backup / "existing-files.txt").write_text( "\n".join(existing) + "\n", encoding="utf-8", ) (backup / "missing-files.txt").write_text( RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V2_REL + "\n", encoding="utf-8", ) (backup / "runtime-before.json").write_text( json.dumps(healthy_device_plane_inventory(include_manager=True)), encoding="utf-8", ) with ( mock.patch.object( RUNNER, "stop_and_remove_compose_services", ) as stop, mock.patch.object( RUNNER, "restore_platform_overlay", return_value=len(entries), ), mock.patch.object( RUNNER, "run_component_runtime", ) as restore_runtime, mock.patch.object( RUNNER, "healthcheck_compose_service_with_grace", ) as restore_health, mock.patch.object( RUNNER, "validate_device_manager_control_plane_runtime", ) as runtime_acceptance, ): result = RUNNER.rollback_device_plane_apply( Path(directory) / "live", backup, entries, "test-stamp", True, ("device-control-core",), ) stop.assert_not_called() restore_runtime.assert_called_once_with( "device-plane", existing, ("device-control-core",), ) self.assertEqual( [call.args for call in restore_health.call_args_list], [ ("device-plane", "device-control-core"), ("device-plane", "device-manager"), ("device-plane", "device-gateway"), ("device-plane", "device-postgres"), ], ) runtime_acceptance.assert_called_once_with(require_edge_channel=True) self.assertEqual(result, f"source+runtime-restored:{len(entries)}") def test_edge_upgrade_v4_rollback_restores_upgrade_021_core_runtime(self): entries = RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_ENTRIES missing = { RUNNER.DEVICE_PLANE_EDGE_CORE_CHANNEL_UPGRADE_V4_REL, } existing = [entry for entry in entries if entry not in missing] with tempfile.TemporaryDirectory( prefix="nodedc-device-edge-upgrade-v4-rollback-", ) as directory: backup = Path(directory) / "backup" backup.mkdir() (backup / "existing-files.txt").write_text( "\n".join(existing) + "\n", encoding="utf-8", ) (backup / "missing-files.txt").write_text( "\n".join(sorted(missing)) + "\n", encoding="utf-8", ) (backup / "runtime-before.json").write_text( json.dumps(healthy_device_plane_inventory(include_manager=True)), encoding="utf-8", ) with ( mock.patch.object( RUNNER, "stop_and_remove_compose_services", ) as stop, mock.patch.object( RUNNER, "restore_platform_overlay", return_value=len(entries), ), mock.patch.object( RUNNER, "run_component_runtime", ) as restore_runtime, mock.patch.object( RUNNER, "healthcheck_compose_service_with_grace", ) as restore_health, mock.patch.object( RUNNER, "validate_device_manager_control_plane_runtime", ) as runtime_acceptance, ): result = RUNNER.rollback_device_plane_apply( Path(directory) / "live", backup, entries, "test-stamp", True, ("device-control-core",), ) stop.assert_not_called() restore_runtime.assert_called_once_with( "device-plane", existing, ("device-control-core",), ) self.assertEqual( [call.args for call in restore_health.call_args_list], [ ("device-plane", "device-control-core"), ("device-plane", "device-manager"), ("device-plane", "device-gateway"), ("device-plane", "device-postgres"), ], ) runtime_acceptance.assert_called_once_with(require_edge_channel=True) self.assertEqual(result, f"source+runtime-restored:{len(entries)}") def test_control_core_release_rollback_restores_only_core_on_v4_topology(self): entries = RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_ENTRIES missing = {RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_REL} existing = [entry for entry in entries if entry not in missing] with tempfile.TemporaryDirectory( prefix="nodedc-control-core-release-rollback-", ) as directory: backup = Path(directory) / "backup" backup.mkdir() (backup / "existing-files.txt").write_text( "\n".join(existing) + "\n", encoding="utf-8", ) (backup / "missing-files.txt").write_text( RUNNER.DEVICE_PLANE_CONTROL_CORE_RELEASE_REL + "\n", encoding="utf-8", ) (backup / "runtime-before.json").write_text( json.dumps(healthy_device_plane_inventory(include_manager=True)), encoding="utf-8", ) with ( mock.patch.object( RUNNER, "stop_and_remove_compose_services", ) as stop, mock.patch.object( RUNNER, "restore_platform_overlay", return_value=len(entries), ), mock.patch.object( RUNNER, "run_component_runtime", ) as restore_runtime, mock.patch.object( RUNNER, "healthcheck_compose_service_with_grace", ) as restore_health, mock.patch.object( RUNNER, "validate_device_manager_control_plane_runtime", ) as runtime_acceptance, ): result = RUNNER.rollback_device_plane_apply( Path(directory) / "live", backup, entries, "test-stamp", True, ("device-control-core",), ) stop.assert_not_called() restore_runtime.assert_called_once_with( "device-plane", existing, ("device-control-core",), ) self.assertEqual( [call.args for call in restore_health.call_args_list], [ ("device-plane", "device-control-core"), ("device-plane", "device-manager"), ("device-plane", "device-gateway"), ("device-plane", "device-postgres"), ], ) runtime_acceptance.assert_called_once_with( require_edge_channel=True, core_network_mode="private-egress", ) self.assertEqual(result, f"source+runtime-restored:{len(entries)}") if __name__ == "__main__": unittest.main(verbosity=2)