Self-hosted knowledge management for SMEs: a split-screen Markdown editor whose sections an LLM refines while you write, and RAG question answering over the documents that result. FastAPI + Postgres/pgvector on the back, SvelteKit on the front, everything OpenAI-compatible and self-hostable. Squashed into a single commit; the development history stays local. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
115 lines
3.4 KiB
Python
115 lines
3.4 KiB
Python
"""Built-in help documents: imported from files, never editable in the UI."""
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.help_import import (
|
|
HelpImportError,
|
|
import_help_documents,
|
|
parse_help_document,
|
|
)
|
|
from app.models import Document, Job, User
|
|
|
|
pytestmark = pytest.mark.usefixtures("fake_embed")
|
|
|
|
HELP_FILE = """---
|
|
key: test-hilfe
|
|
title: "Testhilfe"
|
|
---
|
|
|
|
# Testhilfe
|
|
|
|
So funktioniert es.
|
|
"""
|
|
|
|
|
|
def test_parse_splits_frontmatter_from_body() -> None:
|
|
key, title, body = parse_help_document(HELP_FILE)
|
|
assert (key, title) == ("test-hilfe", "Testhilfe")
|
|
assert body.startswith("# Testhilfe")
|
|
|
|
|
|
def test_parse_rejects_a_file_without_frontmatter() -> None:
|
|
with pytest.raises(HelpImportError):
|
|
parse_help_document("# Just markdown")
|
|
|
|
|
|
async def _import(db: AsyncSession, tmp_path, monkeypatch, text: str) -> int:
|
|
(tmp_path / "hilfe.md").write_text(text)
|
|
monkeypatch.setenv("PABLAN_HELP_DIR", str(tmp_path))
|
|
from app.config import get_settings
|
|
|
|
get_settings.cache_clear()
|
|
try:
|
|
return await import_help_documents(db)
|
|
finally:
|
|
get_settings.cache_clear()
|
|
|
|
|
|
async def test_import_is_idempotent_and_reindexes_only_on_change(
|
|
db: AsyncSession, tmp_path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
assert await _import(db, tmp_path, monkeypatch, HELP_FILE) == 1
|
|
document = (
|
|
await db.execute(select(Document).where(Document.is_builtin.is_(True)))
|
|
).scalar_one()
|
|
assert document.title == "Testhilfe"
|
|
assert document.status.value == "published"
|
|
assert document.visibility.value == "public"
|
|
assert document.author_id is None
|
|
|
|
# Unchanged: no second document, no new index job.
|
|
assert await _import(db, tmp_path, monkeypatch, HELP_FILE) == 0
|
|
|
|
# Changed: content is refreshed in place and re-indexed.
|
|
assert (
|
|
await _import(
|
|
db, tmp_path, monkeypatch, HELP_FILE.replace("So funktioniert es.", "Neu.")
|
|
)
|
|
== 1
|
|
)
|
|
documents = (
|
|
(await db.execute(select(Document).where(Document.is_builtin.is_(True))))
|
|
.scalars()
|
|
.all()
|
|
)
|
|
assert len(documents) == 1
|
|
assert "Neu." in documents[0].content_md
|
|
jobs = (await db.execute(select(Job))).scalars().all()
|
|
assert len(jobs) == 2 # one per actual change
|
|
|
|
|
|
async def test_help_documents_cannot_be_edited_or_deleted(
|
|
client: AsyncClient,
|
|
db: AsyncSession,
|
|
seeded_admin: User,
|
|
tmp_path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
"""Not even an admin may edit them — the next deploy would overwrite it."""
|
|
await _import(db, tmp_path, monkeypatch, HELP_FILE)
|
|
document = (
|
|
await db.execute(select(Document).where(Document.is_builtin.is_(True)))
|
|
).scalar_one()
|
|
|
|
login = await client.post(
|
|
"/api/auth/login", json={"email": "florian@test.dev", "password": "secret123"}
|
|
)
|
|
assert login.status_code == 200
|
|
|
|
listed = (await client.get("/api/documents")).json()["items"]
|
|
entry = next(row for row in listed if row["id"] == str(document.id))
|
|
assert entry["is_builtin"] is True
|
|
assert entry["can_edit"] is False
|
|
|
|
patched = await client.patch(
|
|
f"/api/documents/{document.id}", json={"title": "Gekapert"}
|
|
)
|
|
assert patched.status_code == 409
|
|
assert patched.json()["code"] == "builtin_readonly"
|
|
|
|
deleted = await client.delete(f"/api/documents/{document.id}")
|
|
assert deleted.status_code == 409
|