agnes-the-ai-analyst/tests/test_fixtures_smoke.py
ZdenekSrotyr a47c2be282 test: clean-bootstrap fixtures (fastapi_test_server, test_pat, zero_grants_workspace)
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.
2026-05-04 19:11:54 +02:00

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"
)