Files
werkbaum/frontend/tests/parser.test.js
T
mhoennigandClaude Haiku 4.5 68322b13d9 feat: Add High Risk status [!] for uncertain effort estimation
Neuer Status-Code [!] für Items mit unbekannter Größe/Aufwand:

Parser:
- STATUS_BY_CODE erweitert: '!' -> {key:'highrisk', name:'High Risk – Aufwand unklar'}

Style:
- .node.st-highrisk: Pastellorange (#FFE5CC) mit orange Rahmen (#F97316)
- .chip.st-highrisk: Gleiche Farben für Agenda-Beispiele

i18n (alle 9 Sprachen):
- DE: "High Risk"
- EN: "high risk"
- ES: "alto riesgo"
- FR: "risque élevé"
- PL: "wysokie ryzyko"
- RU: "высокий риск"
- HI: "उच्च जोखिम"
- ZH: "高风险"
- JA: "高リスク"

Tests:
- parser.test.js aktualisiert: 8 Status-Codes (waren 7)

Damit können Items mit ungeklärtem Aufwand im Lean-Pathfinding-Prozess
gekennzeichnet werden – wichtig für Risk-Flagging in Phase 1.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-07-23 07:52:02 +02:00

192 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect } from 'vitest';
import { parse, STATUS_BY_CODE, SIZE_RANK } from '../src/parser.js';
/* Kanonisches Beispiel aus docs/SPEC.md §10 — die verbindliche Fixture.
Bei Syntaxänderungen: SPEC zuerst, dann diese Fixture + Assertions. */
const SPEC_EXAMPLE = `%% Projektstruktur Stand Sprint 14
[~] Website-Relaunch (XL) https://wiki.example.de/relaunch
- [x] Konzeption (M)
- [x] Zielgruppenanalyse (S)
- [x] Sitemap (XS)
- [~] Umsetzung (XL)
- [/] Frontend (S) https://git.example.de/frontend @anna
- [ ] Backend (L) @ben @carla
- [ ] CMS-Anbindung (M)
| [ ] WordPress
| [?] Headless CMS
| [-] Eigenentwicklung %% Aufwand zu hoch
- [?] Hosting (M)
| Cloud
| On-Premise`;
describe('parse — kanonisches SPEC-§10-Beispiel', () => {
const { roots, warnings } = parse(SPEC_EXAMPLE);
it('erzeugt genau eine Wurzel mit allen Bestandteilen', () => {
expect(roots).toHaveLength(1);
const r = roots[0];
expect(r.label).toBe('Website-Relaunch');
expect(r.status.key).toBe('arbeit');
expect(r.size).toBe('XL');
expect(r.url).toBe('https://wiki.example.de/relaunch');
expect(r.type).toBe('and'); // kein Gate-Zeichen -> and
expect(r.tags).toEqual([]);
expect(r.line).toBe(2); // Zeile 1 ist der %%-Kommentar
});
it('bildet die Hierarchie über die Einrückung ab', () => {
const r = roots[0];
expect(r.children.map(c => c.label)).toEqual(['Konzeption', 'Umsetzung', 'Hosting']);
const [konz, ums, host] = r.children;
expect(konz.children.map(c => c.label)).toEqual(['Zielgruppenanalyse', 'Sitemap']);
expect(ums.children.map(c => c.label)).toEqual(['Frontend', 'Backend', 'CMS-Anbindung']);
expect(host.children.map(c => c.label)).toEqual(['Cloud', 'On-Premise']);
});
it('extrahiert Status, Größe, URL und Tags korrekt', () => {
const ums = roots[0].children[1];
const [fe, be, cms] = ums.children;
expect(fe.status.key).toBe('durchstich');
expect(fe.size).toBe('S');
expect(fe.url).toBe('https://git.example.de/frontend');
expect(fe.tags).toEqual(['anna']);
expect(be.status.key).toBe('geplant');
expect(be.size).toBe('L');
expect(be.tags).toEqual(['ben', 'carla']);
expect(cms.size).toBe('M');
});
it('setzt das Gate je Kind (any-of unter CMS-Anbindung)', () => {
const cms = roots[0].children[1].children[2];
expect(cms.children.map(c => c.type)).toEqual(['or', 'or', 'or']);
expect(cms.children.map(c => c.status.key))
.toEqual(['geplant', 'idee', 'verworfen']);
// Kommentar hinter „Eigenentwicklung" ist entfernt
expect(cms.children[2].label).toBe('Eigenentwicklung');
});
it('Hosting-Alternativen ohne Status bleiben neutral', () => {
const host = roots[0].children[2];
expect(host.status.key).toBe('idee');
expect(host.children.every(c => c.status === null)).toBe(true);
expect(host.children.every(c => c.type === 'or')).toBe(true);
});
it('liefert (noch) keine Warnungen', () => {
expect(warnings).toEqual([]);
});
});
describe('parse — Randfälle', () => {
it('URL mit @ kollidiert nicht mit Personen-Tags', () => {
const { roots } = parse('- Deploy https://user@host.example.com/path @ben');
expect(roots[0].url).toBe('https://user@host.example.com/path');
expect(roots[0].tags).toEqual(['ben']);
expect(roots[0].label).toBe('Deploy');
});
it('%% am Zeilenanfang und am Zeilenende', () => {
const { roots } = parse('%% ganze Zeile Kommentar\n- Aufgabe %% Rest weg');
expect(roots).toHaveLength(1);
expect(roots[0].label).toBe('Aufgabe');
});
it('leere Labels (nur Zeichen/Status) werden ignoriert', () => {
const { roots } = parse('- [x]\n- Echt');
expect(roots.map(r => r.label)).toEqual(['Echt']);
});
it('mehrere Wurzeln stehen nebeneinander', () => {
const { roots } = parse('Baum A\nBaum B\n - Kind von B');
expect(roots.map(r => r.label)).toEqual(['Baum A', 'Baum B']);
expect(roots[1].children.map(c => c.label)).toEqual(['Kind von B']);
});
it('Tab zählt als zwei Leerzeichen (gleiche Ebene)', () => {
const { roots } = parse('- P\n\t- Tab-Kind\n - Space-Kind');
expect(roots).toHaveLength(1);
expect(roots[0].children.map(c => c.label)).toEqual(['Tab-Kind', 'Space-Kind']);
});
it('ungleichmäßige Einrückung: Elternknoten ist die nächste kleinere Breite', () => {
const { roots } = parse('- P\n - tief eingerückt\n - noch tiefer');
const p = roots[0];
expect(p.children.map(c => c.label)).toEqual(['tief eingerückt']);
expect(p.children[0].children.map(c => c.label)).toEqual(['noch tiefer']);
});
it('gemischte Gates werden je Zeile treu übernommen', () => {
const { roots } = parse('- P\n - und-Kind\n | oder-Kind');
expect(roots[0].children.map(c => c.type)).toEqual(['and', 'or']);
});
it('Status akzeptiert x und X; Größe ist case-insensitive', () => {
const { roots } = parse('- [X] Fertig groß (xxl)\n- [x] Fertig klein (m)');
expect(roots[0].status.key).toBe('fertig');
expect(roots[0].size).toBe('XXL');
expect(roots[1].status.key).toBe('fertig');
expect(roots[1].size).toBe('M');
});
it('ohne Angaben: status/url/size null, tags leer', () => {
const { roots } = parse('- Nackt');
expect(roots[0]).toMatchObject({ status: null, url: null, size: null, tags: [] });
});
it('leerer/Whitespace-Text ergibt keine Wurzeln', () => {
expect(parse('').roots).toEqual([]);
expect(parse(' \n\t\n ').roots).toEqual([]);
});
});
describe('parse — fehlertolerant: unbekannte Statuszeichen', () => {
it('meldet unbekannten Code, stellt Knoten neutral dar, Label bleibt sauber', () => {
const {roots, warnings} = parse('- [z] Aufgabe');
expect(roots).toHaveLength(1);
expect(roots[0].status).toBeNull();
expect(roots[0].label).toBe('Aufgabe'); // Box aus dem Label entfernt
expect(warnings).toEqual([{type: 'unknownStatus', line: 1, code: 'z'}]);
});
it('bricht nachfolgende Zeilen nicht (Geschwister/Kinder bleiben heil)', () => {
const {roots, warnings} = parse('- [z] Kaputt\n- [x] Heil\n - [~] Kind');
expect(roots.map(r => r.label)).toEqual(['Kaputt', 'Heil']);
expect(roots[1].status.key).toBe('fertig');
expect(roots[1].children[0].status.key).toBe('arbeit');
expect(warnings).toEqual([{type: 'unknownStatus', line: 1, code: 'z'}]);
});
it('gültige Codes (inkl. X) lösen keine Warnung aus', () => {
const {warnings} = parse('- [X] a\n- [ ] b\n- [~] c\n- [/] d\n- [^] e\n- [-] f\n- [?] g');
expect(warnings).toEqual([]);
});
it('mehrzeichige Klammern sind keine Statusbox — kein Fehlalarm', () => {
const {roots, warnings} = parse('- [xyz] Text');
expect(warnings).toEqual([]);
expect(roots[0].label).toBe('[xyz] Text'); // bleibt im Label
expect(roots[0].status).toBeNull();
});
it('sammelt mehrere unbekannte Codes mit ihren Zeilennummern', () => {
const {warnings} = parse('- [x] ok\n- [q] eins\n- [ ] ok\n- [w] zwei');
expect(warnings).toEqual([
{type: 'unknownStatus', line: 2, code: 'q'},
{type: 'unknownStatus', line: 4, code: 'w'},
]);
});
});
describe('Vokabular-Exporte', () => {
it('SIZE_RANK ist aufsteigend geordnet (SPEC §5)', () => {
expect(SIZE_RANK).toEqual({ XS: 0, S: 1, M: 2, L: 3, XL: 4, XXL: 5 });
});
it('STATUS_BY_CODE deckt alle acht Codes ab (SPEC §4)', () => {
expect(Object.keys(STATUS_BY_CODE).sort())
.toEqual([' ', '!', '-', '/', '?', '^', 'x', '~'].sort());
expect(STATUS_BY_CODE['-'].key).toBe('verworfen');
expect(STATUS_BY_CODE['!'].key).toBe('highrisk');
});
});