"""Tests for legacy-string scan in admin CLAUDE.md template endpoint. The scanner flags admin-saved CLAUDE.md overrides that still reference the pre-clean-bootstrap CLI surface (`da sync`, `da fetch`, `data/parquet/`, `da analyst setup`, `da metrics list/show`). The admin UI surfaces the hits as a yellow banner so operators know to re-author the override; the scanner itself is informational only — saves with legacy strings are still accepted. """ from __future__ import annotations import tempfile import uuid import pytest from fastapi.testclient import TestClient from app.api.claude_md import _LEGACY_STRINGS, _scan_legacy_strings # --------------------------------------------------------------------------- # Unit tests — pure-function behaviour # --------------------------------------------------------------------------- def test_scan_finds_all_known_legacy_strings(): text = """ Run `da sync` then `da fetch web_sessions --where ...`. Old workspace at data/parquet/ — see `da analyst setup`. Use `da metrics list` and `da metrics show `. """ hits = _scan_legacy_strings(text) assert "da sync" in hits assert "da fetch" in hits assert "data/parquet" in hits assert "da analyst setup" in hits assert "da metrics list" in hits assert "da metrics show" in hits def test_scan_returns_empty_for_clean_text(): text = "Use `agnes pull` to refresh, `agnes snapshot create` for ad-hoc, `server/parquet/`." assert _scan_legacy_strings(text) == [] def test_scan_returns_unique_sorted_hits(): text = "da sync da sync data/parquet/ data/parquet/foo" hits = _scan_legacy_strings(text) assert hits == sorted(set(hits)) def test_legacy_strings_constant_shape(): assert isinstance(_LEGACY_STRINGS, tuple) assert all(isinstance(s, str) for s in _LEGACY_STRINGS) assert "da sync" in _LEGACY_STRINGS assert "data/parquet" in _LEGACY_STRINGS # --------------------------------------------------------------------------- # HTTP-level tests — admin GET surfaces detected hits # # Lifts the Bearer-session pattern from tests/test_tokens_bootstrap_scope.py # (Task 1) — Task 20's shared `web_session` cookie fixture isn't built yet, # but the endpoint surface we're exercising is identical either way. # --------------------------------------------------------------------------- @pytest.fixture def fresh_db(monkeypatch): with tempfile.TemporaryDirectory() as tmp: monkeypatch.setenv("DATA_DIR", tmp) monkeypatch.setenv("TESTING", "1") monkeypatch.setenv("JWT_SECRET_KEY", "test-jwt-secret-key-minimum-32-chars!!") yield tmp @pytest.fixture def web_session(fresh_db): """TestClient authenticated as an admin user via a Bearer session JWT.""" from app.auth.jwt import create_access_token from app.main import app from src.db import close_system_db, get_system_db from src.repositories.users import UserRepository from tests.helpers.auth import grant_admin conn = get_system_db() try: uid = str(uuid.uuid4()) UserRepository(conn).create(id=uid, email="admin@example.com", name="Admin") grant_admin(conn, uid) sess_token = create_access_token(user_id=uid, email="admin@example.com") finally: conn.close() close_system_db() client = TestClient(app) client.headers.update({"Authorization": f"Bearer {sess_token}"}) return client def test_admin_get_template_returns_legacy_strings_when_override_dirty(web_session): """Setting an override containing legacy strings populates the field.""" put = web_session.put( "/api/admin/workspace-prompt-template", json={"content": "Run `da sync` and check data/parquet/."}, ) assert put.status_code == 200, put.text resp = web_session.get("/api/admin/workspace-prompt-template") assert resp.status_code == 200, resp.text body = resp.json() assert "da sync" in body["legacy_strings_detected"] assert "data/parquet" in body["legacy_strings_detected"] def test_admin_get_template_returns_empty_when_clean(web_session): put = web_session.put( "/api/admin/workspace-prompt-template", json={"content": "Use `agnes pull` and check `server/parquet/`."}, ) assert put.status_code == 200, put.text resp = web_session.get("/api/admin/workspace-prompt-template") assert resp.status_code == 200, resp.text assert resp.json()["legacy_strings_detected"] == [] # --------------------------------------------------------------------------- # HTML banner tests — admin /admin/workspace-prompt page renders a yellow # warning banner above the editor when the saved override contains stale # CLI verbs / paths. # --------------------------------------------------------------------------- def test_admin_workspace_prompt_page_renders_banner_when_legacy_present(web_session): """When the saved override contains legacy strings, the admin UI renders a yellow warning banner above the editor listing the hits.""" web_session.put( "/api/admin/workspace-prompt-template", json={"content": "Run `da sync` and check data/parquet/."}, ) resp = web_session.get("/admin/workspace-prompt") assert resp.status_code == 200 text = resp.text # Banner is rendered (id, class, or distinctive styling) assert "legacy-banner" in text or "renamed" in text.lower() or "warning" in text.lower() # Specific hits appear in the banner content assert "da sync" in text assert "data/parquet" in text def test_admin_workspace_prompt_page_no_banner_when_clean(web_session): """When the override has no legacy strings, the banner block is absent (or rendered as empty/hidden).""" web_session.put( "/api/admin/workspace-prompt-template", json={"content": "Use `agnes pull` and `server/parquet/`."}, ) resp = web_session.get("/admin/workspace-prompt") assert resp.status_code == 200 text = resp.text # The banner div should not appear (or its content should not list hits) # Pin to the legacy-banner id; the {% if %} guard means absent when clean assert "legacy-banner" not in text def test_admin_workspace_prompt_page_no_banner_when_no_override(web_session): """No saved override at all → banner absent.""" # Reset to default by deleting any existing override web_session.delete("/api/admin/workspace-prompt-template") resp = web_session.get("/admin/workspace-prompt") assert resp.status_code == 200 assert "legacy-banner" not in resp.text