orgstate / tests /test_infra_api_run_preview.py
Legal-i's picture
Initial OrgState deploy via Stage 150 free-tier stack
d2d1903 verified
"""
Tests for Stage 45 β€” GET /tenants/{tid}/runs/preview.
HTTP shell over the Stage 37 service method. The contract:
- tenant key (readonly) or admin can call
- cross-tenant probe β†’ 403
- vertical is inferred from the tenant's registration
- the route never writes to the runs/run_issues/run_decisions
tables β€” that's the whole point of preview
Tests verify auth, the inference, the underlying no-write
contract (smoking-gun via row counts), and the error paths.
"""
from datetime import datetime, timedelta, timezone
import pytest
pytest.importorskip("fastapi")
pytest.importorskip("httpx")
pytest.importorskip("yaml")
from fastapi.testclient import TestClient
from infra import OrgStateService
from infra.api import create_app
def _seed_calibrated_tenant(dbfile, tenant_id="acme"):
"""Register a logistics tenant + push 20 days of observations +
calibrate. Preview can then run end-to-end on stored state."""
from verticals import get_vertical_config
svc = OrgStateService(dbfile)
try:
svc.register_tenant(tenant_id, tenant_id, vertical="logistics")
cfg = get_vertical_config("logistics").entity_type("warehouse")
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
rows = []
for d in range(20):
for eid in ("wh_a", "wh_b"):
values = {m.name: 100.0 + d for m in cfg.metrics}
rows.append({
"entity_id": eid,
"day": (base + timedelta(days=d)).date().isoformat(),
"values": values,
})
svc.observations.upsert_batch(tenant_id, "warehouse", rows)
svc.calibrate_and_store(tenant_id, [
__import__("core").Observation(
entity_id=r["entity_id"], day=r["day"],
values=r["values"],
) for r in rows
], cfg, vertical="logistics")
finally:
svc.close()
def _bootstrap(tmp_path):
dbfile = str(tmp_path / "preview_http.sqlite3")
_seed_calibrated_tenant(dbfile, "acme")
_seed_calibrated_tenant(dbfile, "globex")
svc = OrgStateService(dbfile)
try:
keys = {
"acme_ro": svc.create_api_key("acme", role="readonly").raw,
"acme_op": svc.create_api_key("acme", role="operator").raw,
"globex_ro": svc.create_api_key("globex", role="readonly").raw,
"admin": svc.create_admin_key().raw,
}
finally:
svc.close()
return dbfile, keys
def _client(dbfile):
return TestClient(create_app(dbfile))
def _auth(key):
return {"Authorization": f"Bearer {key}"}
# --- happy path -------------------------------------------------------
def test_owner_can_preview_with_readonly_key(tmp_path):
"""The whole point β€” a customer SDK with the tenant readonly key
asks 'what would the next run say' and gets the summary."""
dbfile, keys = _bootstrap(tmp_path)
client = _client(dbfile)
r = client.get("/tenants/acme/runs/preview?entity_type=warehouse",
headers=_auth(keys["acme_ro"]))
assert r.status_code == 200
body = r.json()
assert body["tenant_id"] == "acme"
assert body["entity_type"] == "warehouse"
assert body["vertical"] == "logistics"
assert "preview" in body
p = body["preview"]
assert {"n_states", "n_issues", "n_decisions",
"top_issues"} <= p.keys()
def test_admin_can_preview_any_tenant(tmp_path):
dbfile, keys = _bootstrap(tmp_path)
client = _client(dbfile)
r = client.get("/tenants/globex/runs/preview?entity_type=warehouse",
headers=_auth(keys["admin"]))
assert r.status_code == 200
# --- vertical inference ----------------------------------------------
def test_vertical_inferred_from_tenant_registration(tmp_path):
"""The customer SDK doesn't pass vertical β€” it's taken from the
tenant row. Regression: if a tenant on a different vertical were
registered, preview would resolve THEIR config, not 'logistics'."""
dbfile, keys = _bootstrap(tmp_path)
# acme is logistics β†’ warehouse is a valid entity_type
client = _client(dbfile)
r = client.get("/tenants/acme/runs/preview?entity_type=warehouse",
headers=_auth(keys["acme_ro"]))
assert r.status_code == 200
assert r.json()["vertical"] == "logistics"
# --- THE central contract: no DB writes ------------------------------
def test_preview_endpoint_does_not_write_a_run_row(tmp_path):
"""The HTTP route inherits Stage 37's no-persist contract. If the
`runs` count changes after a preview call, the whole point of the
feature is gone."""
dbfile, keys = _bootstrap(tmp_path)
svc = OrgStateService(dbfile)
try:
before = svc.db.query_one("SELECT COUNT(*) AS n FROM runs")["n"]
finally:
svc.close()
client = _client(dbfile)
r = client.get("/tenants/acme/runs/preview?entity_type=warehouse",
headers=_auth(keys["acme_ro"]))
assert r.status_code == 200
svc = OrgStateService(dbfile)
try:
after = svc.db.query_one("SELECT COUNT(*) AS n FROM runs")["n"]
finally:
svc.close()
assert before == after, (
f"preview wrote a run row (before={before}, after={after}) "
"β€” the no-persist contract is broken"
)
# --- auth -------------------------------------------------------------
def test_no_key_returns_401(tmp_path):
dbfile, _ = _bootstrap(tmp_path)
client = _client(dbfile)
r = client.get("/tenants/acme/runs/preview?entity_type=warehouse")
assert r.status_code == 401
def test_cross_tenant_returns_403(tmp_path):
"""An acme key probing globex's preview β€” the standard 403."""
dbfile, keys = _bootstrap(tmp_path)
client = _client(dbfile)
r = client.get("/tenants/globex/runs/preview?entity_type=warehouse",
headers=_auth(keys["acme_ro"]))
assert r.status_code == 403
# --- error paths ------------------------------------------------------
def test_missing_entity_type_query_returns_422(tmp_path):
"""FastAPI's Query(...) (required) surfaces missing params as 422."""
dbfile, keys = _bootstrap(tmp_path)
client = _client(dbfile)
r = client.get("/tenants/acme/runs/preview",
headers=_auth(keys["acme_ro"]))
assert r.status_code == 422
def test_unknown_entity_type_returns_404(tmp_path):
"""Wrong entity_type for the vertical β†’ 404 via _resolve_config."""
dbfile, keys = _bootstrap(tmp_path)
client = _client(dbfile)
r = client.get("/tenants/acme/runs/preview?entity_type=ghost",
headers=_auth(keys["acme_ro"]))
assert r.status_code == 404
def test_unknown_tenant_returns_403_via_auth(tmp_path):
"""A ghost tenant probed by a valid key from another tenant β€”
require_tenant_or_admin's 403, not 404 (no enumeration)."""
dbfile, keys = _bootstrap(tmp_path)
client = _client(dbfile)
r = client.get("/tenants/ghost/runs/preview?entity_type=warehouse",
headers=_auth(keys["acme_ro"]))
assert r.status_code == 403