fix(device-pool): scope primary key by host and thread through capability_tags
- pooled_devices primary key changed from device_id alone to (host_id, device_id), so two hosts reporting the same local device_id no longer crash sync_host_devices() with an uncaught sqlite3.IntegrityError. - Device gained a capability_tags field so DevicePool.sync_host_devices() can actually populate PooledDevice.capability_tags from a real host sync instead of always falling back to an empty list. openspec: device-pool capability, archived change cloud-runtime
This commit is contained in:
@@ -125,3 +125,40 @@ def test_capability_tags_round_trip(tmp_path) -> None:
|
||||
device = store.get_device("dev-1")
|
||||
assert device is not None
|
||||
assert device.capability_tags == ["ios", "physical"]
|
||||
|
||||
|
||||
def test_two_hosts_with_colliding_local_device_id_do_not_crash(tmp_path) -> None:
|
||||
"""Two hosts legitimately reporting the same local device_id string are
|
||||
different devices in the pool; syncing both must not violate a uniqueness
|
||||
constraint and must keep both rows independently addressable by host."""
|
||||
store = CloudStore(tmp_path / "cloud.sqlite3")
|
||||
from datetime import UTC, datetime
|
||||
|
||||
store.upsert_host("host-a", address="a", last_seen_at=datetime(2026, 1, 1, tzinfo=UTC))
|
||||
store.upsert_host("host-b", address="b", last_seen_at=datetime(2026, 1, 1, tzinfo=UTC))
|
||||
|
||||
store.replace_host_devices("host-a", [_pooled("dev-1", "host-a", status="idle")])
|
||||
# Same local device_id "dev-1", different host -- must not raise.
|
||||
store.replace_host_devices("host-b", [_pooled("dev-1", "host-b", status="busy")])
|
||||
|
||||
devices = store.list_devices()
|
||||
assert len(devices) == 2
|
||||
by_host = {d.host_id: d for d in devices}
|
||||
assert set(by_host) == {"host-a", "host-b"}
|
||||
assert by_host["host-a"].device_id == "dev-1"
|
||||
assert by_host["host-b"].device_id == "dev-1"
|
||||
assert by_host["host-a"].status == "idle"
|
||||
assert by_host["host-b"].status == "busy"
|
||||
|
||||
# Lookup by id alone must still work (finds a device on some host) without crashing.
|
||||
found = store.get_device("dev-1")
|
||||
assert found is not None
|
||||
assert found.host_id in {"host-a", "host-b"}
|
||||
|
||||
# Re-syncing host-a alone must not disturb host-b's colliding-id row.
|
||||
store.replace_host_devices("host-a", [_pooled("dev-1", "host-a", status="error")])
|
||||
devices = store.list_devices()
|
||||
assert len(devices) == 2
|
||||
by_host = {d.host_id: d for d in devices}
|
||||
assert by_host["host-a"].status == "error"
|
||||
assert by_host["host-b"].status == "busy"
|
||||
|
||||
Reference in New Issue
Block a user