piphi-runtime-kit-python 0.4.2__tar.gz → 0.4.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/PKG-INFO +1 -1
  2. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/pyproject.toml +1 -1
  3. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/__init__.py +19 -1
  4. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/config_sync.py +74 -0
  5. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/health.py +1 -0
  6. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/schemas.py +22 -0
  7. piphi_runtime_kit_python-0.4.3/tests/test_config_sync.py +138 -0
  8. piphi_runtime_kit_python-0.4.2/tests/test_config_sync.py +0 -74
  9. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/LICENSE +0 -0
  10. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/README.md +0 -0
  11. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/adapters/__init__.py +0 -0
  12. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/adapters/fastapi.py +0 -0
  13. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/fastapi.py +0 -0
  14. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/__init__.py +0 -0
  15. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/auth.py +0 -0
  16. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/configuration.py +0 -0
  17. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/context.py +0 -0
  18. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/discovery.py +0 -0
  19. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/dispatch.py +0 -0
  20. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/errors.py +0 -0
  21. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/events.py +0 -0
  22. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/identity.py +0 -0
  23. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/lifespan.py +0 -0
  24. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/mqtt.py +0 -0
  25. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/registry.py +0 -0
  26. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/starter.py +0 -0
  27. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/state.py +0 -0
  28. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/tasks.py +0 -0
  29. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/src/piphi_runtime_kit_python/runtime/telemetry.py +0 -0
  30. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/__init__.py +0 -0
  31. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_fastapi_adapter.py +0 -0
  32. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_auth.py +0 -0
  33. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_auth_matrix.py +0 -0
  34. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_config_sync_matrix.py +0 -0
  35. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_configuration.py +0 -0
  36. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_configuration_matrix.py +0 -0
  37. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_delivery_errors.py +0 -0
  38. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_discovery.py +0 -0
  39. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_discovery_matrix.py +0 -0
  40. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_dispatch.py +0 -0
  41. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_dispatch_matrix.py +0 -0
  42. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_errors_matrix.py +0 -0
  43. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_events.py +0 -0
  44. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_events_matrix.py +0 -0
  45. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_health.py +0 -0
  46. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_identity.py +0 -0
  47. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_lifespan.py +0 -0
  48. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_mqtt.py +0 -0
  49. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_mqtt_matrix.py +0 -0
  50. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_registry.py +0 -0
  51. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_registry_matrix.py +0 -0
  52. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_starter.py +0 -0
  53. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_support_matrix.py +0 -0
  54. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_tasks.py +0 -0
  55. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_telemetry.py +0 -0
  56. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_runtime_telemetry_matrix.py +0 -0
  57. {piphi_runtime_kit_python-0.4.2 → piphi_runtime_kit_python-0.4.3}/tests/test_schemas_matrix.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: piphi-runtime-kit-python
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: PiPhi Network runtime integration helpers
5
5
  Keywords: piphi,runtime,integration,iot
6
6
  Author-Email: KelvinSan <support@piphi.network>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "piphi-runtime-kit-python"
3
- version = "0.4.2"
3
+ version = "0.4.3"
4
4
  description = "PiPhi Network runtime integration helpers"
5
5
  authors = [
6
6
  { name = "KelvinSan", email = "support@piphi.network" },
@@ -8,7 +8,18 @@ from .runtime.auth import (
8
8
  format_runtime_auth_sync_log,
9
9
  mask_token,
10
10
  )
11
- from .runtime.config_sync import ConfigSyncCoordinator, build_sync_response, reconcile_config_ids
11
+ from .runtime.config_sync import (
12
+ CORE_BASE_URL_ENV_NAME,
13
+ DEFAULT_RUNTIME_CONFIG_SNAPSHOT_FILENAME,
14
+ DEFAULT_RUNTIME_VOLUME_DIR,
15
+ RUNTIME_CONFIG_SNAPSHOT_PATH_ENV_NAME,
16
+ ConfigSyncCoordinator,
17
+ build_sync_response,
18
+ load_runtime_config_snapshot,
19
+ reconcile_config_ids,
20
+ resolve_core_base_url,
21
+ resolve_runtime_config_snapshot_path,
22
+ )
12
23
  from .runtime.configuration import (
13
24
  build_config_apply_response,
14
25
  build_config_remove_response,
@@ -109,6 +120,10 @@ __all__ = [
109
120
  "CoreUnavailableError",
110
121
  "CoreUnexpectedResponseError",
111
122
  "DEFAULT_CORE_CLIENT_TIMEOUT_SECONDS",
123
+ "CORE_BASE_URL_ENV_NAME",
124
+ "DEFAULT_RUNTIME_CONFIG_SNAPSHOT_FILENAME",
125
+ "DEFAULT_RUNTIME_VOLUME_DIR",
126
+ "RUNTIME_CONFIG_SNAPSHOT_PATH_ENV_NAME",
112
127
  "DEFAULT_RUNTIME_CONTAINER_ID_ENV_NAME",
113
128
  "DEFAULT_RUNTIME_INTERNAL_TOKEN_ENV_NAME",
114
129
  "RUNTIME_CONTAINER_ID_HEADER_NAME",
@@ -169,6 +184,7 @@ __all__ = [
169
184
  "format_discovery_attempt_log",
170
185
  "format_event_log",
171
186
  "format_runtime_auth_sync_log",
187
+ "load_runtime_config_snapshot",
172
188
  "mask_token",
173
189
  "MqttBrokerConfig",
174
190
  "MqttJsonClient",
@@ -176,6 +192,8 @@ __all__ = [
176
192
  "normalize_event_payload",
177
193
  "redact_config_secrets",
178
194
  "reconcile_config_ids",
195
+ "resolve_core_base_url",
196
+ "resolve_runtime_config_snapshot_path",
179
197
  "runtime_lifespan",
180
198
  "schedule_event_delivery",
181
199
  "schedule_telemetry_delivery",
@@ -3,6 +3,9 @@ from __future__ import annotations
3
3
  """Helpers for basic config-sync orchestration and generation tracking."""
4
4
 
5
5
  from dataclasses import dataclass
6
+ import json
7
+ import os
8
+ from pathlib import Path
6
9
  from typing import Awaitable, Callable, TypeVar
7
10
 
8
11
  from ..schemas import RuntimeConfig, RuntimeConfigSnapshot, RuntimeConfigSyncResponse
@@ -10,6 +13,77 @@ from .state import RuntimeProcessState
10
13
 
11
14
  TConfig = TypeVar("TConfig", bound=RuntimeConfig)
12
15
 
16
+ CORE_BASE_URL_ENV_NAME = "PIPHI_CORE_BASE_URL"
17
+ RUNTIME_CONTAINER_ID_ENV_NAME = "PIPHI_CONTAINER_ID"
18
+ RUNTIME_CONFIG_SNAPSHOT_PATH_ENV_NAME = "PIPHI_CONFIG_SNAPSHOT_PATH"
19
+ DEFAULT_RUNTIME_VOLUME_DIR = "/.piphinetwork"
20
+ DEFAULT_RUNTIME_CONFIG_SNAPSHOT_FILENAME = "configs.json"
21
+
22
+
23
+ def resolve_core_base_url(default: str | None = None) -> str | None:
24
+ """Resolve Core's base URL from the managed-runtime environment."""
25
+ value = (os.getenv(CORE_BASE_URL_ENV_NAME) or "").strip().rstrip("/")
26
+ if value:
27
+ return value
28
+ return default.rstrip("/") if isinstance(default, str) and default.strip() else default
29
+
30
+
31
+ def resolve_runtime_config_snapshot_path(
32
+ *,
33
+ container_id: str | None = None,
34
+ volume_dir: str | Path = DEFAULT_RUNTIME_VOLUME_DIR,
35
+ ) -> Path:
36
+ """Resolve the local config snapshot path mounted by PiPhi Core."""
37
+ explicit_path = (os.getenv(RUNTIME_CONFIG_SNAPSHOT_PATH_ENV_NAME) or "").strip()
38
+ if explicit_path:
39
+ return Path(explicit_path)
40
+
41
+ resolved_container_id = (
42
+ str(container_id or "").strip()
43
+ or (os.getenv(RUNTIME_CONTAINER_ID_ENV_NAME) or "").strip()
44
+ )
45
+ if resolved_container_id:
46
+ return Path(volume_dir).joinpath(f"{resolved_container_id}.json")
47
+ return Path(volume_dir).joinpath(DEFAULT_RUNTIME_CONFIG_SNAPSHOT_FILENAME)
48
+
49
+
50
+ def load_runtime_config_snapshot(
51
+ *,
52
+ path: str | Path | None = None,
53
+ container_id: str | None = None,
54
+ volume_dir: str | Path = DEFAULT_RUNTIME_VOLUME_DIR,
55
+ ) -> RuntimeConfigSnapshot | None:
56
+ """Load Core's last known config snapshot from the mounted runtime volume.
57
+
58
+ Missing, unreadable, or invalid snapshots return ``None`` so integrations can
59
+ treat snapshot rehydration as a safe startup fallback rather than a crash
60
+ trigger.
61
+ """
62
+ snapshot_path = (
63
+ Path(path)
64
+ if path is not None
65
+ else resolve_runtime_config_snapshot_path(
66
+ container_id=container_id,
67
+ volume_dir=volume_dir,
68
+ )
69
+ )
70
+ try:
71
+ raw_payload = snapshot_path.read_text(encoding="utf-8")
72
+ except (FileNotFoundError, OSError):
73
+ return None
74
+
75
+ try:
76
+ payload = json.loads(raw_payload)
77
+ except json.JSONDecodeError:
78
+ return None
79
+ if not isinstance(payload, dict):
80
+ return None
81
+
82
+ try:
83
+ return RuntimeConfigSnapshot.model_validate(payload)
84
+ except ValueError:
85
+ return None
86
+
13
87
 
14
88
  def reconcile_config_ids(
15
89
  *,
@@ -15,6 +15,7 @@ def _build_runtime_snapshot(runtime_context: RuntimeContext) -> dict[str, Any]:
15
15
  "core_auth_present": bool(runtime_context.auth.internal_token),
16
16
  "core_http_client_bound": process_state.core_http_client is not None,
17
17
  "current_generation": process_state.current_generation,
18
+ "config_generation": process_state.current_generation,
18
19
  "pending_task_count": len(
19
20
  [task for task in process_state.background_tasks if not task.done()]
20
21
  ),
@@ -35,12 +35,34 @@ class RuntimeConfig(BaseModel):
35
35
 
36
36
  class RuntimeConfigSnapshot(BaseModel):
37
37
  """Snapshot payload sent from PiPhi Core to an integration runtime."""
38
+ model_config = ConfigDict(populate_by_name=True, extra="allow")
39
+
40
+ schema_version: int | None = Field(
41
+ default=None,
42
+ validation_alias=AliasChoices("schema_version", "schemaVersion"),
43
+ )
38
44
  container_id: str
39
45
  integration_id: str | None = None
40
46
  driver_pid: int | None = None
41
47
  reason: str | None = None
42
48
  generation: int | None = None
49
+ updated_at: str | None = Field(
50
+ default=None,
51
+ validation_alias=AliasChoices("updated_at", "updatedAt"),
52
+ )
43
53
  configs: list[RuntimeConfig] = Field(default_factory=list)
54
+ deleted_config_ids: list[str] = Field(
55
+ default_factory=list,
56
+ validation_alias=AliasChoices("deleted_config_ids", "deletedConfigIds"),
57
+ )
58
+ config_hash: str | None = Field(
59
+ default=None,
60
+ validation_alias=AliasChoices("config_hash", "configHash"),
61
+ )
62
+ internal_token: str | None = Field(
63
+ default=None,
64
+ validation_alias=AliasChoices("internal_token", "internalToken"),
65
+ )
44
66
 
45
67
 
46
68
  class RuntimeConfigSyncResponse(BaseModel):
@@ -0,0 +1,138 @@
1
+ import pytest
2
+
3
+ from piphi_runtime_kit_python import (
4
+ ConfigSyncCoordinator,
5
+ RuntimeConfig,
6
+ RuntimeConfigSnapshot,
7
+ RuntimeProcessState,
8
+ load_runtime_config_snapshot,
9
+ reconcile_config_ids,
10
+ resolve_core_base_url,
11
+ resolve_runtime_config_snapshot_path,
12
+ )
13
+
14
+
15
+ def test_reconcile_config_ids_returns_removed_and_unchanged_ids() -> None:
16
+ removed_ids, unchanged_ids = reconcile_config_ids(
17
+ active_ids=["a", "b", "c"],
18
+ incoming_ids=["b", "c", "d"],
19
+ )
20
+
21
+ assert removed_ids == ["a"]
22
+ assert unchanged_ids == ["b", "c"]
23
+
24
+
25
+ def test_config_sync_coordinator_detects_stale_snapshot() -> None:
26
+ state = RuntimeProcessState(current_generation=10)
27
+ coordinator = ConfigSyncCoordinator(process_state=state)
28
+ snapshot = RuntimeConfigSnapshot(
29
+ container_id="container-1",
30
+ generation=9,
31
+ configs=[RuntimeConfig(id="device-1")],
32
+ )
33
+
34
+ assert coordinator.is_stale(snapshot) is True
35
+
36
+
37
+ def test_config_sync_coordinator_updates_generation() -> None:
38
+ state = RuntimeProcessState()
39
+ coordinator = ConfigSyncCoordinator(process_state=state)
40
+
41
+ assert coordinator.mark_generation(42) == 42
42
+ assert state.current_generation == 42
43
+
44
+
45
+ def test_resolve_core_base_url_uses_managed_runtime_env(monkeypatch) -> None:
46
+ monkeypatch.setenv("PIPHI_CORE_BASE_URL", "http://127.0.0.1:31419/")
47
+
48
+ assert resolve_core_base_url() == "http://127.0.0.1:31419"
49
+
50
+
51
+ def test_resolve_runtime_config_snapshot_path_uses_container_id(monkeypatch, tmp_path) -> None:
52
+ monkeypatch.delenv("PIPHI_CONFIG_SNAPSHOT_PATH", raising=False)
53
+ monkeypatch.setenv("PIPHI_CONTAINER_ID", "container-1")
54
+
55
+ assert resolve_runtime_config_snapshot_path(volume_dir=tmp_path) == tmp_path / "container-1.json"
56
+
57
+
58
+ def test_resolve_runtime_config_snapshot_path_prefers_explicit_path(monkeypatch, tmp_path) -> None:
59
+ explicit_path = tmp_path / "custom.json"
60
+ monkeypatch.setenv("PIPHI_CONFIG_SNAPSHOT_PATH", str(explicit_path))
61
+
62
+ assert resolve_runtime_config_snapshot_path(volume_dir=tmp_path) == explicit_path
63
+
64
+
65
+ def test_load_runtime_config_snapshot_reads_core_volume_contract(tmp_path) -> None:
66
+ snapshot_path = tmp_path / "container-1.json"
67
+ snapshot_path.write_text(
68
+ """
69
+ {
70
+ "schema_version": 1,
71
+ "container_id": "container-1",
72
+ "integration_id": "integration-1",
73
+ "reason": "startup",
74
+ "generation": 42,
75
+ "updated_at": "2026-04-27T12:00:00+00:00",
76
+ "configs": [{"id": "device-1", "serial": "abc123"}],
77
+ "deleted_config_ids": ["device-2"],
78
+ "config_hash": "sha256:abc",
79
+ "internal_token": "runtime-token"
80
+ }
81
+ """,
82
+ encoding="utf-8",
83
+ )
84
+
85
+ snapshot = load_runtime_config_snapshot(path=snapshot_path)
86
+
87
+ assert snapshot is not None
88
+ assert snapshot.schema_version == 1
89
+ assert snapshot.container_id == "container-1"
90
+ assert snapshot.generation == 42
91
+ assert snapshot.configs[0].id == "device-1"
92
+ assert snapshot.deleted_config_ids == ["device-2"]
93
+ assert snapshot.config_hash == "sha256:abc"
94
+ assert snapshot.internal_token == "runtime-token"
95
+
96
+
97
+ def test_load_runtime_config_snapshot_returns_none_for_missing_or_invalid_snapshot(tmp_path) -> None:
98
+ assert load_runtime_config_snapshot(path=tmp_path / "missing.json") is None
99
+
100
+ invalid_path = tmp_path / "invalid.json"
101
+ invalid_path.write_text("not-json", encoding="utf-8")
102
+
103
+ assert load_runtime_config_snapshot(path=invalid_path) is None
104
+
105
+
106
+ @pytest.mark.asyncio
107
+ async def test_config_sync_coordinator_apply_snapshot_orchestrates_callbacks() -> None:
108
+ state = RuntimeProcessState(current_generation=1)
109
+ coordinator = ConfigSyncCoordinator(process_state=state)
110
+ snapshot = RuntimeConfigSnapshot(
111
+ container_id="container-1",
112
+ generation=2,
113
+ configs=[RuntimeConfig(id="device-2"), RuntimeConfig(id="device-3")],
114
+ )
115
+ active_ids = ["device-1", "device-2"]
116
+ applied: list[str] = []
117
+ removed: list[str] = []
118
+
119
+ async def apply_config(config: RuntimeConfig) -> None:
120
+ applied.append(config.id)
121
+
122
+ async def remove_config(config_id: str) -> bool:
123
+ removed.append(config_id)
124
+ return True
125
+
126
+ response = await coordinator.apply_snapshot(
127
+ snapshot=snapshot,
128
+ active_config_ids=active_ids,
129
+ apply_config=apply_config,
130
+ remove_config=remove_config,
131
+ get_active_config_ids=lambda: ["device-2", "device-3"],
132
+ )
133
+
134
+ assert applied == ["device-2", "device-3"]
135
+ assert removed == ["device-1"]
136
+ assert response.status == "synced"
137
+ assert response.generation == 2
138
+ assert response.active_config_ids == ["device-2", "device-3"]
@@ -1,74 +0,0 @@
1
- import pytest
2
-
3
- from piphi_runtime_kit_python import (
4
- ConfigSyncCoordinator,
5
- RuntimeConfig,
6
- RuntimeConfigSnapshot,
7
- RuntimeProcessState,
8
- reconcile_config_ids,
9
- )
10
-
11
-
12
- def test_reconcile_config_ids_returns_removed_and_unchanged_ids() -> None:
13
- removed_ids, unchanged_ids = reconcile_config_ids(
14
- active_ids=["a", "b", "c"],
15
- incoming_ids=["b", "c", "d"],
16
- )
17
-
18
- assert removed_ids == ["a"]
19
- assert unchanged_ids == ["b", "c"]
20
-
21
-
22
- def test_config_sync_coordinator_detects_stale_snapshot() -> None:
23
- state = RuntimeProcessState(current_generation=10)
24
- coordinator = ConfigSyncCoordinator(process_state=state)
25
- snapshot = RuntimeConfigSnapshot(
26
- container_id="container-1",
27
- generation=9,
28
- configs=[RuntimeConfig(id="device-1")],
29
- )
30
-
31
- assert coordinator.is_stale(snapshot) is True
32
-
33
-
34
- def test_config_sync_coordinator_updates_generation() -> None:
35
- state = RuntimeProcessState()
36
- coordinator = ConfigSyncCoordinator(process_state=state)
37
-
38
- assert coordinator.mark_generation(42) == 42
39
- assert state.current_generation == 42
40
-
41
-
42
- @pytest.mark.asyncio
43
- async def test_config_sync_coordinator_apply_snapshot_orchestrates_callbacks() -> None:
44
- state = RuntimeProcessState(current_generation=1)
45
- coordinator = ConfigSyncCoordinator(process_state=state)
46
- snapshot = RuntimeConfigSnapshot(
47
- container_id="container-1",
48
- generation=2,
49
- configs=[RuntimeConfig(id="device-2"), RuntimeConfig(id="device-3")],
50
- )
51
- active_ids = ["device-1", "device-2"]
52
- applied: list[str] = []
53
- removed: list[str] = []
54
-
55
- async def apply_config(config: RuntimeConfig) -> None:
56
- applied.append(config.id)
57
-
58
- async def remove_config(config_id: str) -> bool:
59
- removed.append(config_id)
60
- return True
61
-
62
- response = await coordinator.apply_snapshot(
63
- snapshot=snapshot,
64
- active_config_ids=active_ids,
65
- apply_config=apply_config,
66
- remove_config=remove_config,
67
- get_active_config_ids=lambda: ["device-2", "device-3"],
68
- )
69
-
70
- assert applied == ["device-2", "device-3"]
71
- assert removed == ["device-1"]
72
- assert response.status == "synced"
73
- assert response.generation == 2
74
- assert response.active_config_ids == ["device-2", "device-3"]