import pytest from tests.conftest import run_job_inline async def _set_mode(c, slug, mode): r = await c.patch(f"/api/v1/projects/{slug}", json={"policy_mode": mode}, headers=c.auth_headers); assert r.status_code == 200, r.text async def test_policy_modes_and_provenance(app_client, pool, mock_api): c = app_client; h = c.auth_headers await c.post("/api/v1/projects", json={"slug": "pol", "name": "pol", "policy_mode": "discovery"}, headers=h) # discovery: unregistered URL allowed, recorded provisional + debt, intent/outcome audit, hash + snapshot r = await c.post("/api/v1/execute", json={"project_id": "pol", "url": mock_api["base"] + "/items?page=1", "purpose": "explore"}, headers=h); assert r.status_code == 200, r.text ex = r.json(); assert ex["state"] == "succeeded" and ex["provisional"] and ex["response_hash"] and ex["policy_decision"]["decision"] == "allow" d = (await c.get(f"/api/v1/executions/{ex['id']}", headers=h)).json() assert [a["action"] for a in d["audit"]] == ["execution.intent", "execution.outcome"] debt = (await c.get("/api/v1/debt", headers=h)).json()["debt"] assert any(x["kind"] == "undocumented_endpoint" and x["subject_id"] == ex["id"] for x in debt) # development: unregistered denied (blocked execution persisted with decision) await _set_mode(c, "pol", "development") r = await c.post("/api/v1/execute", json={"project_id": "pol", "url": mock_api["base"] + "/items"}, headers=h); assert r.status_code == 403 blocked = r.json()["error"]["execution"]; assert blocked["state"] == "blocked" and blocked["policy_decision"]["decision"] == "deny" # register the mock API via discovery → development allowed through endpoint res = await run_job_inline(pool, "discover", {"project_id": "pol", "openapi_url": mock_api["base"] + "/openapi.json", "name": "Mock Data API", "provider_slug": "mock", "license": "CC0-1.0", "created_by": "test"}) api = (await c.get(f"/api/v1/apis/{res['api_id']}", headers=h)).json(); ep = next(e for e in api["endpoints"] if e["operation_id"] == "listItems") r = await c.post("/api/v1/execute", json={"project_id": "pol", "api_id": res["api_id"], "endpoint_id": ep["id"], "query": {"page": 2}, "idempotency_key": "idem-1"}, headers=h); assert r.status_code == 200, r.text first = r.json(); assert first["state"] == "succeeded" and not first["provisional"] r2 = await c.post("/api/v1/execute", json={"project_id": "pol", "api_id": res["api_id"], "endpoint_id": ep["id"], "query": {"page": 2}, "idempotency_key": "idem-1"}, headers=h) assert r2.json()["id"] == first["id"] # idempotent replay, no second call # research: requires tested AKP → deny before verification await _set_mode(c, "pol", "research") r = await c.post("/api/v1/execute", json={"project_id": "pol", "package_id": res["package_id"], "operation_id": "listItems"}, headers=h); assert r.status_code == 403 and "needs tested" in r.json()["error"]["message"] vres = await run_job_inline(pool, "verify", {"package_id": res["package_id"], "live": True, "created_by": "test"}) assert vres["promoted_to"] == "tested", vres r = await c.post("/api/v1/execute", json={"project_id": "pol", "package_id": res["package_id"], "operation_id": "listItems", "query": {"page": 1}}, headers=h); assert r.status_code == 200, r.text # production: needs approved akp pinned on the project await _set_mode(c, "pol", "production") r = await c.post("/api/v1/execute", json={"project_id": "pol", "package_id": res["package_id"], "operation_id": "listItems"}, headers=h); assert r.status_code == 403 r = await c.patch("/api/v1/projects/pol", json={"approved_akps": [res["package_id"]]}, headers=h); assert r.status_code == 200 r = await c.post("/api/v1/execute", json={"project_id": "pol", "package_id": res["package_id"], "operation_id": "listItems"}, headers=h); assert r.status_code == 403 # lifecycle tested, needs registered r = await c.post(f"/api/v1/akps/{res['package_id']}/lifecycle", json={"to": "registered"}, headers=h); assert r.status_code == 200, r.text r = await c.post("/api/v1/execute", json={"project_id": "pol", "package_id": res["package_id"], "operation_id": "listItems", "query": {"page": 1}}, headers=h); assert r.status_code == 200, r.text assert r.json()["policy_decision"]["reasons"] == ["production: ok"] # response size limit + egress grant cleanup await _set_mode(c, "pol", "discovery") from akos.settings import settings big = await c.post("/api/v1/execute", json={"project_id": "pol", "url": mock_api["base"] + "/big"}, headers=h) assert big.status_code == 200 and big.json()["state"] == "succeeded" async with pool.acquire() as conn: assert await conn.fetchval("SELECT count(*) FROM egress_grants WHERE expires_at > now() AND execution_id IS NOT NULL") == 0 assert (await conn.fetchval("SELECT count(*) FROM executions WHERE state='intent'")) == 0 async def test_lifecycle_transition_refused_without_evidence(app_client, pool, mock_api): c = app_client; h = c.auth_headers r = await c.post("/api/v1/providers", json={"name": "Manual Prov"}, headers=h); pid = r.json()["id"] r = await c.post("/api/v1/apis", json={"provider_id": pid, "name": "Manual API", "base_url": mock_api["base"]}, headers=h); aid = r.json()["id"] r = await c.post(f"/api/v1/apis/{aid}/lifecycle", json={"to": "tested"}, headers=h); assert r.status_code == 409 r = await c.post(f"/api/v1/apis/{aid}/lifecycle", json={"to": "inspected"}, headers=h); assert r.status_code == 200