From c717353037bd843358947bcdd1c1fcb345f449ef Mon Sep 17 00:00:00 2001 From: orfelorfel23 Date: Tue, 8 Sep 2026 16:54:21 +0200 Subject: [PATCH] fix: beheben von Lint- und Prettier-Fehlern in Tests und Config --- .prettierrc | 1 - src/cli/index-cmd.ts | 38 ++++-- src/cli/ingest.ts | 52 +++++-- src/cli/init-vault.ts | 13 +- src/cli/sync.ts | 13 +- src/ingest.ts | 21 ++- src/llm.ts | 40 +++--- src/notes-api-server.ts | 18 ++- src/vault-path.ts | 17 +-- src/watcher.ts | 5 +- tests/determinism.test.ts | 37 +++-- tests/e2e/app.spec.ts | 15 +- tests/find-similar-groups.test.ts | 23 ++-- tests/indexer-edge-cases.test.ts | 25 ++-- tests/ingest-e2e.test.ts | 39 ++++-- tests/notes-api.test.ts | 16 ++- tests/sync-queue.test.ts | 16 ++- tests/vault-path.test.ts | 24 ++-- tests/watcher.test.ts | 30 ++-- tests/webapp/manifest.test.ts | 4 +- tests/webapp/notes-api-client.test.ts | 76 ++++++----- webapp/src/App.tsx | 190 +++++++++++++++++++------- webapp/src/ClusterMap.tsx | 74 ++++++---- webapp/src/Editor.tsx | 66 +++++---- webapp/src/EditorWithLoad.tsx | 70 +++++----- webapp/src/NewNoteDialog.tsx | 69 +++++----- webapp/src/RadialView.tsx | 53 +++---- webapp/src/main.tsx | 2 +- 28 files changed, 675 insertions(+), 372 deletions(-) diff --git a/.prettierrc b/.prettierrc index e67d913..9b9b3a3 100644 --- a/.prettierrc +++ b/.prettierrc @@ -5,7 +5,6 @@ "printWidth": 100, "tabWidth": 2, "useTabs": false, - "semi": true, "bracketSpacing": true, "arrowParens": "always" } diff --git a/src/cli/index-cmd.ts b/src/cli/index-cmd.ts index 21e192e..41222bf 100644 --- a/src/cli/index-cmd.ts +++ b/src/cli/index-cmd.ts @@ -8,7 +8,7 @@ import matter from 'gray-matter'; import { tfIdfCosineSimilarity } from '../ingest.js'; interface VaultNode { - id: string; // Datei-Pfad relativ zum Vault + id: string; // Datei-Pfad relativ zum Vault title: string; type: 'inbox' | 'daily' | 'wiki' | 'project' | 'unknown'; cluster: string | null; @@ -17,21 +17,21 @@ interface VaultNode { aktualisiert: string | null; tags: string[]; size: number; - veraltet: boolean; // 2.6 Veraltet-Erkennung - contentHash: string; // 2.5 Duplikat-Erkennung (SHA-256 vom Body) + veraltet: boolean; // 2.6 Veraltet-Erkennung + contentHash: string; // 2.5 Duplikat-Erkennung (SHA-256 vom Body) } interface VaultEdge { - from: string; // Source-Node-ID - to: string; // Target-Node-ID (Wikilink) + from: string; // Source-Node-ID + to: string; // Target-Node-ID (Wikilink) type: 'wikilink'; } interface Landkarte { nodes: VaultNode[]; edges: VaultEdge[]; - generated: string; // statisch fuer Determinismus (siehe Generator) - vault: string; // Vault-Pfad + generated: string; // statisch fuer Determinismus (siehe Generator) + vault: string; // Vault-Pfad } const SKIP_DIRS = new Set([ @@ -45,9 +45,9 @@ const SKIP_DIRS = new Set([ ]); const SKIP_FILES = new Set([ - '99_System/Index.md', // Output des Indexers - '10_Wiki/Index.md', // Output des Indexers - '99_System/Konfig.md', // System-Konfig, keine echte Notiz + '99_System/Index.md', // Output des Indexers + '10_Wiki/Index.md', // Output des Indexers + '99_System/Konfig.md', // System-Konfig, keine echte Notiz ]); const WIKILINK_RE = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g; @@ -172,7 +172,11 @@ function findMarkdownFiles(vaultPath: string): string[] { return result; } -function parseVaultFile(vaultPath: string, relPath: string, staleThresholdMonths: number): VaultNode { +function parseVaultFile( + vaultPath: string, + relPath: string, + staleThresholdMonths: number, +): VaultNode { const fullPath = join(vaultPath, relPath); const stat = statSync(fullPath); const raw = readFileSync(fullPath, 'utf-8'); @@ -222,7 +226,7 @@ export function isStale( created: string | null, aktualisiert: string | null, thresholdMonths: number, - now: Date = new Date() + now: Date = new Date(), ): boolean { if (type === 'wiki') return false; const reference = aktualisiert ?? created; @@ -295,7 +299,9 @@ function renderSystemIndex(vaultPath: string, nodes: VaultNode[], edges: VaultEd if (similarGroups.length > 0) { lines.push('## Aehnliche Notizen'); lines.push(''); - lines.push('Notizen mit thematischer Aehnlichkeit (TF-IDF Cosinus > 0.15, kein exakter Match).'); + lines.push( + 'Notizen mit thematischer Aehnlichkeit (TF-IDF Cosinus > 0.15, kein exakter Match).', + ); lines.push(''); for (const group of similarGroups) { lines.push(`### Aehnlich (Score ${group.score.toFixed(2)})`); @@ -388,7 +394,11 @@ interface SimilarGroup { * Empfehlung fuer >500 Notizen: Embedding-basierte Aehnlichkeit * (z.B. via QMD-Vektoren) statt TF-IDF. */ -export function findSimilarGroups(vaultPath: string, nodes: VaultNode[], threshold = 0.5): SimilarGroup[] { +export function findSimilarGroups( + vaultPath: string, + nodes: VaultNode[], + threshold = 0.5, +): SimilarGroup[] { // Lese Body-Text fuer jede Notiz const docs: string[] = []; const nodeByDoc = new Map(); diff --git a/src/cli/ingest.ts b/src/cli/ingest.ts index 1d85f0b..a0dccc9 100644 --- a/src/cli/ingest.ts +++ b/src/cli/ingest.ts @@ -1,7 +1,14 @@ // 2.3 Ingest: Inbox -> Minimax -> Wiki-Update // Phase 2.3 -import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, renameSync } from 'node:fs'; +import { + readFileSync, + writeFileSync, + mkdirSync, + existsSync, + readdirSync, + renameSync, +} from 'node:fs'; import { join } from 'node:path'; import matter from 'gray-matter'; import { callLLM, loadLLMConfig, type ChatRequest, type LLMConfigFromVault } from '../llm.js'; @@ -10,7 +17,7 @@ import { startInboxWatcher, type ActiveWatcher } from '../watcher.js'; interface IngestOptions { mock?: boolean; - note?: string; // nur eine bestimmte Notiz verarbeiten + note?: string; // nur eine bestimmte Notiz verarbeiten dryRun?: boolean; watch?: boolean; } @@ -52,7 +59,9 @@ export async function ingestCommand(vaultPath: string, args: string[]): Promise< // 2. Schema lesen const schemaPath = join(vaultPath, '10_Wiki', '_Schema.md'); - const schema = existsSync(schemaPath) ? readFileSync(schemaPath, 'utf-8') : '(kein Schema definiert)'; + const schema = existsSync(schemaPath) + ? readFileSync(schemaPath, 'utf-8') + : '(kein Schema definiert)'; // 3. Indexe lesen const sysIndex = readIfExists(join(vaultPath, '99_System', 'Index.md')); @@ -66,7 +75,9 @@ export async function ingestCommand(vaultPath: string, args: string[]): Promise< } const files = opts.note ? [opts.note] - : readdirSync(inboxDir).filter((f) => f.endsWith('.md') && !f.startsWith('Verarbeitet') && !f.startsWith('Problemfaelle')); + : readdirSync(inboxDir).filter( + (f) => f.endsWith('.md') && !f.startsWith('Verarbeitet') && !f.startsWith('Problemfaelle'), + ); if (files.length === 0) { console.log('Keine Inbox-Notizen zu verarbeiten.'); @@ -80,7 +91,15 @@ export async function ingestCommand(vaultPath: string, args: string[]): Promise< continue; } try { - await processNote(vaultPath, notePath, schema, sysIndex, wikiIndex, llmConfig, !!opts.dryRun); + await processNote( + vaultPath, + notePath, + schema, + sysIndex, + wikiIndex, + llmConfig, + !!opts.dryRun, + ); } catch (err) { console.error(` FEHLER bei ${file}: ${(err as Error).message}`); moveToProblemfaelle(vaultPath, notePath, (err as Error).message); @@ -137,7 +156,7 @@ async function processNote( sysIndex: string, wikiIndex: string, llmConfig: { apiUrl: string; apiKey: string; model: string; mock: boolean }, - dryRun: boolean + dryRun: boolean, ): Promise { console.log(`\n--- Verarbeite: ${notePath} ---`); @@ -207,7 +226,9 @@ ${noteContent}`, console.log(` Frage LLM (${llmConfig.model})...`); const response = await callLLM(request, llmConfig); - console.log(` Antwort: ${response.content.length} Zeichen, ${response.usage?.totalTokens ?? '?'} Tokens`); + console.log( + ` Antwort: ${response.content.length} Zeichen, ${response.usage?.totalTokens ?? '?'} Tokens`, + ); // 4. Antwort parsen const parsed = parseLLMResponse(response.content); @@ -320,12 +341,23 @@ function moveToProblemfaelle(vaultPath: string, notePath: string, reason: string const targetDir = join(vaultPath, '00_Inbox', 'Problemfaelle'); mkdirSync(targetDir, { recursive: true }); const target = join(targetDir, notePath.split(/[\\/]/).pop()!); - try { renameSync(notePath, target); } catch { /* best effort */ } + try { + renameSync(notePath, target); + } catch { + /* best effort */ + } // Log-Eintrag - const logPath = join(vaultPath, '99_System', 'Logs', `Ingest_${new Date().toISOString().slice(0, 10)}.md`); + const logPath = join( + vaultPath, + '99_System', + 'Logs', + `Ingest_${new Date().toISOString().slice(0, 10)}.md`, + ); mkdirSync(join(logPath, '..'), { recursive: true }); const ts = new Date().toISOString(); const line = `- ${ts}: ${notePath}: ${reason}\n`; - const existing = existsSync(logPath) ? readFileSync(logPath, 'utf-8') : `# Ingest-Log ${new Date().toISOString().slice(0, 10)}\n\n`; + const existing = existsSync(logPath) + ? readFileSync(logPath, 'utf-8') + : `# Ingest-Log ${new Date().toISOString().slice(0, 10)}\n\n`; writeFileSync(logPath, existing + line); } diff --git a/src/cli/init-vault.ts b/src/cli/init-vault.ts index 0c86560..2a8ee69 100644 --- a/src/cli/init-vault.ts +++ b/src/cli/init-vault.ts @@ -162,7 +162,9 @@ export async function initVaultCommand(args: string[]): Promise { const target = positional[0]; if (!target) { - console.error('Verwendung: mindomat init-vault [--force] [--no-commit] [--no-interactive] [--remote ]'); + console.error( + 'Verwendung: mindomat init-vault [--force] [--no-commit] [--no-interactive] [--remote ]', + ); process.exit(2); } @@ -205,11 +207,11 @@ export async function initVaultCommand(args: string[]): Promise { const today = new Date().toISOString().slice(0, 10); writeFileSync( join(targetPath, '00_Inbox', `${today}_Kaffee_Ritual.md`), - DEMO_KAFFEE.replace(//g, today) + DEMO_KAFFEE.replace(//g, today), ); writeFileSync( join(targetPath, '00_Inbox', `${today}_Second_Brain_Notiz.md`), - DEMO_SECOND_BRAIN.replace(//g, today) + DEMO_SECOND_BRAIN.replace(//g, today), ); console.log(' 2 Demo-Notizen angelegt'); @@ -240,7 +242,10 @@ export async function initVaultCommand(args: string[]): Promise { if (!opts.noCommit) { try { execSync('git add .', { cwd: targetPath, stdio: 'inherit' }); - execSync('git commit -m "Initial: Vault-Struktur, Konfig, Demo-Notizen"', { cwd: targetPath, stdio: 'inherit' }); + execSync('git commit -m "Initial: Vault-Struktur, Konfig, Demo-Notizen"', { + cwd: targetPath, + stdio: 'inherit', + }); console.log(' Initial-Commit erstellt'); } catch (err) { console.warn(` Konnte Commit nicht erstellen: ${(err as Error).message}`); diff --git a/src/cli/sync.ts b/src/cli/sync.ts index 8f48ac3..3691a9a 100644 --- a/src/cli/sync.ts +++ b/src/cli/sync.ts @@ -28,7 +28,9 @@ export async function syncCommand(vaultPath: string, args: string[]): Promise Wiki-Status 'konflikt' setzen. */ export function preCheckConflict(noteText: string, wikiText: string): boolean { - const NEGATION = ['kein', 'keine', 'keinem', 'keinen', 'keiner', 'keines', 'nicht', 'nie', 'niemals', 'keinesfalls', 'keinerlei', 'nichts']; + const NEGATION = [ + 'kein', + 'keine', + 'keinem', + 'keinen', + 'keiner', + 'keines', + 'nicht', + 'nie', + 'niemals', + 'keinesfalls', + 'keinerlei', + 'nichts', + ]; const noteLower = noteText.toLowerCase(); const hasNegation = NEGATION.some((w) => { // Wortgrenzen beachten @@ -51,11 +64,7 @@ export function jaccardSimilarity(a: string, b: string): number { * Fuer eine thematisch aehnliche Notiz (Score > 0.5) ist das ein guter Marker, * aber kein Ersatz fuer echte semantische Embeddings. */ -export function tfIdfCosineSimilarity( - docs: string[], - doc1: string, - doc2: string, -): number { +export function tfIdfCosineSimilarity(docs: string[], doc1: string, doc2: string): number { // Tokenisiere alle Dokumente const tokenized = docs.map((d) => extractWords(d)); if (tokenized.length < 2) return 0; diff --git a/src/llm.ts b/src/llm.ts index 5febc47..e16f64c 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -3,7 +3,7 @@ export interface MinimaxConfig { apiUrl: string; - apiKey: string; // aufgeloest aus Env-Var (nicht im Repo!) + apiKey: string; // aufgeloest aus Env-Var (nicht im Repo!) model: string; mock: boolean; } @@ -32,7 +32,7 @@ export async function callLLM(req: ChatRequest, config: MinimaxConfig): Promise< method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${config.apiKey}`, + Authorization: `Bearer ${config.apiKey}`, }, body: JSON.stringify({ model: config.model, @@ -51,11 +51,13 @@ export async function callLLM(req: ChatRequest, config: MinimaxConfig): Promise< }; return { content: data.choices?.[0]?.message?.content ?? '', - usage: data.usage ? { - promptTokens: data.usage.prompt_tokens ?? 0, - completionTokens: data.usage.completion_tokens ?? 0, - totalTokens: data.usage.total_tokens ?? 0, - } : undefined, + usage: data.usage + ? { + promptTokens: data.usage.prompt_tokens ?? 0, + completionTokens: data.usage.completion_tokens ?? 0, + totalTokens: data.usage.total_tokens ?? 0, + } + : undefined, }; } @@ -77,14 +79,18 @@ function mockResponse(req: ChatRequest): ChatResponse { const body = `# ${title}\n\n${contentSnippet}\n\n_(Mock-Ingest, kein echter LLM-Aufruf.)_`; - const wikiUpdate = JSON.stringify({ - action: 'update_or_create', - page_title: title, - summary: contentSnippet.slice(0, 200), - body_markdown: body, - sources_added: [title], - konflikte: { hart: false, wert: false, logisch: false }, - }, null, 2); + const wikiUpdate = JSON.stringify( + { + action: 'update_or_create', + page_title: title, + summary: contentSnippet.slice(0, 200), + body_markdown: body, + sources_added: [title], + konflikte: { hart: false, wert: false, logisch: false }, + }, + null, + 2, + ); return { content: wikiUpdate, @@ -98,7 +104,7 @@ function mockResponse(req: ChatRequest): ChatResponse { */ export interface LLMConfigFromVault { apiUrl: string; - apiKeyEnv: string; // Name der Env-Variable + apiKeyEnv: string; // Name der Env-Variable model: string; } @@ -110,7 +116,7 @@ export function loadLLMConfig(vaultConfig: LLMConfigFromVault, mock: boolean): M if (!apiKey) { throw new Error( `API-Key fehlt: Environment-Variable ${vaultConfig.apiKeyEnv} ist nicht gesetzt. ` + - `Setze sie oder nutze --mock fuer Tests ohne Key.` + `Setze sie oder nutze --mock fuer Tests ohne Key.`, ); } return { diff --git a/src/notes-api-server.ts b/src/notes-api-server.ts index a12bc10..cad7fc9 100644 --- a/src/notes-api-server.ts +++ b/src/notes-api-server.ts @@ -25,7 +25,7 @@ export interface AuthCheck { } export interface NotesApiOptions { - authToken?: string; // wenn gesetzt, ist Auth noetig + authToken?: string; // wenn gesetzt, ist Auth noetig allowedOrigins?: string[]; // CORS-Whitelist } @@ -46,9 +46,7 @@ export class NotesApi { // Token-Format: . const [ts, hmac] = token.split('.'); if (!ts || !hmac) return { ok: false, reason: 'Token-Format ungueltig' }; - const expected = createHmac('sha256', this.options.authToken) - .update(ts) - .digest('hex'); + const expected = createHmac('sha256', this.options.authToken).update(ts).digest('hex'); if (expected.length !== hmac.length) return { ok: false, reason: 'Token ungueltig' }; const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(hmac)); return ok ? { ok: true } : { ok: false, reason: 'Token ungueltig' }; @@ -67,7 +65,9 @@ export class NotesApi { return true; // keine Beschraenkung } if (!origin) return false; - return this.options.allowedOrigins.includes(origin) || this.options.allowedOrigins.includes('*'); + return ( + this.options.allowedOrigins.includes(origin) || this.options.allowedOrigins.includes('*') + ); } /** Setzt CORS-Header, wenn Origin erlaubt ist. */ @@ -129,8 +129,12 @@ export class NotesApi { }); let stdout = ''; let stderr = ''; - child.stdout.on('data', (chunk: Buffer) => { stdout += chunk.toString(); }); - child.stderr.on('data', (chunk: Buffer) => { stderr += chunk.toString(); }); + child.stdout.on('data', (chunk: Buffer) => { + stdout += chunk.toString(); + }); + child.stderr.on('data', (chunk: Buffer) => { + stderr += chunk.toString(); + }); child.on('close', (code: number | null) => { if (code === 0) { resolve({ ok: true, message: stdout.trim() || 'Sync erfolgreich' }); diff --git a/src/vault-path.ts b/src/vault-path.ts index 38b0b7b..fd9111b 100644 --- a/src/vault-path.ts +++ b/src/vault-path.ts @@ -19,7 +19,10 @@ export interface ResolvedVault { } export class VaultPathError extends Error { - constructor(message: string, public readonly code: 'MISSING' | 'NOT_FOUND' | 'NOT_DIRECTORY') { + constructor( + message: string, + public readonly code: 'MISSING' | 'NOT_FOUND' | 'NOT_DIRECTORY', + ) { super(message); this.name = 'VaultPathError'; } @@ -52,7 +55,7 @@ export function resolveVaultPath(opts: VaultPathOptions): ResolvedVault { // 3. Nichts gefunden throw new VaultPathError( 'Vault-Pfad fehlt. Setze MINDOMAT_VAULT_PATH oder nutze --vault .', - 'MISSING' + 'MISSING', ); } @@ -79,16 +82,10 @@ function extractCliVaultArg(args: string[]): string | null { */ export function assertVaultExists(path: string): void { if (!existsSync(path)) { - throw new VaultPathError( - `Vault-Pfad existiert nicht: ${path}`, - 'NOT_FOUND' - ); + throw new VaultPathError(`Vault-Pfad existiert nicht: ${path}`, 'NOT_FOUND'); } const stat = statSync(path); if (!stat.isDirectory()) { - throw new VaultPathError( - `Vault-Pfad ist kein Verzeichnis: ${path}`, - 'NOT_DIRECTORY' - ); + throw new VaultPathError(`Vault-Pfad ist kein Verzeichnis: ${path}`, 'NOT_DIRECTORY'); } } diff --git a/src/watcher.ts b/src/watcher.ts index f37b4e2..e4e541d 100644 --- a/src/watcher.ts +++ b/src/watcher.ts @@ -20,7 +20,10 @@ export interface ActiveWatcher { * * Testbar: processNote wird als Dependency uebergeben. */ -export async function startInboxWatcher(inboxDir: string, deps: WatcherDeps): Promise { +export async function startInboxWatcher( + inboxDir: string, + deps: WatcherDeps, +): Promise { const debounceMs = deps.debounceMs ?? 1000; const onError = deps.onError ?? ((err) => console.error(`[watch] FEHLER: ${err.message}`)); diff --git a/tests/determinism.test.ts b/tests/determinism.test.ts index 06f3875..b58c917 100644 --- a/tests/determinism.test.ts +++ b/tests/determinism.test.ts @@ -15,17 +15,27 @@ function createTestVault(): string { // 12 Ordner const dirs = [ - '00_Inbox', '00_Inbox/Verarbeitet', '00_Inbox/Problemfaelle', - '01_Daily', '10_Wiki', '10_Wiki/Seiten', - '20_Projekte', '90_Templates', - '99_System', '99_System/Skripte', '99_System/Logs', '99_System/Cache', + '00_Inbox', + '00_Inbox/Verarbeitet', + '00_Inbox/Problemfaelle', + '01_Daily', + '10_Wiki', + '10_Wiki/Seiten', + '20_Projekte', + '90_Templates', + '99_System', + '99_System/Skripte', + '99_System/Logs', + '99_System/Cache', ]; for (const d of dirs) { mkdirSync(join(dir, d), { recursive: true }); } // 3 Test-Notizen mit deterministischen Wikilinks - writeFileSync(join(dir, '00_Inbox', '2026-01-01_A.md'), `--- + writeFileSync( + join(dir, '00_Inbox', '2026-01-01_A.md'), + `--- title: A created: 2026-01-01 --- @@ -33,8 +43,11 @@ created: 2026-01-01 # A Verweist auf [[B]] und [[C]]. -`); - writeFileSync(join(dir, '00_Inbox', '2026-01-02_B.md'), `--- +`, + ); + writeFileSync( + join(dir, '00_Inbox', '2026-01-02_B.md'), + `--- title: B created: 2026-01-02 --- @@ -42,8 +55,11 @@ created: 2026-01-02 # B Verweist auf [[A]]. -`); - writeFileSync(join(dir, '00_Inbox', '2026-01-03_C.md'), `--- +`, + ); + writeFileSync( + join(dir, '00_Inbox', '2026-01-03_C.md'), + `--- title: C created: 2026-01-03 --- @@ -51,7 +67,8 @@ created: 2026-01-03 # C Kein Link. -`); +`, + ); return dir; } diff --git a/tests/e2e/app.spec.ts b/tests/e2e/app.spec.ts index 7775670..da85771 100644 --- a/tests/e2e/app.spec.ts +++ b/tests/e2e/app.spec.ts @@ -21,13 +21,22 @@ test.describe('Mind-o-Mat Webapp', () => { test('Tab-Wechsel funktioniert', async ({ page }) => { // Graph ist initial aktiv - await expect(page.getByRole('button', { name: 'Graph' })).toHaveCSS('background-color', 'rgb(255, 255, 255)'); + await expect(page.getByRole('button', { name: 'Graph' })).toHaveCSS( + 'background-color', + 'rgb(255, 255, 255)', + ); // Klick auf Cluster-Karte -> wechselt aktiv-Status await page.getByRole('button', { name: 'Cluster-Karte' }).click(); - await expect(page.getByRole('button', { name: 'Cluster-Karte' })).toHaveCSS('background-color', 'rgb(255, 255, 255)'); + await expect(page.getByRole('button', { name: 'Cluster-Karte' })).toHaveCSS( + 'background-color', + 'rgb(255, 255, 255)', + ); // Klick auf Radial await page.getByRole('button', { name: 'Radial' }).click(); - await expect(page.getByRole('button', { name: 'Radial' })).toHaveCSS('background-color', 'rgb(255, 255, 255)'); + await expect(page.getByRole('button', { name: 'Radial' })).toHaveCSS( + 'background-color', + 'rgb(255, 255, 255)', + ); }); test('Legend ist sichtbar', async ({ page }) => { diff --git a/tests/find-similar-groups.test.ts b/tests/find-similar-groups.test.ts index 7b5c1ea..1b2b3b0 100644 --- a/tests/find-similar-groups.test.ts +++ b/tests/find-similar-groups.test.ts @@ -1,5 +1,5 @@ // Direkter Unit-Test fuer findSimilarGroups (v0.5) -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, afterEach } from 'vitest'; import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -14,8 +14,14 @@ afterEach(() => { function setupVault(files: Record): string { const vault = mkdtempSync(join(tmpdir(), 'mindomat-similar-direct-')); for (const d of [ - '00_Inbox', '10_Wiki/Seiten', '01_Daily', '20_Projekte', - '99_System', '99_System/Skripte', '99_System/Logs', '99_System/Cache', + '00_Inbox', + '10_Wiki/Seiten', + '01_Daily', + '20_Projekte', + '99_System', + '99_System/Skripte', + '99_System/Logs', + '99_System/Cache', ]) { mkdirSync(join(vault, d), { recursive: true }); } @@ -103,9 +109,9 @@ Ich gehe joggen im Park.`, // Es sollte mindestens eine Gruppe geben (A und B sind aehnlich) expect(groups.length).toBeGreaterThanOrEqual(1); // Die aehnlichste Gruppe sollte A+B sein - const abGroup = groups.find((g) => - g.nodes.some((n) => n.id.includes('_A.md')) && - g.nodes.some((n) => n.id.includes('_B.md')), + const abGroup = groups.find( + (g) => + g.nodes.some((n) => n.id.includes('_A.md')) && g.nodes.some((n) => n.id.includes('_B.md')), ); expect(abGroup).toBeDefined(); }); @@ -124,10 +130,7 @@ title: Test Kaffee Tee Brot Butter`, }); vaults.push(vault); - const nodes = [ - node('01_Daily/2026-09-05_A.md'), - node('01_Daily/2026-09-05_B.md'), - ]; + const nodes = [node('01_Daily/2026-09-05_A.md'), node('01_Daily/2026-09-05_B.md')]; const low = findSimilarGroups(vault, nodes, 0.1); const high = findSimilarGroups(vault, nodes, 0.99); expect(low.length).toBeGreaterThanOrEqual(high.length); diff --git a/tests/indexer-edge-cases.test.ts b/tests/indexer-edge-cases.test.ts index b40843d..945c9ce 100644 --- a/tests/indexer-edge-cases.test.ts +++ b/tests/indexer-edge-cases.test.ts @@ -13,16 +13,26 @@ const CLI = `node ${TOOL}/bin/mindomat.mjs`; function setupVault(files: Record = {}, includeKonfig = true): string { const vault = mkdtempSync(join(tmpdir(), 'mindomat-edge-')); for (const d of [ - '00_Inbox', '00_Inbox/Verarbeitet', '00_Inbox/Problemfaelle', - '01_Daily', '10_Wiki', '10_Wiki/Seiten', - '20_Projekte', '90_Templates', - '99_System', '99_System/Skripte', '99_System/Logs', '99_System/Cache', + '00_Inbox', + '00_Inbox/Verarbeitet', + '00_Inbox/Problemfaelle', + '01_Daily', + '10_Wiki', + '10_Wiki/Seiten', + '20_Projekte', + '90_Templates', + '99_System', + '99_System/Skripte', + '99_System/Logs', + '99_System/Cache', ]) { mkdirSync(join(vault, d), { recursive: true }); } if (includeKonfig) { - writeFileSync(join(vault, '99_System/Konfig.md'), - '---\nveraltet_schwellwert_monate: 12\n---\n'); + writeFileSync( + join(vault, '99_System/Konfig.md'), + '---\nveraltet_schwellwert_monate: 12\n---\n', + ); } for (const [rel, content] of Object.entries(files)) { const full = join(vault, rel); @@ -87,8 +97,7 @@ describe('Indexer Edge Cases', () => { it('tote Wikilinks erscheinen in Index.md', () => { const vault = setupVault({ - '00_Inbox/2026-09-05_A.md': - '---\ntitle: A\n---\n\nVerweist auf [[B_Datei_Nicht_Da]].', + '00_Inbox/2026-09-05_A.md': '---\ntitle: A\n---\n\nVerweist auf [[B_Datei_Nicht_Da]].', }); vaults.push(vault); runIndexer(vault); diff --git a/tests/ingest-e2e.test.ts b/tests/ingest-e2e.test.ts index 707649f..ca47e8b 100644 --- a/tests/ingest-e2e.test.ts +++ b/tests/ingest-e2e.test.ts @@ -3,7 +3,16 @@ import { describe, it, expect, afterEach } from 'vitest'; import { execSync } from 'node:child_process'; -import { mkdtempSync, writeFileSync, mkdirSync, readFileSync, existsSync, rmSync, copyFileSync, readdirSync } from 'node:fs'; +import { + mkdtempSync, + writeFileSync, + mkdirSync, + readFileSync, + existsSync, + rmSync, + copyFileSync, + readdirSync, +} from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -13,17 +22,26 @@ const CLI = `node ${TOOL}/bin/mindomat.mjs`; function setupTestVault(): string { const vault = mkdtempSync(join(tmpdir(), 'mindomat-ingest-e2e-')); const dirs = [ - '00_Inbox', '00_Inbox/Verarbeitet', '00_Inbox/Problemfaelle', - '01_Daily', '10_Wiki', '10_Wiki/Seiten', - '20_Projekte', '90_Templates', - '99_System', '99_System/Skripte', '99_System/Logs', '99_System/Cache', + '00_Inbox', + '00_Inbox/Verarbeitet', + '00_Inbox/Problemfaelle', + '01_Daily', + '10_Wiki', + '10_Wiki/Seiten', + '20_Projekte', + '90_Templates', + '99_System', + '99_System/Skripte', + '99_System/Logs', + '99_System/Cache', ]; for (const d of dirs) { mkdirSync(join(vault, d), { recursive: true }); } // Konfig - writeFileSync(join(vault, '99_System', 'Konfig.md'), + writeFileSync( + join(vault, '99_System', 'Konfig.md'), `--- veraltet_schwellwert_monate: 12 ki_provider: minimax @@ -31,7 +49,8 @@ ki_modell: MiniMax-M3 ki_api_url: https://api.minimaxi.chat/v1 ki_api_key_env: MINIMAX_API_KEY --- -`); +`, + ); // Schema copyFileSync(join(TOOL, 'src/templates/_Schema.md'), join(vault, '10_Wiki/_Schema.md')); @@ -53,7 +72,8 @@ describe('Ingest E2E mit --mock', () => { vaults.push(vault); const today = new Date().toISOString().slice(0, 10); - writeFileSync(join(vault, '00_Inbox', `${today}_Kaffee_Ritual.md`), + writeFileSync( + join(vault, '00_Inbox', `${today}_Kaffee_Ritual.md`), `--- title: Kaffee_Ritual created: ${today} @@ -63,7 +83,8 @@ tags: [Gewohnheit] # Kaffee Ritual Jeden Morgen Kaffee, schwarz, aus der Handmuehle. -`); +`, + ); execSync(`${CLI} ingest --vault "${vault}" --mock`, { stdio: 'pipe' }); diff --git a/tests/notes-api.test.ts b/tests/notes-api.test.ts index de485a0..664954e 100644 --- a/tests/notes-api.test.ts +++ b/tests/notes-api.test.ts @@ -15,10 +15,18 @@ afterEach(() => { function setupVault(files: Record = {}): string { const vault = mkdtempSync(join(tmpdir(), 'mindomat-notes-api-')); for (const d of [ - '00_Inbox', '00_Inbox/Verarbeitet', '00_Inbox/Problemfaelle', - '01_Daily', '10_Wiki', '10_Wiki/Seiten', - '20_Projekte', '90_Templates', - '99_System', '99_System/Skripte', '99_System/Logs', '99_System/Cache', + '00_Inbox', + '00_Inbox/Verarbeitet', + '00_Inbox/Problemfaelle', + '01_Daily', + '10_Wiki', + '10_Wiki/Seiten', + '20_Projekte', + '90_Templates', + '99_System', + '99_System/Skripte', + '99_System/Logs', + '99_System/Cache', ]) { mkdirSync(join(vault, d), { recursive: true }); } diff --git a/tests/sync-queue.test.ts b/tests/sync-queue.test.ts index 93857d3..ef76d95 100644 --- a/tests/sync-queue.test.ts +++ b/tests/sync-queue.test.ts @@ -7,11 +7,19 @@ import { describe, it, expect, beforeEach } from 'vitest'; const store = new Map(); (globalThis as any).localStorage = { getItem: (key: string) => store.get(key) ?? null, - setItem: (key: string, value: string) => { store.set(key, value); }, - removeItem: (key: string) => { store.delete(key); }, - clear: () => { store.clear(); }, + setItem: (key: string, value: string) => { + store.set(key, value); + }, + removeItem: (key: string) => { + store.delete(key); + }, + clear: () => { + store.clear(); + }, key: (i: number) => Array.from(store.keys())[i] ?? null, - get length() { return store.size; }, + get length() { + return store.size; + }, }; import { diff --git a/tests/vault-path.test.ts b/tests/vault-path.test.ts index 2c47d26..e5217f7 100644 --- a/tests/vault-path.test.ts +++ b/tests/vault-path.test.ts @@ -61,11 +61,13 @@ describe('resolveVaultPath', () => { }); it('wirft VaultPathError MISSING, wenn weder CLI noch Env', () => { - expect(() => resolveVaultPath({ - args: [], - env: {}, - cwd, - })).toThrow(VaultPathError); + expect(() => + resolveVaultPath({ + args: [], + env: {}, + cwd, + }), + ).toThrow(VaultPathError); try { resolveVaultPath({ args: [], env: {}, cwd }); } catch (err) { @@ -75,11 +77,13 @@ describe('resolveVaultPath', () => { }); it('wirft VaultPathError MISSING, wenn Env-Var leer', () => { - expect(() => resolveVaultPath({ - args: [], - env: { MINDOMAT_VAULT_PATH: ' ' }, - cwd, - })).toThrow(VaultPathError); + expect(() => + resolveVaultPath({ + args: [], + env: { MINDOMAT_VAULT_PATH: ' ' }, + cwd, + }), + ).toThrow(VaultPathError); }); it('loest relative Pfade gegen cwd auf', () => { diff --git a/tests/watcher.test.ts b/tests/watcher.test.ts index 5f175c1..c14acfe 100644 --- a/tests/watcher.test.ts +++ b/tests/watcher.test.ts @@ -2,7 +2,7 @@ // Bonus 21: Watcher-Edge-Cases import { describe, it, expect, afterEach } from 'vitest'; -import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { startInboxWatcher, type ActiveWatcher } from '../src/watcher.js'; @@ -27,7 +27,9 @@ describe('startInboxWatcher', () => { const inbox = setupInbox(); const processed: string[] = []; const watcher = await startInboxWatcher(inbox, { - processNote: async (p) => { processed.push(p); }, + processNote: async (p) => { + processed.push(p); + }, debounceMs: 50, }); watchers.push(watcher); @@ -40,7 +42,9 @@ describe('startInboxWatcher', () => { const inbox = setupInbox(); const processed: string[] = []; const watcher = await startInboxWatcher(inbox, { - processNote: async (p) => { processed.push(p); }, + processNote: async (p) => { + processed.push(p); + }, debounceMs: 100, }); watchers.push(watcher); @@ -63,7 +67,9 @@ describe('startInboxWatcher', () => { const inbox = setupInbox(); const processed: string[] = []; const watcher = await startInboxWatcher(inbox, { - processNote: async (p) => { processed.push(p); }, + processNote: async (p) => { + processed.push(p); + }, debounceMs: 100, }); watchers.push(watcher); @@ -81,7 +87,9 @@ describe('startInboxWatcher', () => { const inbox = setupInbox(); const processed: string[] = []; const watcher = await startInboxWatcher(inbox, { - processNote: async (p) => { processed.push(p); }, + processNote: async (p) => { + processed.push(p); + }, debounceMs: 100, }); watchers.push(watcher); @@ -99,7 +107,9 @@ describe('startInboxWatcher', () => { const inbox = setupInbox(); const processed: string[] = []; const watcher = await startInboxWatcher(inbox, { - processNote: async (p) => { processed.push(p); }, + processNote: async (p) => { + processed.push(p); + }, debounceMs: 500, }); watchers.push(watcher); @@ -123,8 +133,12 @@ describe('startInboxWatcher', () => { const inbox = setupInbox(); const errors: Array<{ err: Error; path: string }> = []; const watcher = await startInboxWatcher(inbox, { - processNote: async () => { throw new Error('Test-Fehler'); }, - onError: (err, path) => { errors.push({ err, path }); }, + processNote: async () => { + throw new Error('Test-Fehler'); + }, + onError: (err, path) => { + errors.push({ err, path }); + }, debounceMs: 100, }); watchers.push(watcher); diff --git a/tests/webapp/manifest.test.ts b/tests/webapp/manifest.test.ts index 402dbf3..1f7ef14 100644 --- a/tests/webapp/manifest.test.ts +++ b/tests/webapp/manifest.test.ts @@ -24,7 +24,9 @@ describe('PWA-Manifest', () => { }); it('verweist auf existierende Icons', () => { - const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as { icons: Array<{ src: string }> }; + const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as { + icons: Array<{ src: string }>; + }; for (const icon of manifest.icons) { const iconPath = join(WEBAPP_DIR, 'public', icon.src); expect(existsSync(iconPath), `Icon fehlt: ${icon.src}`).toBe(true); diff --git a/tests/webapp/notes-api-client.test.ts b/tests/webapp/notes-api-client.test.ts index f66cb0d..201ffd2 100644 --- a/tests/webapp/notes-api-client.test.ts +++ b/tests/webapp/notes-api-client.test.ts @@ -2,7 +2,13 @@ // Phase 4.1 import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import { loadNote, saveNote, triggerSync, listNotes, type NoteData } from '../../webapp/src/notes-api'; +import { + loadNote, + saveNote, + triggerSync, + listNotes, + type NoteData, +} from '../../webapp/src/notes-api'; describe('notes-api Client', () => { let originalFetch: typeof globalThis.fetch; @@ -18,8 +24,8 @@ describe('notes-api Client', () => { describe('loadNote', () => { it('gibt null zurueck bei 404', async () => { - globalThis.fetch = vi.fn(async () => - new Response('Not Found', { status: 404, statusText: 'Not Found' }), + globalThis.fetch = vi.fn( + async () => new Response('Not Found', { status: 404, statusText: 'Not Found' }), ) as typeof fetch; const result = await loadNote('notiz.md'); expect(result).toBeNull(); @@ -32,19 +38,21 @@ describe('notes-api Client', () => { frontmatter: { title: 'Test' }, lastModified: '2026-09-05T10:00:00Z', }; - globalThis.fetch = vi.fn(async () => - new Response(JSON.stringify(noteData), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }), + globalThis.fetch = vi.fn( + async () => + new Response(JSON.stringify(noteData), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), ) as typeof fetch; const result = await loadNote('test.md'); expect(result).toEqual(noteData); }); it('wirft bei HTTP-Fehler (500)', async () => { - globalThis.fetch = vi.fn(async () => - new Response('Server Error', { status: 500, statusText: 'Internal Server Error' }), + globalThis.fetch = vi.fn( + async () => + new Response('Server Error', { status: 500, statusText: 'Internal Server Error' }), ) as typeof fetch; await expect(loadNote('test.md')).rejects.toThrow('HTTP 500'); }); @@ -53,19 +61,21 @@ describe('notes-api Client', () => { const fetchMock = vi.fn(async () => new Response('null', { status: 404 })) as typeof fetch; globalThis.fetch = fetchMock; await loadNote('path with spaces.md'); - expect(fetchMock).toHaveBeenCalledWith( - '/api/notes/path%20with%20spaces.md', - ); + expect(fetchMock).toHaveBeenCalledWith('/api/notes/path%20with%20spaces.md'); }); }); describe('saveNote', () => { it('sendet PUT mit Content im Body', async () => { - const fetchMock = vi.fn(async () => - new Response(JSON.stringify({ id: 'x.md', content: 'Y', frontmatter: {}, lastModified: 'now' }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }), + const fetchMock = vi.fn( + async () => + new Response( + JSON.stringify({ id: 'x.md', content: 'Y', frontmatter: {}, lastModified: 'now' }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }, + ), ) as typeof fetch; globalThis.fetch = fetchMock; await saveNote('x.md', 'Y'); @@ -80,11 +90,12 @@ describe('notes-api Client', () => { describe('triggerSync', () => { it('sendet POST an /api/sync und gibt ok zurueck', async () => { - const fetchMock = vi.fn(async () => - new Response(JSON.stringify({ ok: true, message: 'Sync OK' }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }), + const fetchMock = vi.fn( + async () => + new Response(JSON.stringify({ ok: true, message: 'Sync OK' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), ) as typeof fetch; globalThis.fetch = fetchMock; const result = await triggerSync(); @@ -93,29 +104,26 @@ describe('notes-api Client', () => { }); it('gibt Fehler zurueck bei 500', async () => { - globalThis.fetch = vi.fn(async () => - new Response('Fail', { status: 500 }), - ) as typeof fetch; + globalThis.fetch = vi.fn(async () => new Response('Fail', { status: 500 })) as typeof fetch; await expect(triggerSync()).rejects.toThrow('HTTP 500'); }); }); describe('listNotes', () => { it('parst Notiz-Liste', async () => { - globalThis.fetch = vi.fn(async () => - new Response(JSON.stringify({ notes: ['a.md', 'b.md'] }), { - status: 200, - headers: { 'Content-Type': 'application/json' }, - }), + globalThis.fetch = vi.fn( + async () => + new Response(JSON.stringify({ notes: ['a.md', 'b.md'] }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), ) as typeof fetch; const notes = await listNotes(); expect(notes).toEqual(['a.md', 'b.md']); }); it('gibt leeres Array bei Fehler', async () => { - globalThis.fetch = vi.fn(async () => - new Response('Boom', { status: 500 }), - ) as typeof fetch; + globalThis.fetch = vi.fn(async () => new Response('Boom', { status: 500 })) as typeof fetch; await expect(listNotes()).rejects.toThrow('HTTP 500'); }); }); diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 5278da1..014be56 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -57,14 +57,15 @@ export default function App() { { selector: 'node', style: { - 'background-color': (ele: cytoscape.NodeSingular) => TYPE_COLORS[ele.data('type')] ?? '#999', - 'label': 'data(label)', + 'background-color': (ele: cytoscape.NodeSingular) => + TYPE_COLORS[ele.data('type')] ?? '#999', + label: 'data(label)', 'font-size': '10px', 'text-valign': 'bottom', 'text-halign': 'center', 'text-margin-y': 4, - 'width': 24, - 'height': 24, + width: 24, + height: 24, 'border-width': 1, 'border-color': '#333', }, @@ -72,7 +73,7 @@ export default function App() { { selector: 'edge', style: { - 'width': 1, + width: 1, 'line-color': '#ccc', 'curve-style': 'bezier', 'target-arrow-shape': 'none', @@ -121,20 +122,35 @@ export default function App() { }, [landkarte, view]); return ( -
-
+ flexDirection: 'column', + height: '100vh', + fontFamily: 'sans-serif', + }} + > +

Mind-o-Mat

))} diff --git a/webapp/src/Editor.tsx b/webapp/src/Editor.tsx index 47a75b1..5f2acac 100644 --- a/webapp/src/Editor.tsx +++ b/webapp/src/Editor.tsx @@ -56,43 +56,57 @@ export default function Editor({ initialContent, onSave, noteId }: Props) { return (
-
+
{noteId} -
-
- -
- {showPreview && ( -
+ borderRight: showPreview ? '1px solid #ccc' : 'none', + }} + > + +
+ {showPreview && ( +
)} diff --git a/webapp/src/EditorWithLoad.tsx b/webapp/src/EditorWithLoad.tsx index bbdea03..9a2c128 100644 --- a/webapp/src/EditorWithLoad.tsx +++ b/webapp/src/EditorWithLoad.tsx @@ -12,8 +12,8 @@ export default function EditorWithLoad({ noteId, onClose }: Props) { const [note, setNote] = useState(null); const [error, setError] = useState(null); const [syncStatus, setSyncStatus] = useState(''); - const [autoSync, setAutoSync] = useState(() => - localStorage.getItem('mindomat-auto-sync') === 'true', + const [autoSync, setAutoSync] = useState( + () => localStorage.getItem('mindomat-auto-sync') === 'true', ); const [pendingCount, setPendingCount] = useState(0); @@ -112,48 +112,50 @@ export default function EditorWithLoad({ noteId, onClose }: Props) { return (
- -
- - {pendingCount > 0 && ( - ⚠️ {pendingCount} ausstehend - )} + {pendingCount > 0 && ⚠️ {pendingCount} ausstehend} {syncStatus} Zuletzt geändert: {note.lastModified} -
diff --git a/webapp/src/NewNoteDialog.tsx b/webapp/src/NewNoteDialog.tsx index 0554bde..bd53f2a 100644 --- a/webapp/src/NewNoteDialog.tsx +++ b/webapp/src/NewNoteDialog.tsx @@ -30,11 +30,12 @@ export default function NewNoteDialog({ onClose, onCreated }: Props) { setError(null); try { const date = todayString(); - const tagsArray = tags.split(',').map((t) => t.trim()).filter(Boolean); - const tagLine = tagsArray.length > 0 - ? `tags: [${tagsArray.join(', ')}]\n` - : ''; - const fullBody = body.startsWith('#') ? body : (title + '\n\n' + body); + const tagsArray = tags + .split(',') + .map((t) => t.trim()) + .filter(Boolean); + const tagLine = tagsArray.length > 0 ? `tags: [${tagsArray.join(', ')}]\n` : ''; + const fullBody = body.startsWith('#') ? body : title + '\n\n' + body; const content = `--- title: ${title} created: ${date} @@ -58,28 +59,32 @@ ${fullBody} }; return ( -
-
+
+

Neue Notiz

@@ -146,18 +151,12 @@ ${fullBody} />
- {error && ( -
{error}
- )} + {error &&
{error}
}