import { describe, expect, it } from 'vitest'; import { UMLAUFZEIT_ERSATZWERT_VERMERK, buildSignalPlan, mindestfreigabezeitDerGruppe, umlaufzeitIstErsatzwert, type PlanNote, } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation/engine'; import { computeIntergreen } from '@/domain/rilsa/intergreen'; import { compareCycleTimeMethods, computeCycleTime } from '@/domain/rilsa/cycle'; import { resolveRilsaSettings, settingFields } from '@/domain/rilsa/settings'; import { createConflict, createEmptyProject, createPhase, createSignalGroup, createStandardIntersectionProject, } from '@/domain/model/factory'; import { parseProject } from '@/domain/model/schema'; import type { Anlagenart, Project, SignalGroup } from '@/domain/model/project'; import type { ClearingStream, EnteringStream, VehicleClass } from '@/domain/rilsa/types'; import { ANLAGENART_GRENZEN, RILSA_DEFAULTS, SAFETY_FLOORS } from '@/domain/rilsa/constants'; /* * Exportsperre und stille Ersetzungen - die Fachkern-Befunde C8 bis C11: * * C8 Unbekannte Fahrzeugart setzte stumm 0 m Laengenzuschlag an und * verkuerzte die Zwischenzeit. Jetzt Fehler-Notiz und 6 m (RiLSA- * Rechenlaenge Kfz); Fussgaenger 0 m ohne Notiz. * C9 Eine Mindestfreigabezeit-Vorgabe unter dem Regelwert wurde still auf * den Regelwert gehoben; der Wert blieb im Feld. Jetzt Warnung * "eingetragen 3 s, massgebend bleibt 5 s". * C10 resolveRilsaSettings wertete grenzeVonArt nie aus: Umlaufzeit-Vorgaben * passierten ungeprueft. Jetzt Schranken der Anlagenart (120 s * Knotenpunkt, 300 s einstreifig) - und ALLE Aufrufer reichen sie durch. * C11 Uebersaettigung lieferte bounded 'maximum' und 120 s wie ein * Ergebnis. Jetzt bounded 'uebersaettigt', der Plan traegt den Zustand. * * Mutationsnachweis: Die Kernfaelle unten schlagen gegen den Altstand fehl * (die Notiz gab es nicht, der Wert war 0 m, die Vorgabe wurde nicht gekappt * bzw. am falschen Ort gekappt, bounded war 'maximum'). Einige Faelle sind * bewusst Waechter, die auch am Altstand bestehen - sie halten fest, was sich * NICHT aendern darf: Fussgaenger bleiben bei 0 m, bekannte Fahrzeugarten * schweigen, das Schema bleibt der erste Riegel, eine Vorgabe in Hoehe des * Regelwerts schweigt, 250 s bleiben bei einstreifiger Fuehrung ungekappt, * 'maximum' bleibt dem rechenbaren Umlauf. (Vor der Fassung 5.4.0 behauptete * dieser Kommentar "jeder Fall".) */ const DATUM = new Date('2026-01-01T00:00:00Z'); /** Ein raeumender Kfz-Strom, wie in tests/domain/intergreen.test.ts. */ const kfzClearing = (overrides: Partial = {}): ClearingStream => ({ mode: 'kfz', movement: 'geradeaus', clearingDistance: 22, vehicleClass: 'pkw', vZul: 50, ...overrides, }); const kfzEntering = (overrides: Partial = {}): EnteringStream => ({ mode: 'kfz', movement: 'geradeaus', enteringDistance: 8, ...overrides, }); /** Eine Fahrzeugart, die nur aus einer fremden oder alten Datei stammen kann. */ const UNBEKANNT = 'traktor' as VehicleClass; /** Zwei Kfz-Gruppen in zwei Phasen; die zweite Gruppe laesst sich anpassen. */ function zweiGruppen( zweite: Partial = {}, anlagenart: Anlagenart = 'knotenpunkt', rilsa: Project['settings']['rilsa'] = {}, ): Project { const basis = createEmptyProject('Zwei Gruppen', DATUM, anlagenart); const a = createSignalGroup({ name: 'K1', mode: 'kfz', index: 0 }); const b: SignalGroup = { ...createSignalGroup({ name: 'K2', mode: 'kfz', index: 1 }), ...zweite }; const phase1 = createPhase('Phase 1', [a.id]); const phase2 = createPhase('Phase 2', [b.id]); return { ...basis, signalGroups: [a, b], conflicts: [ { ...createConflict(a.id, b.id), clearingDistance: 22, enteringDistance: 8 }, { ...createConflict(b.id, a.id), clearingDistance: 22, enteringDistance: 8 }, ], phases: [phase1, phase2], program: { ...basis.program, phaseOrder: [phase1.id, phase2.id] }, settings: { ...basis.settings, rilsa }, }; } /** Kfz-Strom und Furt (14 m Furtlaenge -> Furtbedarf 6 s), Furt anpassbar. */ function furtProjekt(furt: Partial = {}): Project { const basis = createEmptyProject('Furt', DATUM); const k = createSignalGroup({ name: 'K1', mode: 'kfz', index: 0 }); const f: SignalGroup = { ...createSignalGroup({ name: 'F1', mode: 'fuss', index: 1 }), ...furt }; const phaseK = createPhase('Kfz', [k.id]); const phaseF = createPhase('Fuss', [f.id]); return { ...basis, signalGroups: [k, f], conflicts: [ { ...createConflict(f.id, k.id), clearingDistance: 14, enteringDistance: 3 }, { ...createConflict(k.id, f.id), clearingDistance: 15, enteringDistance: 5 }, ], phases: [phaseK, phaseF], program: { ...basis.program, phaseOrder: [phaseK.id, phaseF.id] }, }; } /** Der Musterknotenpunkt mit Verkehrsstaerken weit ueber der Kapazitaet. */ function uebersaettigt(anlagenart: Anlagenart = 'knotenpunkt'): Project { const roh = createStandardIntersectionProject('Uebersaettigt', DATUM); return { ...roh, anlagenart, demands: roh.signalGroups .filter((g) => g.mode === 'kfz') .map((g) => ({ signalGroupId: g.id, volume: 1800, heavyVehicleShare: 0.1 })), }; } function notizen(project: Project, code: string): PlanNote[] { return buildSignalPlan(project).notes.filter((n) => n.code === code); } // --- C8 ---------------------------------------------------------------------- describe('C8: unbekannte Fahrzeugart - kein stiller Rueckfall auf 0 m', () => { it('setzt die RiLSA-Rechenlaenge 6 m an und meldet einen Fehler', () => { const result = computeIntergreen(kfzClearing({ vehicleClass: UNBEKANNT }), kfzEntering()); expect(result.vehicleLength).toBe(SAFETY_FLOORS.minVehicleLengthKfz); expect(result.vehicleLength).toBe(6); expect(result.clearingPath).toBe(28); const notiz = result.notes.find((n) => n.code === 'fahrzeugart-unbekannt'); expect(notiz).toBeDefined(); expect(notiz!.severity).toBe('fehler'); expect(notiz!.message).toContain('traktor'); expect(notiz!.message).toContain('unbekannt'); expect(notiz!.message).toContain('6 m'); }); it('rechnet damit dieselbe Zwischenzeit wie ein Pkw - nicht eine Sekunde kuerzer', () => { // Altstand: 0 m -> sr = 22 m -> tr = 2,2 s -> 3 + 2,2 - 0,72 = 4,48 s -> 5 s. // Jetzt: 6 m -> sr = 28 m -> tr = 2,8 s -> 5,08 s -> 6 s, wie beim Pkw. const pkw = computeIntergreen(kfzClearing(), kfzEntering()); const unbekannt = computeIntergreen(kfzClearing({ vehicleClass: UNBEKANNT }), kfzEntering()); expect(pkw.intergreen).toBe(6); expect(unbekannt.intergreen).toBe(6); expect(unbekannt.clearingTime).toBeCloseTo(pkw.clearingTime, 6); }); it('laesst Fussgaenger bei 0 m - dort gibt es keinen Laengenzuschlag, also auch keine Notiz', () => { const result = computeIntergreen( { mode: 'fuss', movement: 'querung', clearingDistance: 12, vehicleClass: UNBEKANNT, vZul: 0, }, kfzEntering(), ); expect(result.vehicleLength).toBe(0); expect(result.notes.some((n) => n.code === 'fahrzeugart-unbekannt')).toBe(false); }); it('meldet bekannte Fahrzeugarten nicht', () => { for (const art of ['pkw', 'lkw', 'bus', 'lastzug', 'strassenbahn', 'rad', 'keine'] as const) { const result = computeIntergreen(kfzClearing({ vehicleClass: art }), kfzEntering()); expect( result.notes.some((n) => n.code === 'fahrzeugart-unbekannt'), art, ).toBe(false); /* * NACHGEZOGEN (Fassung 5.9.0): Hier stand * `toBe(RILSA_DEFAULTS.vehicleLength[art])` fuer ALLE sieben Arten - auch * fuer "Fahrrad" und "ohne Laengenzuschlag", die mit 0 m im Kennwertsatz * stehen. Damit schrieb diese Zeile am KFZ-Strom (kfzClearing) genau das * Verhalten fest, das seit der Fassung 5.9.0 als Mangel gilt: Der * Raeumweg blieb ohne Fahrzeuglaenge, und die Zwischenzeit fiel um rund * eine Sekunde kuerzer aus, waehrend die Vorgabenverwaltung an jedem * Kfz-Laengenfeld SAFETY_FLOORS.minVehicleLengthKfz erzwingt. Der * Gegenstand DIESES Falles ist die Zeile darueber - die unbekannte Art -, * und die bleibt unveraendert: Die neue Meldung heisst * 'fahrzeugart-laengenzuschlag-zu-kurz' und ist eine eigene. */ const erwartet = Math.max( RILSA_DEFAULTS.vehicleLength[art], SAFETY_FLOORS.minVehicleLengthKfz, ); expect(result.vehicleLength, art).toBe(erwartet); } }); it('traegt die Meldung mit Konfliktbeziehung in den Plan und als Fehler in den Bericht', () => { const projekt = zweiGruppen({ vehicleClass: UNBEKANNT }); const [notiz] = notizen(projekt, 'fahrzeugart-unbekannt'); expect(notiz).toBeDefined(); expect(notiz!.severity).toBe('fehler'); expect(notiz!.konfliktId).toBeDefined(); expect(notiz!.message).toContain('Zwischenzeit K2 nach K1'); const bericht = validateProject(projekt, undefined, DATUM); const befund = bericht.findings.find((f) => f.rule === 'signalplan.fahrzeugart-unbekannt'); expect(befund).toBeDefined(); expect(befund!.severity).toBe('fehler'); expect(befund!.category).toBe('zwischenzeiten'); expect(befund!.target).toMatchObject({ kind: 'konflikt' }); expect(befund!.title).toContain('Fahrzeugart unbekannt'); expect(befund!.title).toContain('6 m'); expect(befund!.reference).toContain('Zwischenzeiten'); expect(befund!.suggestion).toContain('Fahrzeugart'); expect(bericht.exportBlocked).toBe(true); }); it('ist der zweite Riegel: das Schema setzt Unbekanntes beim Einlesen auf Pkw und meldet es', () => { const roh = JSON.parse(JSON.stringify(zweiGruppen({ vehicleClass: UNBEKANNT }))) as Record< string, unknown >; const { project, issues } = parseProject(roh); const k2 = project.signalGroups.find((g) => g.name === 'K2')!; expect(k2.vehicleClass).toBe('pkw'); expect( issues.some((i) => i.path.endsWith('vehicleClass') && i.message.includes('traktor')), ).toBe(true); // Und mit dem reparierten Projekt schweigt die Zwischenzeitrechnung. expect(notizen(project, 'fahrzeugart-unbekannt')).toHaveLength(0); }); }); // --- C9 ---------------------------------------------------------------------- describe('C9: Mindestfreigabezeit-Vorgabe unter dem Regelwert wird gemeldet', () => { it('meldet "eingetragen 3 s, massgebend bleibt 5 s" als Warnung an der Gruppe', () => { const projekt = zweiGruppen({ minGreenOverride: 3 }); const k2 = projekt.signalGroups.find((g) => g.name === 'K2')!; const mfz = mindestfreigabezeitDerGruppe(projekt, k2, RILSA_DEFAULTS); expect(mfz.vorgabe).toBe(3); expect(mfz.wert).toBe(5); expect(mfz.massgebend).toBe('regelwert'); const plan = buildSignalPlan(projekt); const geplant = plan.groups.find((g) => g.groupId === k2.id)!; expect(geplant.times.minGreen).toBe(5); const [notiz] = plan.notes.filter( (n) => n.code === 'mindestfreigabezeit-vorgabe-unterschritten', ); expect(notiz).toBeDefined(); expect(notiz!.severity).toBe('warnung'); expect(notiz!.signalGroupId).toBe(k2.id); expect(notiz!.message).toContain('"K2"'); expect(notiz!.message).toContain('3 s eingetragen'); expect(notiz!.message).toContain('Regelwert der Verkehrsart von 5 s'); expect(notiz!.message).toContain('Maßgebend bleibt 5 s'); expect(notiz!.message).toContain('keine Wirkung'); }); it('steht im Pruefbericht unter Signalgruppen und zeigt auf die Gruppe', () => { const projekt = zweiGruppen({ minGreenOverride: 3 }); const bericht = validateProject(projekt, undefined, DATUM); const befund = bericht.findings.find( (f) => f.rule === 'signalplan.mindestfreigabezeit-vorgabe-unterschritten', ); expect(befund).toBeDefined(); expect(befund!.severity).toBe('warnung'); expect(befund!.category).toBe('signalgruppen'); expect(befund!.target).toMatchObject({ kind: 'signalgruppe', label: 'K2' }); expect(befund!.title).toContain('Vorgabe unter der Mindestfreigabezeit'); expect(befund!.reference).toContain('Freigabezeiten'); expect(befund!.suggestion).toContain('Vorgabe'); // Eine Warnung, kein Fehler: das Rechenergebnis ist richtig. expect(bericht.exportBlocked).toBe(false); }); it('schweigt bei einer Vorgabe in Hoehe des Regelwerts oder darueber', () => { expect( notizen(zweiGruppen({ minGreenOverride: 5 }), 'mindestfreigabezeit-vorgabe-unterschritten'), ).toHaveLength(0); expect( notizen(zweiGruppen({ minGreenOverride: 8 }), 'mindestfreigabezeit-vorgabe-unterschritten'), ).toHaveLength(0); expect(notizen(zweiGruppen(), 'mindestfreigabezeit-vorgabe-unterschritten')).toHaveLength(0); }); it('meldet auch eine Vorgabe ueber dem Regelwert, aber unter dem Furtbedarf', () => { // Furt 14 m: halbe Furt 7 m / 1,2 m/s = 5,83 s -> 6 s. Vorgabe 5,5 s liegt // ueber dem Regelwert (5 s), aber unter dem Furtbedarf - ebenso wirkungslos. const projekt = furtProjekt({ minGreenOverride: 5.5 }); const f1 = projekt.signalGroups.find((g) => g.mode === 'fuss')!; const mfz = mindestfreigabezeitDerGruppe(projekt, f1, RILSA_DEFAULTS); expect(mfz.wert).toBe(6); expect(mfz.massgebend).toBe('furt'); const [notiz] = notizen(projekt, 'mindestfreigabezeit-vorgabe-unterschritten'); expect(notiz).toBeDefined(); expect(notiz!.signalGroupId).toBe(f1.id); expect(notiz!.message).toContain('5,5 s eingetragen'); expect(notiz!.message).toContain('Furtbedarf von 6 s'); expect(notiz!.message).toContain('Maßgebend bleibt 6 s'); }); }); // --- C10 --------------------------------------------------------------------- describe('C10: Umlaufzeit-Vorgaben gegen die Schranken der Anlagenart', () => { it('kappt 250 s am Knotenpunkt auf 120 s und meldet einen Fehler', () => { const { defaults, notes } = resolveRilsaSettings( { cycleTimeMax: 250 }, undefined, 'knotenpunkt', ); expect(defaults.cycleTime.max).toBe(ANLAGENART_GRENZEN.knotenpunkt.cycleTime.max); expect(defaults.cycleTime.max).toBe(120); const notiz = notes.find((n) => n.code === 'vorgabe-ueberschritten:cycleTimeMax'); expect(notiz).toBeDefined(); expect(notiz!.severity).toBe('fehler'); expect(notiz!.message).toContain('250'); expect(notiz!.message).toContain('höchstens 120 s'); // Die Begruendung ist die der Anlagenart - als Text, nicht als Funktion. expect(notiz!.message).toContain('nach RiLSA nicht vorzusehen'); expect(notiz!.message).not.toContain('function'); }); it('laesst 250 s bei einstreifiger Verkehrsfuehrung ungekappt und ohne Meldung', () => { const { defaults, notes } = resolveRilsaSettings( { cycleTimeMax: 250 }, undefined, 'einstreifig', ); expect(ANLAGENART_GRENZEN.einstreifig.cycleTime.max).toBe(300); expect(defaults.cycleTime.max).toBe(250); expect(notes).toHaveLength(0); }); it('kappt bei einstreifiger Fuehrung erst oberhalb von 300 s - mit der RSA-Begruendung', () => { const { defaults, notes } = resolveRilsaSettings( { cycleTimeMax: 400 }, undefined, 'einstreifig', ); expect(defaults.cycleTime.max).toBe(300); const notiz = notes.find((n) => n.code === 'vorgabe-ueberschritten:cycleTimeMax'); expect(notiz).toBeDefined(); expect(notiz!.message).toContain('300'); expect(notiz!.message).toContain('einstreifigen Verkehrsführung'); expect(notiz!.message).toContain('RSA'); }); it('nimmt ohne Anlagenart den Knotenpunkt an - alte Aufrufe uebersetzen, kappen aber', () => { const { defaults, notes } = resolveRilsaSettings({ cycleTimeMax: 250 }); expect(defaults.cycleTime.max).toBe(120); expect(notes.some((n) => n.code === 'vorgabe-ueberschritten:cycleTimeMax')).toBe(true); }); it('haelt die untere Schranke von 30 s fuer alle Umlaufzeitfelder', () => { const { defaults, notes } = resolveRilsaSettings( { cycleTimeMin: 5, cycleTimeRecommendedMin: 10 }, undefined, 'einstreifig', ); expect(defaults.cycleTime.min).toBe(SAFETY_FLOORS.minCycleTime); expect(defaults.cycleTime.recommendedMin).toBe(SAFETY_FLOORS.minCycleTime); expect(notes.filter((n) => n.code.startsWith('vorgabe-unterschritten:cycleTime'))).toHaveLength( 2, ); expect(notes.find((n) => n.code === 'vorgabe-unterschritten:cycleTimeMin')!.message).toContain( 'nicht schaltbar', ); }); it('prueft in der Rechnung dieselben Schranken wie die Vorgabenansicht', () => { for (const art of ['knotenpunkt', 'fussgaengerschutzanlage', 'einstreifig'] as const) { const feld = settingFields(undefined, art).find((f) => f.key === 'cycleTimeMax')!; const knappDrueber = resolveRilsaSettings({ cycleTimeMax: feld.max + 1 }, undefined, art); expect(knappDrueber.defaults.cycleTime.max, art).toBe(feld.max); const genau = resolveRilsaSettings({ cycleTimeMax: feld.max }, undefined, art); expect(genau.notes, art).toHaveLength(0); } }); it('kappt einen einstreifigen Plan mit 250 s NICHT - der Plan reicht die Anlagenart durch', () => { const projekt = zweiGruppen({}, 'einstreifig', { cycleTimeMax: 250 }); const plan = buildSignalPlan(projekt); expect(plan.defaults.cycleTime.max).toBe(250); expect(plan.grenzen.cycleTime.max).toBe(250); expect(plan.notes.some((n) => n.code.startsWith('vorgabe-ueberschritten'))).toBe(false); const bericht = validateProject(projekt, undefined, DATUM); expect( bericht.findings.some((f) => f.rule === 'vorgaben.vorgabe-ueberschritten:cycleTimeMax'), ).toBe(false); expect( bericht.findings.filter((f) => f.category === 'vorgaben' && f.severity === 'fehler'), ).toHaveLength(0); }); it('kappt denselben Wert am Knotenpunkt auf 120 s - mit Fehler im Plan und im Bericht', () => { const projekt = zweiGruppen({}, 'knotenpunkt', { cycleTimeMax: 250 }); const plan = buildSignalPlan(projekt); expect(plan.defaults.cycleTime.max).toBe(120); expect(plan.grenzen.cycleTime.max).toBe(120); expect(plan.notes.some((n) => n.code === 'vorgabe-ueberschritten:cycleTimeMax')).toBe(true); const bericht = validateProject(projekt, undefined, DATUM); const befund = bericht.findings.find( (f) => f.rule === 'vorgaben.vorgabe-ueberschritten:cycleTimeMax', ); expect(befund).toBeDefined(); expect(befund!.severity).toBe('fehler'); expect(befund!.category).toBe('vorgaben'); expect(bericht.exportBlocked).toBe(true); }); }); // --- C11 --------------------------------------------------------------------- describe('C11: Uebersaettigung ist kein Umlaufzeitergebnis', () => { it('liefert bounded "uebersaettigt" statt "maximum" - die Zahl ist ein Ersatzwert', () => { // Y = 0,7 + 0,35 = 1,05 >= 1: nicht bemessbar. const result = computeCycleTime('webster', { lostTime: 20, criticalFlowRatios: [0.7, 0.35] }); expect(result.bounded).toBe('uebersaettigt'); expect(result.bounded).not.toBe('maximum'); expect(result.cycleTime).toBe(RILSA_DEFAULTS.cycleTime.max); expect(result.raw).toBe(Number.POSITIVE_INFINITY); const notiz = result.notes.find((n) => n.code === 'uebersaettigt'); expect(notiz).toBeDefined(); expect(notiz!.severity).toBe('fehler'); expect(notiz!.message).toContain('Ersatzwert'); expect(notiz!.message).toContain('nicht bemessbar'); expect(notiz!.message).toContain('120 s'); }); it('haelt "maximum" fuer den rechenbaren, aber zu langen Umlauf getrennt', () => { // Y = 0,85 < 1: rechenbar (433 s), aber ueber 120 s. const result = computeCycleTime('webster', { lostTime: 40, criticalFlowRatios: [0.55, 0.3] }); expect(result.bounded).toBe('maximum'); expect(result.notes.some((n) => n.code === 'uebersaettigt')).toBe(false); }); it('gilt fuer die kapazitaetsorientierten Verfahren ab ihrer eigenen Grenze', () => { // HBS: Y >= Ziel-Auslastungsgrad 0,85. const hbs = computeCycleTime('hbs', { lostTime: 20, criticalFlowRatios: [0.5, 0.4] }); expect(hbs.bounded).toBe('uebersaettigt'); // Webster ist bei Y = 0,9 noch rechenbar. const webster = computeCycleTime('webster', { lostTime: 20, criticalFlowRatios: [0.5, 0.4] }); expect(webster.bounded).not.toBe('uebersaettigt'); const vergleich = compareCycleTimeMethods({ lostTime: 20, criticalFlowRatios: [0.5, 0.4] }); expect(vergleich.reason).toContain('übersättigt'); }); it('der Plan traegt den Zustand: cycleResult.bounded und umlaufzeitIstErsatzwert', () => { const plan = buildSignalPlan(uebersaettigt()); expect(plan.cycleResult).not.toBeNull(); expect(plan.cycleResult!.bounded).toBe('uebersaettigt'); expect(umlaufzeitIstErsatzwert(plan)).toBe(true); // Der Plan hat trotzdem eine Zahl - die Obergrenze der Anlagenart. expect(plan.cycleTime).toBeGreaterThan(0); expect(plan.notes.some((n) => n.code === 'uebersaettigt' && n.severity === 'fehler')).toBe( true, ); }); it('kennzeichnet einen bemessbaren oder fest vorgegebenen Umlauf nicht als Ersatzwert', () => { const bemessbar = buildSignalPlan(createStandardIntersectionProject('Normal', DATUM)); expect(umlaufzeitIstErsatzwert(bemessbar)).toBe(false); const roh = uebersaettigt(); const fest: Project = { ...roh, program: { ...roh.program, method: 'manuell', manualCycleTime: 90 }, }; const plan = buildSignalPlan(fest); expect(plan.cycleResult).toBeNull(); expect(umlaufzeitIstErsatzwert(plan)).toBe(false); }); it('haelt EINEN Wortlaut fuer den Vermerk bereit', () => { expect(UMLAUFZEIT_ERSATZWERT_VERMERK).toContain('nicht bemessbar'); expect(UMLAUFZEIT_ERSATZWERT_VERMERK).toContain('übersättigt'); expect(UMLAUFZEIT_ERSATZWERT_VERMERK).toContain('Ersatzwert'); }); it('nennt im Bericht die Anlagenart und den Ersatzwert', () => { const bericht = validateProject(uebersaettigt('einstreifig'), undefined, DATUM); const befund = bericht.findings.find((f) => f.rule === 'signalplan.uebersaettigt'); expect(befund).toBeDefined(); expect(befund!.severity).toBe('fehler'); expect(befund!.message).toContain('Ersatzwert'); expect(befund!.message).toContain(`${ANLAGENART_GRENZEN.einstreifig.cycleTime.max} s`); expect(befund!.reference).toContain('Umlaufzeit'); expect(bericht.exportBlocked).toBe(true); }); });