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:
ProfessorNova
2026-09-04 09:21:37 +02:00
co-authored by Claude Opus 5
parent 68d3a43191
commit 784b76baf7
346 changed files with 43430 additions and 0 deletions
+152
View File
@@ -0,0 +1,152 @@
import pytest
from sqlalchemy import delete, func, select
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
from app.ingestion.handlers import INDEX_DOCUMENT, REINDEX_ALL
from app.ingestion.queue import enqueue, process_one
from app.models import (
Chunk,
Document,
DocumentStatus,
DocumentVisibility,
Job,
JobStatus,
User,
)
from app.rag.indexing import reindex_document
pytestmark = pytest.mark.usefixtures("fake_embed")
CONTENT = (
"## Wartung\n\nWöchentlich schmieren mit GX-220.\n\n"
"## Sicherheit\n\nLichtvorhang niemals überbrücken.\n"
)
@pytest.fixture
def session_factory(db_engine: AsyncEngine) -> async_sessionmaker[AsyncSession]:
return async_sessionmaker(db_engine, expire_on_commit=False)
async def _doc(
db: AsyncSession,
seeded_user: User,
*,
title: str = "Handbuch",
status: DocumentStatus = DocumentStatus.published,
) -> Document:
document = Document(
title=title,
status=status,
visibility=DocumentVisibility.department,
content_md=CONTENT,
author_id=seeded_user.id,
department_id=seeded_user.department_id,
meta={},
)
db.add(document)
await db.flush()
return document
async def _chunk_count(db: AsyncSession, document_id) -> int:
return (
await db.execute(
select(func.count(Chunk.id)).where(Chunk.document_id == document_id)
)
).scalar_one()
async def test_reindex_creates_chunks_with_meta(
db: AsyncSession, seeded_user: User
) -> None:
document = await _doc(db, seeded_user)
count = await reindex_document(db, document)
await db.commit()
assert count == 2
chunks = (
(await db.execute(select(Chunk).where(Chunk.document_id == document.id)))
.scalars()
.all()
)
assert {chunk.meta["heading_path"] for chunk in chunks} == {
"Handbuch Wartung",
"Handbuch Sicherheit",
}
for chunk in chunks:
assert chunk.meta["visibility"] == "department"
assert chunk.meta["department_id"] == str(seeded_user.department_id)
async def test_reindex_is_idempotent(db: AsyncSession, seeded_user: User) -> None:
document = await _doc(db, seeded_user)
await reindex_document(db, document)
await db.commit()
first_ids = {
chunk.id
for chunk in (
await db.execute(select(Chunk).where(Chunk.document_id == document.id))
).scalars()
}
await reindex_document(db, document)
await db.commit()
chunks = (
(await db.execute(select(Chunk).where(Chunk.document_id == document.id)))
.scalars()
.all()
)
assert len(chunks) == 2
assert first_ids.isdisjoint({chunk.id for chunk in chunks})
async def test_index_job_indexes_published_and_cleans_unpublished(
db: AsyncSession,
seeded_user: User,
session_factory: async_sessionmaker[AsyncSession],
) -> None:
document = await _doc(db, seeded_user)
await enqueue(db, INDEX_DOCUMENT, {"document_id": str(document.id)})
await db.commit()
assert await process_one(session_factory) is True
assert await _chunk_count(db, document.id) == 2
document.status = DocumentStatus.archived
await enqueue(db, INDEX_DOCUMENT, {"document_id": str(document.id)})
await db.commit()
assert await process_one(session_factory) is True
assert await _chunk_count(db, document.id) == 0
async def test_reindex_all_fans_out_and_rebuilds_from_markdown(
db: AsyncSession,
seeded_user: User,
session_factory: async_sessionmaker[AsyncSession],
) -> None:
published = [await _doc(db, seeded_user, title=f"Doc {i}") for i in range(3)]
await _doc(db, seeded_user, title="Entwurf", status=DocumentStatus.draft)
# Simulate an embedding-model swap: all derivatives are gone.
await db.execute(delete(Chunk))
await enqueue(db, REINDEX_ALL)
await db.commit()
# Fan-out: the reindex_all job only enqueues per-document jobs.
assert await process_one(session_factory) is True
index_jobs = (
(
await db.execute(
select(Job).where(
Job.type == INDEX_DOCUMENT, Job.status == JobStatus.pending
)
)
)
.scalars()
.all()
)
assert len(index_jobs) == 3 # the draft is not indexed
while await process_one(session_factory):
pass
for document in published:
assert await _chunk_count(db, document.id) == 2