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,91 @@
|
||||
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');
|
||||
});
|
||||
Reference in New Issue
Block a user