import { describe, expect, it } from 'vitest'; import { validateProject } from '@/domain/validation/engine'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { createEmptyProject, createStandardIntersectionProject } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; /* * Fassung 5.9.0 - derselbe Sachverhalt zweimal im Pruefbericht * (Befunde 14, 35, 36, 37). * * Der Pruefbericht wird gedruckt und traegt eine Fehler- und Warnungsbilanz. * Wo eine Regel der Pruefung und eine Notiz des Planaufbaus DENSELBEN Zustand * melden, steht die Aussage zweimal darin, und die Bilanz nennt eine Ursache * zweimal. Fuer die Vorgabennotizen ist dieses Muster bereits behoben * (istVorgabenNotiz in engine.ts); hier fehlte es an vier weiteren Stellen. */ const DATUM = new Date('2026-01-01T00:00:00Z'); function bericht(project: Project) { return validateProject(project, buildSignalPlan(project), DATUM); } describe('Befund 14 - Signalgruppe ohne Phase steht nur einmal im Bericht', () => { /** Der Musterknotenpunkt, aus dem K1 aus jeder Phase entfernt ist. */ function ohneK1InPhasen(): Project { const roh = createStandardIntersectionProject('Ohne Phase', DATUM); const k1 = roh.signalGroups.find((g) => g.name === 'K1')!; return { ...roh, phases: roh.phases.map((p) => ({ ...p, signalGroupIds: p.signalGroupIds.filter((id) => id !== k1.id), })), }; } it('meldet eine Gruppe ohne jede Freigabe nicht einmal als Fehler und einmal als Warnung', () => { // Sonst liest der Pruefer in derselben Unterlage "Fehler: K1 ist keiner // Phase zugeordnet" UND "Warnung: Signalgruppe ohne Freigabezeit" - eine // Ursache, zwei Zeilen, zwei verschiedene Schweregrade. const treffer = bericht(ohneK1InPhasen()).findings.filter((f) => f.rule.endsWith('ohne-phase')); expect(treffer).toHaveLength(1); expect(treffer[0]!.rule).toBe('signalgruppen.ohne-phase'); expect(treffer[0]!.severity).toBe('fehler'); }); it('laesst die Notiz im Plan stehen - er ist fuer sich vollstaendig', () => { const plan = buildSignalPlan(ohneK1InPhasen()); expect(plan.notes.some((n) => n.code === 'signalgruppe-ohne-phase')).toBe(true); }); }); describe('Befund 37 - Ein Projekt ohne Phasen meldet "Keine Phase angelegt" nur einmal', () => { it('zaehlt fuer ein leeres Projekt zwei Ursachen und nicht drei Fehler', () => { // Ohne die Behebung standen im leeren Projekt zwei wortgleiche // Fehlerzeilen ("Keine Phase angelegt") in zwei verschiedenen Karten, und // die Ausgabe war "gesperrt - 3 Fehler" fuer zwei Ursachen. const ergebnis = bericht(createEmptyProject('Leer', DATUM)); const phasenzeilen = ergebnis.findings.filter((f) => f.title === 'Keine Phase angelegt'); expect(phasenzeilen).toHaveLength(1); expect(phasenzeilen[0]!.rule).toBe('phasen.leer'); expect(ergebnis.findings.map((f) => f.rule)).not.toContain('signalplan.keine-phasen'); expect(ergebnis.errorCount).toBe(2); }); }); describe('Befund 35 - Ein fehlender Raeumweg erzeugt nur einen Befund', () => { /** Der Musterknotenpunkt mit dem Raeumweg 0 m an einer Beziehung. */ function ohneRaeumweg(manualIntergreen: number | null = null): Project { const roh = createStandardIntersectionProject('Ohne Räumweg', DATUM); const k1 = roh.signalGroups.find((g) => g.name === 'K1')!; const k3 = roh.signalGroups.find((g) => g.name === 'K3')!; return { ...roh, conflicts: roh.conflicts.map((c) => c.fromId === k1.id && c.toId === k3.id ? { ...c, clearingDistance: 0, manualIntergreen } : c, ), }; } it('meldet den Raeumweg 0 m als Fehler und nicht zusaetzlich als Warnung aus dem Planaufbau', () => { const ergebnis = bericht(ohneRaeumweg()); const regeln = ergebnis.findings.map((f) => f.rule); expect(regeln).toContain('konflikte.raeumweg-fehlt'); expect(regeln).not.toContain('signalplan.raeumweg-null'); }); it('behaelt die Meldung, wo die Regel der Pruefung schweigt', () => { // Bei einer unbrauchbaren Handvorgabe schweigt konflikte.raeumweg-fehlt // (sie greift nur bei manualIntergreen === null), gerechnet wird aber // trotzdem - und zwar mit 0 m Raeumweg. Dort ist die Notiz die EINZIGE // Meldung dazu und darf nicht verschwinden. const ergebnis = bericht(ohneRaeumweg(-1)); const regeln = ergebnis.findings.map((f) => f.rule); expect(regeln).not.toContain('konflikte.raeumweg-fehlt'); expect(regeln).toContain('signalplan.raeumweg-null'); }); }); describe('Befund 36 - Enger Innenradius neben einer Raeumgeschwindigkeit von Hand', () => { /** K1 biegt ab; an K1 nach F2 stehen Merkmal UND Handeingabe. */ function merkmalUndHandeingabe(manualIntergreen: number | null = null): Project { const roh = createStandardIntersectionProject('Vorrang', DATUM); const k1 = roh.signalGroups.find((g) => g.name === 'K1')!; const f2 = roh.signalGroups.find((g) => g.name === 'F2')!; return { ...roh, signalGroups: roh.signalGroups.map((g) => g.id === k1.id ? { ...g, movement: 'rechts' as const } : g, ), conflicts: roh.conflicts.map((c) => c.fromId === k1.id && c.toId === f2.id ? { ...c, engerRadius: true, clearingSpeedOverride: 9, manualIntergreen } : c, ), }; } it('meldet den Vorrang der Handeingabe genau einmal', () => { const treffer = bericht(merkmalUndHandeingabe()).findings.filter((f) => f.rule.includes('enger-radius'), ); expect(treffer).toHaveLength(1); expect(treffer[0]!.rule).toBe('zwischenzeiten.enger-radius-ohne-wirkung'); expect(treffer[0]!.category).toBe('zwischenzeiten'); }); it('behaelt den Satz zur Verkuerzung, der bisher nur an der Plan-Notiz stand', () => { // Die verbleibende Meldung muss sagen, in welche Richtung die Handeingabe // wirkt. 9 m/s gegen 5 m/s des Merkmals: Die Zwischenzeit faellt KUERZER // aus - die gefaehrliche Richtung, und ohne diesen Satz stuende sie // nirgends mehr im Bericht. const treffer = bericht(merkmalUndHandeingabe()).findings.find( (f) => f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung', ); expect(treffer?.message).toContain('KÜRZER'); }); it('behauptet neben einer von Hand gesetzten Zwischenzeit keine Verkuerzung', () => { // Merkmal, Raeumgeschwindigkeit 9 m/s UND eine Zwischenzeit von 20 s von // Hand: Die Vorgabe ersetzt die Rechnung vollstaendig, es geht WEDER die // eingetragene Raeumgeschwindigkeit NOCH die des Merkmals in eine Zahl // ein. Der Satz zur Richtung sagte hier trotzdem, die Zwischenzeit falle // KÜRZER aus - eine Aussage ueber eine Zwischenzeit, die niemand // gerechnet hat, und sie steht in der gedruckten Planunterlage. const treffer = bericht(merkmalUndHandeingabe(20)).findings.find( (f) => f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung', ); expect(treffer?.message).not.toContain('KÜRZER'); expect(treffer?.message).not.toContain('kürzer'); // Stattdessen muss dort stehen, was tatsaechlich angesetzt wird. expect(treffer?.message).toContain('20 s'); }); });