mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 12:03:59 +00:00
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>
198 lines
5.2 KiB
Python
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)
|