Pablan, as it stands
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
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user