prole/tests/test_ekosystem.py
chrisfu d114801758 feat: wire ekosystem UUID system into CNPG manifests (Tasks 1, 3, 4)
Task 1 — ConfigMap + CNPG wiring:
- Add k8s/knoe/knoe-ekosystem-sql.yaml: ConfigMap embedding ekosystem.sql
  and ekosystem_objects.sql for CNPG postInitApplicationSQLRefs
- Add scripts/gen-ekosystem-configmap.py: generation script to keep the
  ConfigMap in sync with knoe-db/schema/ekosystem*.sql source files
- Add Makefile target: make k8s/knoe/knoe-ekosystem-sql.yaml
- Wire postInitApplicationSQLRefs into all three CNPG cluster manifests:
    k8s/knoe/knoe-db.yaml (k3s / prole-service-context production)
    deploy/gcp/gke/knoe-db.yaml (GKE)
    deploy/opentofu/k3s/manifests/knoe/knoe-db.yaml (OpenTofu k3s)
- Add knoe-ekosystem-sql.yaml to k8s/knoe/kustomization.yaml

Task 3 — Python counterpart utility:
- Add knoe/ekosystem.py: thread-safe EkosystemID generator matching the
  PostgreSQL bit layout [49:ts_ms|12:tenant|10:shard|11:seq], with
  decode() and can_access() helpers
- Add tests/test_ekosystem.py: 23 tests covering base36 encoding,
  round-trips, thread safety, can_access, and the spec round-trip assertion

Task 4 — knoe.user ekosystem_uuid column:
- Add ALTER TABLE knoe.user ADD COLUMN IF NOT EXISTS ekosystem_uuid text UNIQUE
  to postInitSQL in all three CNPG manifests

Task 2 (register prole tenant) requires a live DB connection — manual step.
Task 5 (LDAP/AD reconciler) is design-only per spec.

Co-authored-by: Junie <junie@jetbrains.com>
2026-05-30 00:26:48 -07:00

198 lines
5.2 KiB
Python

"""Tests for knoe.ekosystem — Python ekosystem UUID utility."""
import threading
import time
import pytest
from knoe.ekosystem import (
ALPHABET,
EPOCH_MS,
EkosystemID,
_from_base36,
_to_base36,
can_access,
decode,
)
# ---------------------------------------------------------------------------
# Base36 encoding round-trip
# ---------------------------------------------------------------------------
def test_base36_roundtrip():
for n in [0, 1, 35, 36, 1000, 2**82 - 1]:
assert _from_base36(_to_base36(n)) == n
def test_base36_length():
assert len(_to_base36(0)) == 16
assert len(_to_base36(2**82 - 1)) == 16
def test_base36_alphabet():
assert len(ALPHABET) == 36
assert ALPHABET == "0123456789abcdefghijklmnopqrstuvwxyz"
# ---------------------------------------------------------------------------
# EkosystemID generation
# ---------------------------------------------------------------------------
def test_generate_returns_16_chars():
uid = EkosystemID(1, 0).generate()
assert len(uid) == 16
def test_generate_only_base36_chars():
uid = EkosystemID(0, 0).generate()
assert all(c in ALPHABET for c in uid)
def test_round_trip_tenant_id():
"""Spec-mandated round-trip test."""
assert decode(EkosystemID(1, 0).generate())["tenant_id"] == 1
def test_round_trip_shard_id():
assert decode(EkosystemID(0, 5).generate())["shard_id"] == 5
def test_round_trip_tenant_and_shard():
info = decode(EkosystemID(3, 7).generate())
assert info["tenant_id"] == 3
assert info["shard_id"] == 7
def test_issued_at_is_recent():
from datetime import datetime, timezone
uid = EkosystemID(1, 0).generate()
info = decode(uid)
now = datetime.now(tz=timezone.utc)
delta = abs((now - info["issued_at"]).total_seconds())
assert delta < 5, f"issued_at too far from now: {delta}s"
def test_sequential_uids_are_unique():
gen = EkosystemID(1, 0)
uids = [gen.generate() for _ in range(100)]
assert len(set(uids)) == 100
def test_invalid_tenant_id_raises():
with pytest.raises(ValueError):
EkosystemID(4096, 0) # 2**12
def test_invalid_shard_id_raises():
with pytest.raises(ValueError):
EkosystemID(0, 1024) # 2**10
# ---------------------------------------------------------------------------
# project_prefix / object_name
# ---------------------------------------------------------------------------
def test_project_prefix_format():
prefix = EkosystemID(1, 0).project_prefix()
assert prefix.startswith("p_")
assert prefix.endswith("_")
assert len(prefix) == 2 + 16 + 1 # 'p_' + uuid + '_'
def test_object_name_format():
name = EkosystemID(1, 0).object_name("t")
assert name.startswith("t_")
assert len(name) == 2 + 16
# ---------------------------------------------------------------------------
# decode
# ---------------------------------------------------------------------------
def test_decode_wrong_length_raises():
with pytest.raises(ValueError):
decode("tooshort")
def test_decode_seq_zero_on_first_call():
uid = EkosystemID(1, 0).generate()
info = decode(uid)
# seq may be 0 on first call within a millisecond
assert 0 <= info["seq"] < 2**11
def test_decode_ts_ms_positive():
uid = EkosystemID(1, 0).generate()
assert decode(uid)["ts_ms"] > 0
# ---------------------------------------------------------------------------
# Thread safety
# ---------------------------------------------------------------------------
def test_thread_safety_no_duplicates():
gen = EkosystemID(1, 0)
results = []
lock = threading.Lock()
def worker():
uid = gen.generate()
with lock:
results.append(uid)
threads = [threading.Thread(target=worker) for _ in range(200)]
for t in threads:
t.start()
for t in threads:
t.join()
assert len(results) == 200
assert len(set(results)) == 200, "Duplicate UUIDs generated across threads"
# ---------------------------------------------------------------------------
# can_access
# ---------------------------------------------------------------------------
def test_can_access_own_tenant():
uid = EkosystemID(1, 0).generate()
assert can_access(uid, "table", 1, [])
def test_can_access_other_tenant_no_grant():
uid = EkosystemID(1, 0).generate()
assert not can_access(uid, "table", 2, [])
def test_can_access_with_grant():
uid = EkosystemID(1, 0).generate()
grants = [{"object_uuid": uid, "kind": "table", "grantee_tenant_id": 2}]
assert can_access(uid, "table", 2, grants)
def test_can_access_grant_wrong_kind():
uid = EkosystemID(1, 0).generate()
grants = [{"object_uuid": uid, "kind": "embedding", "grantee_tenant_id": 2}]
assert not can_access(uid, "table", 2, grants)
# ---------------------------------------------------------------------------
# EPOCH constant
# ---------------------------------------------------------------------------
def test_epoch_ms_value():
"""2026-01-01 00:00:00 UTC in milliseconds."""
from datetime import datetime, timezone
epoch_dt = datetime(2026, 1, 1, tzinfo=timezone.utc)
assert EPOCH_MS == int(epoch_dt.timestamp() * 1000)