orgstate / tests /test_infra_api_ops_summary.py
Legal-i's picture
Initial OrgState deploy via Stage 150 free-tier stack
d2d1903 verified
"""
Tests for Stage 46 — `GET /admin/ops-summary` + service refactor.
Two layers:
- svc.ops_summary() — the pure method now shared by CLI + HTTP. The
Stage 44 CLI tests already exercise it via the CLI path; here we
test it directly so a future CLI-removal wouldn't lose coverage.
- GET /admin/ops-summary — admin-only HTTP route, same shape.
"""
import json
import os
from datetime import datetime, timedelta, timezone
import pytest
from infra import OrgStateService
# --- service-level direct tests --------------------------------------
@pytest.fixture
def svc():
s = OrgStateService(":memory:")
yield s
s.close()
def test_ops_summary_empty_platform(svc):
"""A fresh DB returns zeros for every count plus the schema/db
metadata. Total invariant: tenants.total == healthy + unhealthy."""
out = svc.ops_summary()
assert out["tenants"]["total"] == 0
assert out["tenants"]["healthy"] == 0
assert out["tenants"]["unhealthy"] == 0
assert out["schedules"]["total"] == 0
assert out["runs"]["total_all_time"] == 0
assert out["runs"]["last_24h"] == 0
assert out["calibrations"] == {"total": 0, "stale": 0}
assert out["locks_held"] == 0
assert out["audit_log_entries"] == 0
assert out["thresholds"] == {"max_age_days": 90.0, "max_gap_hours": 48.0}
def test_ops_summary_thresholds_propagate(svc):
"""Custom thresholds change the stale count without changing
anything else."""
svc.register_tenant("acme", "ACME")
now = datetime.now(timezone.utc)
svc.db.execute(
"""INSERT INTO calibrations
(tenant_id, entity_type, vertical, calibration_json,
sample_size, derived_at)
VALUES ('acme', 'warehouse', 'logistics', '{}', 42, ?)""",
((now - timedelta(days=200)).isoformat(),),
)
svc.db.execute(
"""INSERT INTO runs
(run_id, tenant_id, entity_type, vertical, status,
n_states, n_issues, n_decisions, summary_json,
started_at, finished_at)
VALUES ('r1', 'acme', 'warehouse', 'logistics', 'completed',
0, 0, 0, '{}', ?, ?)""",
((now - timedelta(hours=5)).isoformat(),
(now - timedelta(hours=5)).isoformat()),
)
tight = svc.ops_summary()
loose = svc.ops_summary(max_age_days=365)
assert tight["calibrations"]["stale"] == 1
assert loose["calibrations"]["stale"] == 0
assert loose["thresholds"]["max_age_days"] == 365
def test_ops_summary_tenant_total_invariant(svc):
"""tenants.total == tenants.healthy + tenants.unhealthy, always."""
for tid in ("a", "b", "c"):
svc.register_tenant(tid, tid)
now = datetime.now(timezone.utc)
svc.db.execute(
"""INSERT INTO calibrations
(tenant_id, entity_type, vertical, calibration_json,
sample_size, derived_at)
VALUES ('a', 'warehouse', 'logistics', '{}', 42, ?)""",
((now - timedelta(days=200)).isoformat(),),
)
out = svc.ops_summary()
assert out["tenants"]["total"] == \
out["tenants"]["healthy"] + out["tenants"]["unhealthy"]
# --- HTTP route smoke -------------------------------------------------
def _client(dbfile=":memory:"):
pytest.importorskip("fastapi")
pytest.importorskip("httpx")
from fastapi.testclient import TestClient
from infra.api import create_app
return TestClient(create_app(dbfile))
def test_admin_ops_summary_returns_200_with_admin_key(tmp_path):
dbfile = str(tmp_path / "admin_ops.sqlite3")
boot = OrgStateService(dbfile)
try:
boot.register_tenant("acme", "ACME")
admin = boot.create_admin_key()
finally:
boot.close()
client = _client(dbfile)
r = client.get("/admin/ops-summary",
headers={"Authorization": f"Bearer {admin.raw}"})
assert r.status_code == 200
body = r.json()
# all the same top-level keys the CLI emits
assert set(body) >= {"schema_version", "db_backend", "tenants",
"schedules", "runs", "calibrations",
"locks_held", "audit_log_entries",
"thresholds"}
assert body["tenants"]["total"] == 1
def test_admin_ops_summary_401_without_key(tmp_path):
"""Once an admin key is configured, the route locks. Same pattern
as the other /admin/* routes."""
dbfile = str(tmp_path / "noauth.sqlite3")
OrgStateService(dbfile).close()
os.environ["ORGSTATE_ADMIN_KEY"] = "admin_secret"
try:
client = _client(dbfile)
r = client.get("/admin/ops-summary")
assert r.status_code == 401
finally:
os.environ.pop("ORGSTATE_ADMIN_KEY", None)
def test_admin_ops_summary_with_tenant_key_is_forbidden(tmp_path):
"""A tenant key — even role=admin on its own tenant — must NOT
be able to call this route. ops-summary enumerates the platform."""
dbfile = str(tmp_path / "tenant_key.sqlite3")
boot = OrgStateService(dbfile)
try:
boot.register_tenant("acme", "ACME")
boot.create_admin_key() # configures admin auth
tk = boot.create_api_key("acme", role="admin")
finally:
boot.close()
client = _client(dbfile)
r = client.get("/admin/ops-summary",
headers={"Authorization": f"Bearer {tk.raw}"})
assert r.status_code in (401, 403)
def test_admin_ops_summary_query_params_propagate(tmp_path):
dbfile = str(tmp_path / "qp.sqlite3")
boot = OrgStateService(dbfile)
try:
boot.register_tenant("acme", "ACME")
now = datetime.now(timezone.utc)
boot.db.execute(
"""INSERT INTO calibrations
(tenant_id, entity_type, vertical, calibration_json,
sample_size, derived_at)
VALUES ('acme', 'warehouse', 'logistics', '{}', 42, ?)""",
((now - timedelta(days=200)).isoformat(),),
)
admin = boot.create_admin_key()
finally:
boot.close()
client = _client(dbfile)
# default 90d: stale
r = client.get("/admin/ops-summary",
headers={"Authorization": f"Bearer {admin.raw}"})
assert r.json()["calibrations"]["stale"] == 1
# loose threshold: not stale
r = client.get("/admin/ops-summary?max_age_days=365",
headers={"Authorization": f"Bearer {admin.raw}"})
assert r.json()["calibrations"]["stale"] == 0
assert r.json()["thresholds"]["max_age_days"] == 365
# --- CLI/HTTP parity --------------------------------------------------
def test_cli_and_http_emit_identical_payloads(tmp_path):
"""The whole point of the Stage 46 refactor: same numbers on
both surfaces, always. If they ever diverge, this test fires."""
from infra.cli import main
dbfile = str(tmp_path / "parity.sqlite3")
main(["--db", dbfile, "tenant", "register", "acme", "ACME"])
boot = OrgStateService(dbfile)
try:
admin = boot.create_admin_key()
finally:
boot.close()
# invoke CLI; capture stdout
import contextlib
import io
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
main(["--db", dbfile, "ops-summary"])
# the CLI tail (the LAST JSON object on stdout) is the payload —
# earlier output is the tenant-register row from above
chunks = buf.getvalue().strip().split("\n}\n")
cli_payload = json.loads(chunks[-1] if chunks[-1].startswith("{")
else chunks[-1] + "\n}")
client = _client(dbfile)
http_payload = client.get(
"/admin/ops-summary",
headers={"Authorization": f"Bearer {admin.raw}"},
).json()
# the dynamic fields (audit count goes up between calls; runs.last_24h
# uses wall-clock now) make exact-equality flaky, so compare the
# stable structural keys
for k in ("tenants", "schedules", "calibrations", "locks_held",
"thresholds", "schema_version", "db_backend"):
assert cli_payload[k] == http_payload[k], (
f"CLI and HTTP disagree on {k!r}: "
f"{cli_payload[k]!r} vs {http_payload[k]!r}"
)