Files
gps-denied-onboard/tests/unit/c6_tile_cache/test_uuid_namespace.py
T
Oleksandr Bezdieniezhnykh dde838d2cc [AZ-304] C6 Postgres schema: additive 0002 migration + UUIDv5
Strictly additive Alembic migration on the AZ-263 baseline (data_model
.md § 6.1 / § 6.3): six new tiles columns (tile_uuid UNIQUE,
location_hash, content_sha256, disk_bytes, accessed_at, uploaded_at),
four new btree indices, one UNIQUE expression index over the
COALESCE-zero-uuid natural key, CHECK widening of
ck_tiles_freshness_status to the AZ-263 + AZ-303 vocabulary UNION,
four NULLable bbox columns on sector_classifications, and a new
tile_freshness_rules table seeded with the two default thresholds.

Pinned UUIDv5 namespace (TILE_NAMESPACE_UUID =
5b8d0c2e-1a4f-4b3a-8c9d-e7f6a3b2c1d0) + derive_tile_id /
derive_location_hash helpers cross-coordinated with
satellite-provider. Migration runner apply_migrations(config) drives
Alembic command.upgrade("head") against the AZ-263 env with one
retry on PG SQLSTATE 40001 and structured INFO logs on apply / no-op.

Contract bump tile_metadata_store.md v1.1.0 -> v1.2.0 adds
TileMetadata.location_hash: UUID | None = None (non-breaking).
module-layout.md updated so c6_tile_cache explicitly Owns
db/migrations/**.

Tier-1 tests: UUIDv5 determinism + locked vectors + DSN resolution +
retry mocked DBAPIError -> 1180 passed, 32 skipped. Tier-2 docker
schema tests gated by @pytest.mark.docker run against the existing
docker-compose.test.yml db service.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 17:05:41 +03:00

178 lines
6.1 KiB
Python

"""AZ-304 AC-10 / AC-11 — UUIDv5 namespace determinism + cross-repo coordination.
The expected UUIDs locked below are the *cross-repo coordination
evidence* between ``gps-denied-onboard`` (Python ``uuid.uuid5``) and
``satellite-provider`` (C# Guid.NewGuid v5 implementation per
``AZ-TBD_tile_identity_uuidv5_bulk_list``). Both sides MUST emit
byte-identical UUIDs for these input vectors; changing any expected
value here without a coordinated cross-workspace release breaks the
correlation key joining the two systems.
"""
from __future__ import annotations
from uuid import UUID
import pytest
from gps_denied_onboard.components.c6_tile_cache._types import TileSource
from gps_denied_onboard.components.c6_tile_cache._uuid_namespace import (
TILE_NAMESPACE_UUID,
derive_location_hash,
derive_tile_id,
)
_EXPECTED_NAMESPACE = UUID("5b8d0c2e-1a4f-4b3a-8c9d-e7f6a3b2c1d0")
# AC-10 — five locked tile-id vectors. (z, x, y, source, flight_id) -> expected uuidv5.
_TILE_ID_VECTORS: list[tuple[int, int, int, str, str | None, str]] = [
(18, 72346, 46342, "googlemaps", None, "6f49531b-1351-55ba-b733-66d3f1fca1a5"),
(
18,
72346,
46342,
"onboard_ingest",
"11111111-2222-3333-4444-555555555555",
"c7f6eda4-3b95-5818-a0b7-1aa8cbb5aa95",
),
(10, 300, 200, "googlemaps", None, "3604dd59-1018-5889-97dc-ba5635761ac5"),
(
21,
999999,
999999,
"onboard_ingest",
"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"36c6e0c0-54a1-56ab-9dfd-a4f8f184cb22",
),
(15, 1000, 2000, "googlemaps", None, "955df722-8d4e-5375-8a23-4f45dc16fef1"),
]
# AC-11 — four locked location-hash vectors. (z, x, y) -> expected uuidv5.
_LOCATION_HASH_VECTORS: list[tuple[int, int, int, str]] = [
(18, 72346, 46342, "e95c7edb-550e-58eb-8f94-3056f73a57d3"),
(10, 300, 200, "76aa22b7-fd8e-5089-8b20-c45fb4a0f5e8"),
(21, 999999, 999999, "4337a27e-f118-524f-8d74-82cf9295c632"),
(15, 1000, 2000, "0501b2fc-0fc8-5330-a407-c7ccbf1fb9c7"),
]
# ----------------------------------------------------------------------
# AC-10: namespace + derivation are deterministic and locked.
def test_ac10_namespace_uuid_locked() -> None:
# Assert
assert TILE_NAMESPACE_UUID == _EXPECTED_NAMESPACE
assert TILE_NAMESPACE_UUID.version == 4 # the namespace itself was a one-time UUIDv4 pick
assert str(TILE_NAMESPACE_UUID) == "5b8d0c2e-1a4f-4b3a-8c9d-e7f6a3b2c1d0"
@pytest.mark.parametrize("z,x,y,source,flight_id,expected", _TILE_ID_VECTORS)
def test_ac10_derive_tile_id_locked_vectors(
z: int, x: int, y: int, source: str, flight_id: str | None, expected: str
) -> None:
# Act
result = derive_tile_id(z, x, y, source, flight_id)
# Assert
assert result == UUID(expected)
assert result.version == 5
@pytest.mark.parametrize("z,x,y,source,flight_id,expected", _TILE_ID_VECTORS)
def test_ac10_derive_tile_id_idempotent_on_second_call(
z: int, x: int, y: int, source: str, flight_id: str | None, expected: str
) -> None:
# Act
first = derive_tile_id(z, x, y, source, flight_id)
second = derive_tile_id(z, x, y, source, flight_id)
# Assert
assert first == second == UUID(expected)
def test_ac10_derive_tile_id_accepts_tile_source_enum() -> None:
"""Passing a `TileSource` enum yields the same UUID as its string value."""
# Act
from_enum = derive_tile_id(18, 72346, 46342, TileSource.GOOGLEMAPS, None)
from_str = derive_tile_id(18, 72346, 46342, "googlemaps", None)
# Assert
assert from_enum == from_str
assert from_enum == UUID("6f49531b-1351-55ba-b733-66d3f1fca1a5")
def test_ac10_derive_tile_id_accepts_uuid_flight_id() -> None:
"""Passing a `UUID` instance for ``flight_id`` matches the string form."""
# Arrange
flight_uuid_str = "11111111-2222-3333-4444-555555555555"
# Act
from_uuid = derive_tile_id(18, 72346, 46342, "onboard_ingest", UUID(flight_uuid_str))
from_str = derive_tile_id(18, 72346, 46342, "onboard_ingest", flight_uuid_str)
# Assert
assert from_uuid == from_str
assert from_uuid == UUID("c7f6eda4-3b95-5818-a0b7-1aa8cbb5aa95")
def test_ac10_derive_tile_id_rejects_unknown_source_type() -> None:
# Act + Assert
with pytest.raises(TypeError, match="source must be"):
derive_tile_id(18, 0, 0, 123, None)
def test_ac10_derive_tile_id_rejects_unknown_flight_id_type() -> None:
# Act + Assert
with pytest.raises(TypeError, match="flight_id must be"):
derive_tile_id(18, 0, 0, "googlemaps", 12345)
def test_ac10_derive_tile_id_rejects_malformed_flight_id_string() -> None:
# Act + Assert
with pytest.raises(ValueError):
derive_tile_id(18, 0, 0, "googlemaps", "not-a-uuid")
# ----------------------------------------------------------------------
# AC-11: location_hash is invariant across source / flight_id.
@pytest.mark.parametrize("z,x,y,expected", _LOCATION_HASH_VECTORS)
def test_ac11_derive_location_hash_locked_vectors(z: int, x: int, y: int, expected: str) -> None:
# Act
result = derive_location_hash(z, x, y)
# Assert
assert result == UUID(expected)
assert result.version == 5
def test_ac11_location_hash_invariant_across_source_and_flight() -> None:
"""Same (z, x, y) yields the same location_hash for any source + flight_id."""
# Arrange
cell = (18, 72346, 46342)
cell_lh = derive_location_hash(*cell)
# Act + Assert — `derive_tile_id` produces _different_ tile UUIDs for
# different source/flight combos but the cell-bag is the same; this is
# explicitly tested via the locked vectors above. Here we only need to
# confirm that `location_hash` NEVER depends on these inputs.
for _source in ("googlemaps", "onboard_ingest"):
for _flight_id in (None, "11111111-2222-3333-4444-555555555555"):
assert derive_location_hash(*cell) == cell_lh
def test_ac11_different_cells_yield_different_location_hashes() -> None:
# Act
lh1 = derive_location_hash(18, 72346, 46342)
lh2 = derive_location_hash(18, 72346, 46343) # neighbour cell
lh3 = derive_location_hash(19, 72346, 46342) # same x,y at different zoom
# Assert
assert lh1 != lh2
assert lh1 != lh3
assert lh2 != lh3