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
92 lines
3.8 KiB
TypeScript
92 lines
3.8 KiB
TypeScript
import { expect, test, type Page } from '@playwright/test';
|
|
import { login as loginAs } from './helpers';
|
|
|
|
// Writing-first capture as a USER sees it: pick a documentation type, write in
|
|
// the editor, get a refined version of the section at the cursor, accept it,
|
|
// read the diff before saving, and publish. The refine step drives the real
|
|
// LLM, so it can take a few seconds.
|
|
|
|
test.describe.configure({ mode: 'serial' });
|
|
test.setTimeout(180_000);
|
|
|
|
// Template names are matched in German on purpose: a template is product
|
|
// CONTENT in the instance's own language (PABLAN_DEFAULT_LOCALE), not UI copy
|
|
// that follows the reader's language setting.
|
|
|
|
/** Draft documents this spec created, so cleanup removes only its own. */
|
|
let created: string[] = [];
|
|
|
|
async function newDraft(page: Page, templateText: string): Promise<string> {
|
|
await page.goto('/documents/new');
|
|
await page.locator('body[data-hydrated]').waitFor();
|
|
await page.getByTestId('capture-template').filter({ hasText: templateText }).first().click();
|
|
await expect(page).toHaveURL(/\/documents\/[0-9a-f-]{36}\/edit$/);
|
|
const id = page.url().split('/')[4];
|
|
created.push(id);
|
|
await page.locator('.cm-content').waitFor();
|
|
return id;
|
|
}
|
|
|
|
test.afterEach(async ({ page }) => {
|
|
for (const id of created) {
|
|
await page.request.delete(`/api/documents/${id}`);
|
|
}
|
|
created = [];
|
|
await page.request.post('/api/auth/logout');
|
|
});
|
|
|
|
test('picking a type opens the editor on the template skeleton', async ({ page }) => {
|
|
await loginAs(page, 'pablo@pablan.dev');
|
|
await newDraft(page, 'Über dich');
|
|
|
|
// The editor opens on the skeleton headings, not an empty box, and nothing
|
|
// else: the suggestion is a block inside the text that appears when one
|
|
// streams, so before any typing there is no widget and nothing to accept.
|
|
await expect(page.getByTestId('editor-source')).toContainText('##');
|
|
await expect(page.getByTestId('editor-suggestion')).toHaveCount(0);
|
|
await expect(page.getByTestId('editor-accept')).toHaveCount(0);
|
|
});
|
|
|
|
test('a typing pause yields a section suggestion that overwrites on accept', async ({ page }) => {
|
|
await loginAs(page, 'pablo@pablan.dev');
|
|
await newDraft(page, 'Über dich');
|
|
|
|
// Write rough notes under the first heading.
|
|
await page.locator('.cm-content').click();
|
|
await page.keyboard.press('Control+Home');
|
|
await page.keyboard.press('ArrowDown');
|
|
await page.keyboard.type(
|
|
'also der neue kollege wartet die cnc maschinen und ist ansprechpartner für den einkauf.'
|
|
);
|
|
|
|
// After the pause the model streams a refined version of the section.
|
|
await expect(page.getByTestId('editor-accept')).toBeVisible({ timeout: 40_000 });
|
|
await page.getByTestId('editor-accept').click();
|
|
|
|
// Accepting consumes the suggestion (the pane returns to its empty state)
|
|
// and replaces the rough notes — the lowercase draft phrasing is gone.
|
|
await expect(page.getByTestId('editor-accept')).toHaveCount(0);
|
|
await expect(page.getByTestId('editor-source')).not.toContainText('also der neue kollege');
|
|
|
|
// Saving shows what changed first, VSCode-style, and asks again.
|
|
await page.getByTestId('editor-open-save').click();
|
|
await expect(page.getByTestId('editor-diff')).toBeVisible();
|
|
});
|
|
|
|
test('a draft is published from the editor in one step', async ({ page }) => {
|
|
await loginAs(page, 'pablo@pablan.dev');
|
|
const id = await newDraft(page, 'Notiz');
|
|
|
|
await page.locator('.cm-content').click();
|
|
await page.keyboard.type('\nEin kurzer, brauchbarer Inhalt zum Veröffentlichen.');
|
|
|
|
await page.getByTestId('editor-open-save').click();
|
|
await page.getByTestId('editor-publish').click();
|
|
|
|
// Published, and the author is offered a colleague to check it.
|
|
await expect(page.getByTestId('capture-success')).toBeVisible();
|
|
await page.getByTestId('success-view').click();
|
|
await expect(page).toHaveURL(new RegExp(`/documents/${id}$`));
|
|
await expect(page.getByTestId('document-status')).toHaveAttribute('data-status', 'published');
|
|
});
|