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:
co-authored by
Claude Opus 5
parent
68d3a43191
commit
784b76baf7
@@ -0,0 +1,128 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user