Task 20: reusable pytest fixtures for the clean-bootstrap test suite. Tasks 21 and 22 (reader smoke matrix + init smoke matrix) consume them. - fastapi_test_server boots a real uvicorn subprocess against a tmp DATA_DIR, pre-seeded with admin@example.com (Admin group), analyst@example.com (Everyone group), and three tables (one per query_mode: local / materialized / remote). - web_session: cookie-authenticated httpx.Client for the admin user. - test_pat: minted JWT for the analyst with table grants on local + materialized. - test_pat_no_grants: same shape, zero resource_grants. - zero_grants_workspace: subprocess invocation of `agnes init` against the no-grants PAT; returns the bootstrapped workspace path. - NONEXISTENT_TABLE: module-level sentinel for the upcoming reader matrix. Subprocess uvicorn (mirrors tests/test_e2e_corporate_memory.py) instead of in-thread so DATA_DIR + module-level singletons in src.db don't bleed across tests. agnes CLI invoked via `python -m cli.main` instead of the .venv/bin/agnes shim, which depends on .pth file visibility that iCloud Drive intermittently re-hides on macOS.
99 lines
3.7 KiB
Python
99 lines
3.7 KiB
Python
"""Smoke tests for the clean-bootstrap fixtures.
|
|
|
|
Verifies the fixtures defined in `tests/fixtures/analyst_bootstrap.py`
|
|
actually boot a FastAPI server, authenticate sessions, mint usable PATs,
|
|
and run `agnes init` end-to-end. Tasks 21 and 22 layer their reader/init
|
|
matrices on top of these primitives.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
|
|
|
|
def test_server_boots(fastapi_test_server):
|
|
"""The subprocess uvicorn answers /api/health with 200."""
|
|
resp = httpx.get(f"{fastapi_test_server.url}/api/health")
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
|
|
def test_web_session_authenticates(web_session, fastapi_test_server):
|
|
"""Admin cookie session can hit an admin-only endpoint.
|
|
|
|
GET /api/users requires `require_admin`. A 200 here proves the
|
|
session cookie carries through; a 401/403 would mean the form-login
|
|
fixture is broken.
|
|
"""
|
|
resp = web_session.get(f"{fastapi_test_server.url}/api/users")
|
|
assert resp.status_code == 200, (
|
|
f"expected 200, got {resp.status_code}: {resp.text[:300]}"
|
|
)
|
|
payload = resp.json()
|
|
assert isinstance(payload, list)
|
|
emails = {u.get("email") for u in payload}
|
|
# Both seeded users appear in the admin list.
|
|
assert "admin@example.com" in emails
|
|
assert "analyst@example.com" in emails
|
|
|
|
|
|
def test_test_pat_minted(test_pat):
|
|
"""test_pat is a non-empty JWT-looking string."""
|
|
assert isinstance(test_pat, str)
|
|
assert len(test_pat) > 20
|
|
# JWT (3 dot-separated base64 segments) — we issue a `typ=pat` JWT.
|
|
assert test_pat.count(".") == 2, "PAT does not look like a JWT"
|
|
|
|
|
|
def test_test_pat_no_grants_minted(test_pat_no_grants):
|
|
"""test_pat_no_grants also returns a usable JWT string."""
|
|
assert isinstance(test_pat_no_grants, str)
|
|
assert len(test_pat_no_grants) > 20
|
|
assert test_pat_no_grants.count(".") == 2
|
|
|
|
|
|
def test_test_pat_authenticates_against_server(fastapi_test_server, test_pat):
|
|
"""The minted PAT successfully authorizes a /api/catalog/tables call.
|
|
|
|
/api/catalog/tables is the same endpoint `agnes init` step 2 hits to
|
|
verify the token, so this is the exact contract the bootstrap path
|
|
needs.
|
|
"""
|
|
resp = httpx.get(
|
|
f"{fastapi_test_server.url}/api/catalog/tables",
|
|
headers={"Authorization": f"Bearer {test_pat}"},
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
|
|
|
|
def test_zero_grants_workspace_minimal(zero_grants_workspace):
|
|
"""`agnes init` with a no-grants PAT produces a minimal workspace.
|
|
|
|
Expected files (always written):
|
|
- CLAUDE.md (rendered from /api/welcome)
|
|
- AGNES_WORKSPACE.md (client-side template)
|
|
- .claude/settings.json (model + permissions seed)
|
|
- user/duckdb/analytics.duckdb (load-bearing artifact for downstream
|
|
readers, even with zero parquets)
|
|
|
|
Expected absences (no grants → empty manifest):
|
|
- server/parquet/ — lazy mkdir, only created when a parquet is
|
|
written (none with zero grants).
|
|
- .claude/rules/ — lazy mkdir, only created when the memory bundle
|
|
has at least one mandatory item or non-empty approved list.
|
|
"""
|
|
ws = Path(zero_grants_workspace)
|
|
assert (ws / "CLAUDE.md").exists(), "CLAUDE.md missing"
|
|
assert (ws / "AGNES_WORKSPACE.md").exists(), "AGNES_WORKSPACE.md missing"
|
|
assert (ws / ".claude" / "settings.json").exists(), "settings.json missing"
|
|
assert (ws / "user" / "duckdb" / "analytics.duckdb").exists(), (
|
|
"analytics.duckdb missing — downstream `agnes query` won't work"
|
|
)
|
|
|
|
assert not (ws / "server" / "parquet").exists(), (
|
|
"zero grants should produce no parquets"
|
|
)
|
|
assert not (ws / ".claude" / "rules").exists(), (
|
|
"zero rules should leave the rules directory absent"
|
|
)
|