// Tests der reinen Module: gjs -m dev/tests/run.js import * as D from '../../desktop@marha.de/dates.js'; import {continueList, renderMarkdown} from '../../desktop@marha.de/markdown.js'; import {batches, concat, fileStamp, planPull, planPush, stampToSeconds, tailHash, uploadItem} from '../../desktop@marha.de/files.js'; let failed = 0; function eq(actual, expected, name) { const a = JSON.stringify(actual), e = JSON.stringify(expected); if (a === e) print(`ok ${name}`); else { failed++; print(`FAIL ${name}\n got ${a}\n expected ${e}`); } } eq(D.toDateString(new Date(2026, 8, 17)), '2026-09-17', 'toDateString'); eq(D.addDays('2026-09-30', 1), '2026-10-01', 'addDays über Monatsgrenze'); eq(D.addDays('2026-01-01', -1), '2025-12-31', 'addDays rückwärts über Jahr'); eq(D.startOfWeek('2026-09-17'), '2026-09-14', 'startOfWeek Do → Mo'); eq(D.startOfWeek('2026-09-20'), '2026-09-14', 'startOfWeek So → Mo'); eq(D.startOfWeek('2026-09-14'), '2026-09-14', 'startOfWeek Mo bleibt'); eq(D.weekDays('2026-09-17').length, 7, 'weekDays 7 Tage'); const grid = D.monthGrid(2026, 8); eq(grid[0], '2026-08-31', 'monthGrid startet Montag davor'); eq(grid.length, 42, 'monthGrid 42 Zellen'); eq(D.monthGrid(2026, 5)[0], '2026-06-01', 'monthGrid Monat beginnt Montag'); eq(D.isoWeek('2026-09-17'), 38, 'isoWeek'); eq(D.isoWeek('2027-01-01'), 53, 'isoWeek Jahreswechsel'); eq(D.formatMonth(2026, 8), 'September 2026', 'formatMonth'); eq(D.formatDate('2026-09-17'), 'Do, 17.09.2026', 'formatDate'); eq(D.formatDayMonth('2026-09-17'), '17.09.', 'formatDayMonth'); eq(D.parseDateInput('17.09.2026'), '2026-09-17', 'parseDateInput dd.mm.yyyy'); eq(D.parseDateInput('1.9.26'), '2026-09-01', 'parseDateInput kurz'); eq(D.parseDateInput('2026-09-17'), '2026-09-17', 'parseDateInput iso'); eq(D.parseDateInput('31.02.2026'), null, 'parseDateInput ungültig'); eq(D.parseDateInput('abc'), null, 'parseDateInput Müll'); eq(D.parseTimeInput('9:05'), '09:05', 'parseTimeInput h:mm'); eq(D.parseTimeInput('14'), '14:00', 'parseTimeInput nur Stunde'); eq(D.parseTimeInput('9.30'), '09:30', 'parseTimeInput Punkt'); eq(D.parseTimeInput('25:00'), null, 'parseTimeInput ungültig'); eq(D.timeToMinutes('01:30'), 90, 'timeToMinutes'); eq(D.minutesToTime(90), '01:30', 'minutesToTime'); eq(renderMarkdown('# Titel'), 'Titel', 'md h1'); eq(renderMarkdown('## A **b** c'), 'A b c', 'md h2 bold'); eq(renderMarkdown('a *kursiv* b'), 'a kursiv b', 'md italic'); eq(renderMarkdown('snake_case_name'), 'snake_case_name', 'md underscore im Wort bleibt'); eq(renderMarkdown('2 * 3 * 4'), '2 * 3 * 4', 'md einzelne Sterne bleiben'); eq(renderMarkdown('- eins\n - zwei'), ' • eins\n • zwei', 'md verschachtelte Liste'); eq(renderMarkdown('1. a\n2. b'), ' 1. a\n 2. b', 'md nummeriert'); eq(renderMarkdown('- [ ] offen\n- [x] fertig'), ' ☐ offen\n ☑ fertig', 'md checkbox'); eq(renderMarkdown('a `x < y` b'), 'a x < y b', 'md code escaped'); eq(renderMarkdown('`**nicht fett**`'), '**nicht fett**', 'md code schützt Sterne'); eq(renderMarkdown('```\nlet a = 1 & 2;\n```'), 'let a = 1 & 2;', 'md codeblock'); eq(renderMarkdown('[Link](http://x)'), 'Link', 'md link'); eq(renderMarkdown('[[Wiki]]'), 'Wiki', 'md wikilink'); eq(renderMarkdown('![Bild](a.png)'), '[Bild: Bild]', 'md image'); eq(renderMarkdown('> zitat'), '▎ zitat', 'md quote'); eq(renderMarkdown('---'), '────────────────────────────', 'md hr'); eq(renderMarkdown('~~weg~~'), 'weg', 'md strike'); eq(renderMarkdown('a\n\nb'), 'a\n\nb', 'md leere Zeile bleibt'); eq(continueList('- eins'), '- ', 'Liste: nächster Punkt'); eq(continueList(' * eins'), ' * ', 'Liste: Einrückung und Marker bleiben'); eq(continueList('- [x] fertig'), '- [ ] ', 'Liste: Checkbox leer weiter'); eq(continueList('3. drei'), '4. ', 'Liste: Nummer zählt hoch'); eq(continueList('- '), '', 'Liste: leerer Punkt beendet'); eq(continueList('2. '), '', 'Liste: leerer nummerierter Punkt beendet'); eq(continueList('nur Text'), null, 'Liste: kein Marker'); eq(continueList('---'), null, 'Liste: Trennlinie ist keine Liste'); const H = 3600, NOW = 1_800_000_000; eq(fileStamp(NOW), '2027-01-15T08:00:00.000Z', 'fileStamp'); eq(stampToSeconds('2027-01-15T08:00:00.000Z'), NOW, 'stampToSeconds'); const local = new Map([['a/s.jsonl', NOW - 2 * H], ['a/frisch.jsonl', NOW - 10], ['m/MEMORY.md', NOW - 3 * H]]); eq(planPush(local, {'m/MEMORY.md': {updated: fileStamp(NOW - 3 * H), size: 5, hash: 'x'}, 'weg.md': fileStamp(NOW - 5 * H), 'alt.md': null}, NOW, H), {upload: [{id: 'a/s.jsonl', updated: fileStamp(NOW - 2 * H)}], remove: ['weg.md']}, 'planPush: nur ruhende, geänderte Dateien hoch, Verschwundene als Tombstone, Gemeldete nicht nochmal'); const enc = s => new TextEncoder().encode(s); eq(tailHash(enc('abc')), 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad', 'tailHash = SHA-256 bei kleinen Dateien'); eq(tailHash(enc('x'.repeat(70000) + 'abc')), tailHash(enc('y'.repeat(70000) + 'x'.repeat(65533) + 'abc')), 'tailHash sieht nur die letzten 64 KB'); const known = {updated: 'u', size: 4, hash: tailHash(enc('a\nb\n'))}; eq(uploadItem('s', 'v', enc('a\nb\nc\n'), known), {id: 's', updated: 'v', base: {size: 4, hash: known.hash}, append: 'c\n'}, 'uploadItem: nur der Rest, wenn der Anfang stimmt'); eq(uploadItem('s', 'v', enc('x\nb\nc\n'), known), {id: 's', updated: 'v', content: 'x\nb\nc\n'}, 'uploadItem: ganze Datei bei anderem Anfang'); eq(uploadItem('s', 'v', enc('a\n'), known), {id: 's', updated: 'v', content: 'a\n'}, 'uploadItem: ganze Datei, wenn kürzer'); eq(uploadItem('s', 'v', enc('a\nb\nc\n'), 'u'), {id: 's', updated: 'v', content: 'a\nb\nc\n'}, 'uploadItem: alter Stand ohne Größe → ganz'); eq(uploadItem('s', 'v', new Uint8Array([0xff, 0xfe]), null), null, 'uploadItem: kein UTF-8 → nichts'); eq(Array.from(concat(new Uint8Array([1, 2]), new Uint8Array([3]))), [1, 2, 3], 'concat'); eq(planPull([ {id: 'a/s.jsonl', updated: fileStamp(NOW - 1 * H), content: 'x'}, {id: 'a/s.jsonl', updated: fileStamp(NOW - 3 * H), content: 'x'}, {id: 'neu.md', updated: fileStamp(NOW), content: 'y'}, {id: 'm/MEMORY.md', updated: fileStamp(NOW), deleted: true}, {id: 'nie.md', updated: fileStamp(NOW), deleted: true}, ], local).map(p => `${p.file.id}:${p.action}`), ['a/s.jsonl:write', 'a/s.jsonl:skip', 'neu.md:write', 'm/MEMORY.md:delete', 'nie.md:skip'], 'planPull: neuer schreibt, älter überspringt, Tombstone löscht'); eq(batches([1, 2, 3, 4], n => n, 5), [[1, 2], [3], [4]], 'batches nach Größe'); eq(batches([], n => n, 5), [], 'batches leer'); print(failed ? `\n${failed} FEHLER` : '\nalle Tests ok'); if (failed) imports.system.exit(1);