- app/api/claude_md.py: GET /api/welcome (analyst, auth required); GET/PUT/DELETE /api/admin/workspace-prompt-template; POST …/preview; two-pass Jinja2 validation on PUT; validation stub mirrors build_claude_md_context() shape - app/main.py: register claude_md_router - app/web/router.py: GET /admin/workspace-prompt → admin_workspace_prompt.html - app/web/templates/admin_workspace_prompt.html: CodeMirror editor + live preview + status chip + reset modal; mirrors admin_welcome.html for Agent Setup Prompt - app/web/templates/_app_header.html: add "Agent Workspace Prompt" nav item next to "Agent Setup Prompt"; extend _admin_active to cover /admin/workspace-prompt - cli/commands/analyst.py: _init_claude_workspace now accepts server_url + token; _write_claude_md fetches GET /api/welcome, writes CLAUDE.md, graceful 404/5xx; setup command adds --no-claude-md flag to opt out; default = write CLAUDE.md - tests: test_claude_md_api.py (16 tests); test_analyst_bootstrap.py updated with 4 new CLAUDE.md bootstrap tests; test_welcome_template_api.py: update stale assertion about /api/welcome being removed (endpoint restored) - tests/snapshots/openapi.json: regenerated
257 lines
8.6 KiB
Python
257 lines
8.6 KiB
Python
"""End-to-end tests for the agent-workspace-prompt API endpoints.
|
|
|
|
GET /api/welcome — analyst-facing rendered CLAUDE.md
|
|
GET /api/admin/workspace-prompt-template — admin: get template + default
|
|
PUT /api/admin/workspace-prompt-template — admin: set override
|
|
DELETE /api/admin/workspace-prompt-template — admin: reset to default
|
|
POST /api/admin/workspace-prompt-template/preview — admin: live preview
|
|
"""
|
|
|
|
|
|
def _auth(token: str) -> dict[str, str]:
|
|
return {"Authorization": f"Bearer {token}"}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /api/welcome — analyst-facing rendered CLAUDE.md
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_get_welcome_requires_auth(seeded_app):
|
|
"""Unauthenticated GET /api/welcome must return 401 or 422."""
|
|
c = seeded_app["client"]
|
|
resp = c.get("/api/welcome", params={"server_url": "https://example.com"})
|
|
assert resp.status_code in (401, 422)
|
|
|
|
|
|
def test_get_welcome_returns_rendered_markdown(seeded_app):
|
|
c = seeded_app["client"]
|
|
analyst = _auth(seeded_app["analyst_token"])
|
|
|
|
resp = c.get(
|
|
"/api/welcome",
|
|
params={"server_url": "https://example.com"},
|
|
headers=analyst,
|
|
)
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert "content" in body
|
|
assert isinstance(body["content"], str)
|
|
assert body["content"].strip() != ""
|
|
|
|
|
|
def test_get_welcome_uses_override_when_set(seeded_app):
|
|
c = seeded_app["client"]
|
|
admin = _auth(seeded_app["admin_token"])
|
|
analyst = _auth(seeded_app["analyst_token"])
|
|
|
|
# Set an override
|
|
r = c.put(
|
|
"/api/admin/workspace-prompt-template",
|
|
json={"content": "# Custom CLAUDE.md for {{ user.email }}"},
|
|
headers=admin,
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
# Analyst fetch should include the override
|
|
resp = c.get(
|
|
"/api/welcome",
|
|
params={"server_url": "https://example.com"},
|
|
headers=analyst,
|
|
)
|
|
assert resp.status_code == 200
|
|
assert "Custom CLAUDE.md" in resp.json()["content"]
|
|
assert "analyst@test.com" in resp.json()["content"]
|
|
|
|
# Reset
|
|
c.delete("/api/admin/workspace-prompt-template", headers=admin)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# GET /api/admin/workspace-prompt-template — admin get
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_admin_get_template_initially_null(seeded_app):
|
|
c = seeded_app["client"]
|
|
admin = _auth(seeded_app["admin_token"])
|
|
|
|
r = c.get("/api/admin/workspace-prompt-template", headers=admin)
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["content"] is None
|
|
assert "default" in body
|
|
assert body["default"] # non-empty default
|
|
|
|
|
|
def test_admin_get_template_default_contains_instance_name(seeded_app):
|
|
c = seeded_app["client"]
|
|
admin = _auth(seeded_app["admin_token"])
|
|
|
|
r = c.get("/api/admin/workspace-prompt-template", headers=admin)
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
# Default template renders the instance name
|
|
assert body["default"] != ""
|
|
|
|
|
|
def test_non_admin_cannot_get_template(seeded_app):
|
|
c = seeded_app["client"]
|
|
analyst = _auth(seeded_app["analyst_token"])
|
|
r = c.get("/api/admin/workspace-prompt-template", headers=analyst)
|
|
assert r.status_code == 403
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# PUT /api/admin/workspace-prompt-template — save override
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_admin_can_set_and_reset_template(seeded_app):
|
|
c = seeded_app["client"]
|
|
admin = _auth(seeded_app["admin_token"])
|
|
|
|
# PUT override
|
|
r = c.put(
|
|
"/api/admin/workspace-prompt-template",
|
|
json={"content": "# Hello {{ user.email }}"},
|
|
headers=admin,
|
|
)
|
|
assert r.status_code == 200
|
|
|
|
# GET reflects override
|
|
r = c.get("/api/admin/workspace-prompt-template", headers=admin)
|
|
assert r.status_code == 200
|
|
assert r.json()["content"] == "# Hello {{ user.email }}"
|
|
|
|
# DELETE = reset
|
|
r = c.delete("/api/admin/workspace-prompt-template", headers=admin)
|
|
assert r.status_code == 204
|
|
r = c.get("/api/admin/workspace-prompt-template", headers=admin)
|
|
assert r.json()["content"] is None
|
|
|
|
|
|
def test_non_admin_cannot_put_template(seeded_app):
|
|
c = seeded_app["client"]
|
|
analyst = _auth(seeded_app["analyst_token"])
|
|
r = c.put(
|
|
"/api/admin/workspace-prompt-template",
|
|
json={"content": "# evil override"},
|
|
headers=analyst,
|
|
)
|
|
assert r.status_code == 403
|
|
|
|
|
|
def test_invalid_jinja2_returns_400(seeded_app):
|
|
c = seeded_app["client"]
|
|
admin = _auth(seeded_app["admin_token"])
|
|
r = c.put(
|
|
"/api/admin/workspace-prompt-template",
|
|
json={"content": "{% for x in y %}"}, # unclosed loop
|
|
headers=admin,
|
|
)
|
|
assert r.status_code == 400
|
|
assert "invalid" in r.json()["detail"].lower()
|
|
|
|
|
|
def test_put_rejects_undefined_placeholder(seeded_app):
|
|
c = seeded_app["client"]
|
|
admin = _auth(seeded_app["admin_token"])
|
|
r = c.put(
|
|
"/api/admin/workspace-prompt-template",
|
|
json={"content": "{{ no_such_variable }}"},
|
|
headers=admin,
|
|
)
|
|
assert r.status_code == 400
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# DELETE /api/admin/workspace-prompt-template
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_non_admin_cannot_delete_template(seeded_app):
|
|
c = seeded_app["client"]
|
|
analyst = _auth(seeded_app["analyst_token"])
|
|
r = c.delete("/api/admin/workspace-prompt-template", headers=analyst)
|
|
assert r.status_code == 403
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# POST /api/admin/workspace-prompt-template/preview
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_admin_preview_renders_content(seeded_app):
|
|
c = seeded_app["client"]
|
|
admin = _auth(seeded_app["admin_token"])
|
|
r = c.post(
|
|
"/api/admin/workspace-prompt-template/preview",
|
|
json={"content": "# Preview for {{ user.email }}"},
|
|
headers=admin,
|
|
)
|
|
assert r.status_code == 200
|
|
assert r.json()["content"].startswith("# Preview for admin@test.com")
|
|
|
|
|
|
def test_preview_rejects_invalid_template(seeded_app):
|
|
c = seeded_app["client"]
|
|
admin = _auth(seeded_app["admin_token"])
|
|
r = c.post(
|
|
"/api/admin/workspace-prompt-template/preview",
|
|
json={"content": "{% for x in y %}"},
|
|
headers=admin,
|
|
)
|
|
assert r.status_code == 400
|
|
|
|
|
|
def test_preview_requires_admin(seeded_app):
|
|
c = seeded_app["client"]
|
|
analyst = _auth(seeded_app["analyst_token"])
|
|
r = c.post(
|
|
"/api/admin/workspace-prompt-template/preview",
|
|
json={"content": "# Preview"},
|
|
headers=analyst,
|
|
)
|
|
assert r.status_code == 403
|
|
|
|
|
|
def test_preview_uses_live_context(seeded_app):
|
|
"""Preview should include live table data from context."""
|
|
c = seeded_app["client"]
|
|
admin = _auth(seeded_app["admin_token"])
|
|
r = c.post(
|
|
"/api/admin/workspace-prompt-template/preview",
|
|
json={"content": "tables: {{ tables | length }}, metrics: {{ metrics.count }}"},
|
|
headers=admin,
|
|
)
|
|
assert r.status_code == 200
|
|
# Content must be a rendered string (not raise), numbers may be 0 on fresh DB
|
|
assert "tables:" in r.json()["content"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Validation stub vs. build_claude_md_context shape alignment
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_validation_stub_matches_build_context_shape(seeded_app, tmp_path, monkeypatch):
|
|
"""_VALIDATION_STUB_CONTEXT top-level keys must match build_claude_md_context() output."""
|
|
from app.api.claude_md import _VALIDATION_STUB_CONTEXT
|
|
from src.db import _ensure_schema, get_system_db
|
|
import duckdb
|
|
|
|
db_path = tmp_path / "system.duckdb"
|
|
c = duckdb.connect(str(db_path))
|
|
_ensure_schema(c)
|
|
|
|
user = {
|
|
"id": "u1",
|
|
"email": "admin@test.com",
|
|
"name": "Admin",
|
|
"is_admin": True,
|
|
"groups": ["Admin"],
|
|
}
|
|
from src.claude_md import build_claude_md_context
|
|
real_ctx = build_claude_md_context(c, user=user, server_url="https://example.com")
|
|
|
|
assert set(_VALIDATION_STUB_CONTEXT.keys()) == set(real_ctx.keys()), (
|
|
f"_VALIDATION_STUB_CONTEXT top-level keys differ from build_claude_md_context output. "
|
|
f"Stub: {set(_VALIDATION_STUB_CONTEXT.keys())}, "
|
|
f"real: {set(real_ctx.keys())}"
|
|
)
|
|
c.close()
|