Files
pablan/backend/tests/test_template_catalog.py
T
ProfessorNovaandClaude Opus 5 97dbff309c 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
2026-09-04 08:36:17 +02:00

129 lines
4.6 KiB
Python

"""The catalog must never write over a customer's templates.
These tests exist because the opposite behaviour shipped first: the catalog
used to be re-imported on every start, which would silently discard an
admin's edits the next time we improved a shipped blueprint.
"""
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import Template
from app.template_catalog import (
STARTER_TEMPLATE_IDS,
catalog_for_locale,
load_catalog,
seed_starter_templates,
)
from app.template_import import parse_template, upsert_template
async def test_starter_set_seeds_only_an_empty_table(db: AsyncSession) -> None:
seeded = await seed_starter_templates(db)
assert seeded == len(STARTER_TEMPLATE_IDS)
ids = set((await db.execute(select(Template.config["id"].astext))).scalars().all())
assert ids == set(STARTER_TEMPLATE_IDS)
# A second start adds nothing — the admin's curation is the truth.
assert await seed_starter_templates(db) == 0
count = (await db.execute(select(func.count(Template.id)))).scalar_one()
assert count == len(STARTER_TEMPLATE_IDS)
async def test_seeding_never_overwrites_an_edited_template(db: AsyncSession) -> None:
"""The regression this module is named for: an admin edits a starter
template, the server restarts, and the edit survives."""
await seed_starter_templates(db)
row = (
await db.execute(
select(Template).where(Template.config["id"].astext == "prozess")
)
).scalar_one()
row.name = "Unser Ablauf"
row.config = {**row.config, "persona": "Komplett umgeschrieben."}
await db.commit()
await seed_starter_templates(db)
await db.refresh(row)
assert row.name == "Unser Ablauf"
assert row.config["persona"] == "Komplett umgeschrieben."
async def test_deleting_a_starter_template_keeps_it_deleted(db: AsyncSession) -> None:
"""Deleting all but one must not resurrect the rest on restart — the
table is non-empty, so seeding stays out."""
await seed_starter_templates(db)
rows = (await db.execute(select(Template))).scalars().all()
for row in rows[1:]:
await db.delete(row)
await db.commit()
await seed_starter_templates(db)
count = (await db.execute(select(func.count(Template.id)))).scalar_one()
assert count == 1
async def test_an_empty_table_after_deleting_everything_reseeds(
db: AsyncSession,
) -> None:
"""The flip side, and the honest consequence of the empty-table rule: an
admin who removes every template gets the starter set back on restart
rather than an instance nobody can capture with."""
await seed_starter_templates(db)
for row in (await db.execute(select(Template))).scalars().all():
await db.delete(row)
await db.commit()
assert await seed_starter_templates(db) == len(STARTER_TEMPLATE_IDS)
async def test_importing_a_customer_template_is_untouched_by_seeding(
db: AsyncSession,
) -> None:
own = parse_template(
"\n".join(
[
"id: unser-eigenes",
'name: "Unser eigenes"',
'version: "1.0"',
"kind: authoring",
"persona: |",
" Du bist ein Fachredakteur.",
'title_template: "X: {{user.name}} ({{date}})"',
"skeleton: |",
" ## Thema",
"sections:",
' - heading: "Thema"',
' hint: "Worum es geht."',
]
)
)
await upsert_template(db, own)
await db.commit()
# The table is not empty, so nothing is seeded over it.
assert await seed_starter_templates(db) == 0
count = (await db.execute(select(func.count(Template.id)))).scalar_one()
assert count == 1
def test_every_shipped_blueprint_parses_and_declares_its_language() -> None:
"""A broken blueprint is skipped at load time, so a silent typo would
quietly shrink the catalog instead of failing loudly."""
entries = load_catalog()
assert len(entries) >= len(STARTER_TEMPLATE_IDS)
assert all(entry.locale in ("de", "en") for entry in entries)
assert all(entry.description for entry in entries)
# A blueprint may have no sections at all (`notiz` opens an empty
# document on purpose), but one that HAS a skeleton must hint at it.
assert all(entry.sections >= 1 for entry in entries if entry.id != "notiz")
def test_catalog_collapses_language_variants_to_one_entry_per_id() -> None:
per_locale = load_catalog()
collapsed = catalog_for_locale("de")
assert len(collapsed) == len({entry.id for entry in per_locale})
assert all(entry.locale == "de" for entry in collapsed)