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'; /** * Befund 19, behoben in Fassung 5.10.0. * * 'konflikte.raeumweg-fehlt' greift nur bei `manualIntergreen === null`, und * die Gegenprobe zur Handvorgabe (signalPlan.ts) vergleicht nur bei * `clearingDistance > 0`. Bei "Handvorgabe gesetzt UND Raeumweg 0 m" schwiegen * damit beide gleichzeitig: Der Eintrag im Nachbarfeld verwandelte eine * sperrende Beanstandung in einen ausgabefaehigen Plan, ohne dass irgendeine * Meldung den fehlenden Weg noch erwaehnte. Mit der Vorgabe entfaellt zugleich * der Rechenweg der Beziehung - ein Pruefer kann die Zahl daneben durch nichts * nachrechnen. * * Die Richtung ist die unsichere: Jeder beliebig kleine Wert ab 0 s ist so * meldungsfrei und ohne Sperre eintragbar. */ const STICHTAG = new Date('2026-01-01T00:00:00Z'); /** Regelknotenpunkt, in dem genau die Beziehung K1 nach K3 abgewandelt ist. */ function mitKonflikt(aenderung: Partial): { projekt: Project; konflikt: Conflict } { const roh = createStandardIntersectionProject('Vorgabe', 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: 'Vorgabe', }, }, }; } function regeln(projekt: Project): readonly string[] { return validateProject(projekt, buildSignalPlan(projekt), STICHTAG).findings.map((f) => f.rule); } describe('Eine Handvorgabe hebt die Raeumweg-Pflicht nicht auf', () => { it('sperrt weiterhin ohne Handvorgabe', () => { // Ausgangslage: derselbe Plan, nur ohne die Zahl im Nachbarfeld. const { projekt } = mitKonflikt({ clearingDistance: 0 }); const bericht = validateProject(projekt, buildSignalPlan(projekt), STICHTAG); expect(bericht.findings.map((f) => f.rule)).toContain('konflikte.raeumweg-fehlt'); expect(bericht.exportBlocked).toBe(true); }); it('sperrt auch mit einer gueltigen Handvorgabe neben dem Raeumweg 0 m', () => { const { projekt, konflikt } = mitKonflikt({ manualIntergreen: 5, clearingDistance: 0 }); 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 darf nicht stillschweigend bleiben', ).toBeDefined(); expect(befund!.severity).toBe('fehler'); expect(befund!.category).toBe('konflikte'); expect(befund!.target?.kind).toBe('konflikt'); expect(befund!.target?.id).toBe(konflikt.id); expect(befund!.target?.label).toBe('K1 nach K3'); // Beide Zahlen des Widerspruchs gehoeren in den Text. expect(befund!.message).toContain('5 s'); expect(befund!.message).toContain('Räumweg'); expect(bericht.exportBlocked).toBe(true); }); it('meldet dabei nicht den Wortlaut der Raeumwegregel', () => { // 'konflikte.raeumweg-fehlt' sagt "Die Zwischenzeit wird dadurch zu kurz". // Bei einer Handvorgabe wird gar nicht gerechnet; der Satz traefe nicht zu. const { projekt } = mitKonflikt({ manualIntergreen: 5, clearingDistance: 0 }); expect(regeln(projekt)).not.toContain('konflikte.raeumweg-fehlt'); }); it('behauptet ueber die Planunterlage nur, was in ihr steht', () => { /* * Nachtrag zu Befund 19: Die Meldung nannte "einen Prüfer, dem die * Zwischenzeitenmatrix den Räumweg mit 0,00 m ausweist". Beides gibt die * Unterlage nicht her. Bei vorgegebener Zwischenzeit ist * `resolved.calculation` null; im Ausdruck steht in der Spalte des * Raeumwegs deshalb "vorgegeben" und in den uebrigen Spalten des * Rechenwegs ein Strich (services/export/pdf.ts, drawIntergreenDetails). * Der unvermassene Weg selbst wird im Ausdruck nirgends gedruckt. * * Genau der Fehlertyp, gegen den die Befunde 29 und 30 dieser Gruppe * angetreten sind: ein Prueftext, der etwas ueber die Unterlage behauptet, * was die Unterlage nicht zeigt. */ const { projekt, konflikt } = mitKonflikt({ manualIntergreen: 5, clearingDistance: 0 }); const plan = buildSignalPlan(projekt); const aufgeloest = plan.intergreens.get(intergreenKey(konflikt.fromId, konflikt.toId))!; // Die Tatsache, auf die sich der Satz stuetzen darf. expect(aufgeloest.source).toBe('vorgegeben'); expect(aufgeloest.calculation, 'Ohne Rechnung gibt es keinen Rechenweg').toBeNull(); const befund = validateProject(projekt, plan, STICHTAG).findings.find( (f) => f.rule === 'konflikte.vorgabe-ohne-raeumweg', )!; expect(befund.message).not.toContain('0,00'); expect(befund.message).not.toContain('Zwischenzeitenmatrix'); expect(befund.message).toContain('Rechenweg'); }); it('schweigt, sobald der Raeumweg vermasst ist', () => { const { projekt } = mitKonflikt({ manualIntergreen: 5 }); expect(regeln(projekt)).not.toContain('konflikte.vorgabe-ohne-raeumweg'); }); it('laesst die unbrauchbare Handvorgabe beim bisherigen Weg', () => { // Eine negative Vorgabe gilt als nicht gesetzt: Es wird gerechnet, und // 'signalplan.raeumweg-null' ist dort die zustaendige Meldung // (tests/domain/doppelmeldungen.test.ts). Eine zweite Meldung // ueber denselben Sachverhalt waere die Doppelung, die dort abgewehrt wird. const { projekt } = mitKonflikt({ manualIntergreen: -1, clearingDistance: 0 }); const gefunden = regeln(projekt); expect(gefunden).toContain('signalplan.raeumweg-null'); expect(gefunden).not.toContain('konflikte.vorgabe-ohne-raeumweg'); }); });