import { describe, expect, it } from 'vitest'; import { validateProject } from '@/domain/validation/engine'; import { buildSignalPlan, intergreenKey } from '@/domain/plan/signalPlan'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import type { Conflict, Project } from '@/domain/model/project'; const STICHTAG = new Date('2026-01-01T00:00:00Z'); /** Regelknotenpunkt, in dem genau eine Kfz-Beziehung abgewandelt ist. */ function mitKonflikt(aenderung: Partial): { projekt: Project; konflikt: Conflict } { const roh = createStandardIntersectionProject('Schranken', STICHTAG); const [k1, , k3] = roh.signalGroups; const ziel = roh.conflicts.find((c) => c.fromId === k1!.id && c.toId === k3!.id)!; const geaendert = { ...ziel, ...aenderung }; return { konflikt: geaendert, projekt: { ...roh, conflicts: roh.conflicts.map((c) => (c.id === ziel.id ? geaendert : c)), meta: { ...roh.meta, projectNumber: 'VP-2026-001', client: 'Stadt Musterhausen', planner: 'O. Willerding', location: 'Schranken', }, }, }; } describe('Schranken der Zwischenzeit lassen sich nicht ueber ein Nachbarfeld umgehen', () => { /* * Die vorgegebene Zwischenzeit ersetzt die Rechnung vollstaendig - * computeIntergreen wurde gar nicht erst gerufen. Damit liefen alle Schranken * ins Leere, die fuer die Einzelwerte gelten. In der Zwischenzeitenansicht * stehen beide Felder nebeneinander: Wer kuerzen wollte, nahm das Feld ohne * Schranke. */ it('meldet eine vorgegebene Zwischenzeit unter dem Rechenwert', () => { const { projekt: ohne, konflikt: ungeaendert } = mitKonflikt({}); const gerechnet = buildSignalPlan(ohne).intergreens.get(intergreenKey(ungeaendert.fromId, ungeaendert.toId)) ?.value ?? 0; expect(gerechnet).toBeGreaterThan(0); const { projekt, konflikt } = mitKonflikt({ manualIntergreen: gerechnet - 3 }); const plan = buildSignalPlan(projekt); expect(plan.intergreens.get(intergreenKey(konflikt.fromId, konflikt.toId))?.value).toBe( gerechnet - 3, ); const meldung = plan.notes.find((n) => n.code === 'zwischenzeit-vorgabe-unter-rechenwert'); expect(meldung, 'Die Unterschreitung darf nicht stillschweigend bleiben').toBeDefined(); expect(meldung!.severity).toBe('warnung'); // Beide Zahlen muessen im Text stehen, sonst ist die Abweichung nicht zu // beziffern. expect(meldung!.message).toContain(`${gerechnet - 3} s`); expect(meldung!.message).toContain(`${gerechnet} s`); }); it('schweigt, wenn die Vorgabe den Rechenwert erreicht oder ueberschreitet', () => { const { projekt: ohne, konflikt: ungeaendert } = mitKonflikt({}); const gerechnet = buildSignalPlan(ohne).intergreens.get(intergreenKey(ungeaendert.fromId, ungeaendert.toId)) ?.value ?? 0; for (const vorgabe of [gerechnet, gerechnet + 2]) { const { projekt } = mitKonflikt({ manualIntergreen: vorgabe }); expect( buildSignalPlan(projekt).notes.some( (n) => n.code === 'zwischenzeit-vorgabe-unter-rechenwert', ), `Vorgabe ${vorgabe}`, ).toBe(false); } }); it('vergleicht nicht, solange kein Raeumweg erfasst ist - meldet den Fall aber', () => { /* * BERICHTIGUNG (Befund 19, Fassung 5.10.0): Hier stand * "Die fehlende Vermassung meldet bereits konflikte.raeumweg-fehlt". Das * war nachweislich falsch - jene Regel greift nur bei * `manualIntergreen === null` (engine.ts), also gerade nicht in diesem * Fall. Der Testfall hielt damit fest, dass zu "Handvorgabe gesetzt und * Raeumweg 0 m" ueberhaupt nichts gemeldet wird, und begruendete es mit * einer Meldung, die es dort nie gab: Der Eintrag im Nachbarfeld hob die * Ausgabesperre auf - unter der Ueberschrift dieses describe, die das * Gegenteil zusagt. * * Ohne Vergleichsgrundlage NICHT ZU VERGLEICHEN bleibt richtig; gerechnet * wird ohne Raeumweg nichts, was sich gegenhalten liesse. Gemeldet werden * muss der Zustand trotzdem, und zwar als Fehler - das leistet jetzt * 'konflikte.vorgabe-ohne-raeumweg'. */ const { projekt, konflikt } = mitKonflikt({ manualIntergreen: 1, clearingDistance: 0 }); expect( buildSignalPlan(projekt).notes.some( (n) => n.code === 'zwischenzeit-vorgabe-unter-rechenwert', ), ).toBe(false); const bericht = validateProject(projekt, buildSignalPlan(projekt), STICHTAG); const befund = bericht.findings.find((f) => f.rule === 'konflikte.vorgabe-ohne-raeumweg'); expect(befund, 'Die Vorgabe ohne Vergleichsgrundlage gehoert in den Bericht').toBeDefined(); expect(befund!.severity).toBe('fehler'); expect(befund!.target?.id).toBe(konflikt.id); expect(bericht.exportBlocked).toBe(true); }); it('fuehrt Meldungen der Zwischenzeitrechnung auf ihre Konfliktbeziehung zurueck', () => { // Zuvor zeigten alle Meldungen aus dem Planaufbau pauschal auf das // Programm; der Sprungknopf im Pruefbericht fuehrte damit in die // Phasenansicht, wo sich an einem Raeumweg nichts aendern laesst. const { projekt, konflikt } = mitKonflikt({ crossingTimeOverride: 0 }); const bericht = validateProject(projekt, buildSignalPlan(projekt), STICHTAG); const befund = bericht.findings.find( (f) => f.rule === 'signalplan.ueberfahrzeit-unter-gelbzeitschranke', ); expect(befund, 'Die gekappte Ueberfahrzeit muss im Bericht stehen').toBeDefined(); expect(befund!.severity).toBe('fehler'); expect(befund!.category).toBe('zwischenzeiten'); expect(befund!.target?.kind).toBe('konflikt'); expect(befund!.target?.id).toBe(konflikt.id); expect(befund!.target?.label).toBe('K1 nach K3'); // Eigene Ueberschrift statt des Sammeltitels "Hinweis aus dem Planaufbau". expect(befund!.title).not.toContain('Hinweis aus dem Planaufbau'); }); });