POST /maintenance/quality-report {user}:
- runs the tenant-scoped duplicate scan (cosine >= threshold, default 0.9)
- flags stale pages: not updated in stale_days AND <= max_search_hits
SearchQuery FOUND hits from the tenant's graph data
- lists pages missing tags/description (report subtree exempt)
- folds in the latest integrity-check results (Redis cache from
/maintenance/integrity-check, or run inline when absent)
- writes the dated report to users/{user}/system/quality-reports/YYYY-MM-DD
via the existing wiki write path; same-day reruns update the same page
(page id remembered in Redis because the Wiki.js listing lags creation)
- response returns the full markdown report + page path + counts +
duration_ms
Also fixes WikiJSClient.update_page: Wiki.js 2.x requires tags on the
update mutation (server maps over it unconditionally); calls without tags
failed with "Cannot read properties of undefined (reading 'map')" -
which also silently broke the consolidation page-update path. Current
tags are now preserved when the caller supplies none.
Verified live end-to-end on the local dev server as llm_tester
(tests/test_quality_report_live.py, integration-marked): probe page
flagged for missing metadata, report page written and fetched back,
same-day rerun updates in place, teardown leaves zero llm_tester pages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QbFZyDvYksazX6nYQYZ67L
162 lines
5.8 KiB
Python
162 lines
5.8 KiB
Python
"""
|
|
Live end-to-end test for the weekly quality report (integration, guard-gated).
|
|
|
|
Runs against the LOCAL wakeup server (./wakeup.sh, port 8778 — never the
|
|
production container on 8089) with the shared backing services, entirely
|
|
under the reserved llm_tester tenant namespace:
|
|
|
|
1. Create + ingest a wiki page as ``llm_tester`` (deliberately without tags
|
|
so the report has something to flag).
|
|
2. POST /maintenance/quality-report {user: llm_tester}: asserts the report
|
|
is generated, mentions the created page in the missing-metadata section,
|
|
folds in integrity results, and writes the dated report page under
|
|
users/llm_tester/system/quality-reports/YYYY-MM-DD.
|
|
3. Fetches the written report page back from the wiki.
|
|
4. Teardown deletes the created pages; the session-scoped teardown in
|
|
conftest purges every remaining llm_tester artifact.
|
|
|
|
Run with:
|
|
RUN_INTEGRATION_TESTS=1 TEST_HOST=<shared-host> \\
|
|
LIBRARY_DESK_URL=http://localhost:8778 \\
|
|
.venv/bin/python -m pytest tests/test_quality_report_live.py -v
|
|
"""
|
|
|
|
import time
|
|
import uuid
|
|
from datetime import datetime, timezone
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from tests.conftest import (
|
|
LIBRARY_DESK_URL,
|
|
PRODUCTION_TENANT,
|
|
TEST_TENANT,
|
|
assert_safe_test_tenant,
|
|
)
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def api():
|
|
"""HTTP client for the local dev server, with bearer auth."""
|
|
from src.config import get_settings
|
|
|
|
assert ":8089" not in LIBRARY_DESK_URL, (
|
|
"Refusing to run the live quality-report test against the "
|
|
"production container (port 8089); point LIBRARY_DESK_URL at ./wakeup.sh"
|
|
)
|
|
settings = get_settings()
|
|
client = httpx.Client(
|
|
base_url=LIBRARY_DESK_URL,
|
|
headers={"Authorization": f"Bearer {settings.library_api_key}"},
|
|
timeout=httpx.Timeout(300.0, connect=10.0),
|
|
)
|
|
yield client
|
|
client.close()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def ingested_page(api):
|
|
"""Create + ingest a metadata-poor wiki page as the test tenant."""
|
|
assert_safe_test_tenant(TEST_TENANT)
|
|
|
|
slug = f"quality-probe-{uuid.uuid4().hex[:8]}"
|
|
create_resp = api.post(
|
|
"/wiki/pages",
|
|
json={
|
|
"title": f"Quality Probe {slug}",
|
|
"path": f"/quality-tests/{slug}",
|
|
"content": (
|
|
"# Quality Probe\n\n"
|
|
"Ephemeral page used to verify the weekly quality report. "
|
|
"It intentionally has no tags so the report flags it."
|
|
),
|
|
"description": "",
|
|
"tags": [],
|
|
"user": TEST_TENANT,
|
|
},
|
|
)
|
|
assert create_resp.status_code == 201, create_resp.text
|
|
page = create_resp.json()
|
|
page_id = page["id"]
|
|
assert page["path"].lstrip("/").startswith(f"users/{TEST_TENANT}")
|
|
|
|
ingest_resp = api.post(
|
|
"/ingest/page",
|
|
json={"page_id": page_id, "user": TEST_TENANT, "force_refresh": True},
|
|
)
|
|
assert ingest_resp.status_code == 200, ingest_resp.text
|
|
|
|
# Wiki.js updates its page-listing index asynchronously after creation;
|
|
# the quality report relies on that listing, so wait until the new page
|
|
# is visible (up to ~30s) before running the report.
|
|
deadline = time.time() + 30
|
|
while time.time() < deadline:
|
|
listing = api.get("/wiki/pages", params={"user": TEST_TENANT})
|
|
assert listing.status_code == 200, listing.text
|
|
if any(p["id"] == page_id for p in listing.json().get("pages", [])):
|
|
break
|
|
time.sleep(2)
|
|
else:
|
|
pytest.fail(f"Page {page_id} never appeared in the Wiki.js listing")
|
|
|
|
yield {"page_id": page_id, "path": page["path"]}
|
|
|
|
delete_resp = api.delete(f"/wiki/pages/{page_id}", params={"user": TEST_TENANT})
|
|
assert delete_resp.status_code == 200, delete_resp.text
|
|
|
|
|
|
class TestLiveQualityReport:
|
|
def test_quality_report_end_to_end(self, api, ingested_page):
|
|
assert_safe_test_tenant(TEST_TENANT)
|
|
|
|
resp = api.post(
|
|
"/maintenance/quality-report",
|
|
json={"user": TEST_TENANT, "stale_days": 30},
|
|
)
|
|
assert resp.status_code == 200, resp.text
|
|
data = resp.json()
|
|
|
|
assert data["success"] is True
|
|
assert data["user"] == TEST_TENANT
|
|
assert data["duration_ms"] > 0
|
|
|
|
# The dated report page was written inside the tenant's namespace
|
|
today = datetime.now(timezone.utc).date().isoformat()
|
|
expected_path = f"users/{TEST_TENANT}/system/quality-reports/{today}"
|
|
assert data["page_path"] == expected_path
|
|
assert data["page_id"]
|
|
assert PRODUCTION_TENANT not in data["page_path"]
|
|
|
|
# The metadata-poor probe page is flagged
|
|
flagged_ids = {p["page_id"] for p in data["pages_missing_metadata"]}
|
|
assert ingested_page["page_id"] in flagged_ids
|
|
|
|
# Integrity results are folded in (cached or inline)
|
|
assert data["integrity"] is not None
|
|
assert data["integrity"]["source"] in ("cached", "inline")
|
|
assert "counts" in data["integrity"]
|
|
|
|
# Report content includes the summary + the probe page path
|
|
assert "## Summary" in data["report"]
|
|
assert ingested_page["path"].lstrip("/") in data["report"]
|
|
|
|
# The written page is retrievable from the wiki via the API
|
|
page_resp = api.get(
|
|
f"/wiki/pages/{data['page_id']}", params={"user": TEST_TENANT}
|
|
)
|
|
assert page_resp.status_code == 200, page_resp.text
|
|
page = page_resp.json()
|
|
assert page["path"].lstrip("/") == expected_path
|
|
assert "## Summary" in page["content"]
|
|
|
|
# Same-day rerun must update the same page, not create a duplicate
|
|
rerun = api.post(
|
|
"/maintenance/quality-report",
|
|
json={"user": TEST_TENANT, "stale_days": 30},
|
|
)
|
|
assert rerun.status_code == 200, rerun.text
|
|
assert rerun.json()["page_id"] == data["page_id"]
|