import { describe, expect, it } from 'vitest'; import { defaultVehicleClass } from '@/domain/model/factory'; import { parseProject, type ParseResult } from '@/domain/model/schema'; import { validateProject } from '@/domain/validation/engine'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { createConflict, createPhase, createSignalGroup, createEmptyProject, } from '@/domain/model/factory'; import { TRAFFIC_MODE_LABELS } from '@/domain/rilsa/constants'; import type { Project } from '@/domain/model/project'; import type { TrafficMode } from '@/domain/rilsa/types'; /* * Fassung 5.9.0 - der Fachkern. * * Das Einlesen setzte eine unbrauchbare Fahrzeugart zur Zeit des Befundes C8 * "auf Pkw". Seit der Fassung 5.9.0 ist der Rueckfall * `defaultVehicleClass(mode)` - am OePNV-Strom Bus, an der Fussgaengergruppe * "ohne Laengenzuschlag", an der Radgruppe Fahrrad. Ein OePNV-Strom, dessen * Fahrzeugart auf Pkw zurueckgefallen waere, raeumte mit 6 m statt mit den * 6 m des Busses - hier zufaellig dieselbe Zahl, aber der Weg ist ein anderer. * * Dieselbe Behebung hat einen zweiten Riegel eingezogen: Eine BEKANNTE Art * mit zu kurzem Laengenzuschlag ("ohne Laengenzuschlag", "Fahrrad" - beide * 0 m) ist an einem Kfz- oder OePNV-Strom ein Fehler mit Exportsperre, und das * Einlesen ersetzt sie: `fahrzeugart` in `model/schema.ts` meldet es, und * jeder Leseweg laeuft ueber `parseProject`. Sichtbar bleibt sie nur an einem * von Hand zusammengesetzten Projekt. * * Diese Faelle fragen den Fachkern, was gilt. */ const FIXED_DATE = new Date('2026-01-01T00:00:00Z'); const MODI: readonly TrafficMode[] = ['kfz', 'oepnv', 'rad', 'fuss']; /** * Ein Projekt aus einer Datei, deren OePNV-Signalgruppe eine unbekannte * Fahrzeugart traegt. Nur ueber `parseProject` erreichbar - die Oberflaeche * bietet keinen Weg dorthin. */ function eingelesenMitUnbekannterArt(mode: TrafficMode): Project { const roh = { schemaVersion: 13, signalGroups: [ { id: 'sg-1', name: 'X1', mode, vehicleClass: 'einhorn' }, { id: 'sg-2', name: 'X2', mode }, ], }; return parseProject(roh, FIXED_DATE).project; } /** * Ein Projekt aus einer Datei, deren Signalgruppe eine BEKANNTE Art mit zu * kurzem Laengenzuschlag traegt. */ function eingelesenMitZuKurzerArt(mode: TrafficMode, vehicleClass: string): ParseResult { return parseProject( { schemaVersion: 13, signalGroups: [{ id: 'sg-1', name: 'X1', mode, vehicleClass }], }, FIXED_DATE, ); } /** Zwei Kfz-Gruppen im Konflikt; die raeumende traegt eine zu kurze Art. */ function mitZuKurzemZuschlag(): Project { const leer = createEmptyProject('Prueffall', FIXED_DATE); const raeumend = { ...createSignalGroup({ name: 'K1', mode: 'kfz', index: 0 }), id: 'sg-raeumend', vehicleClass: 'keine' as const, }; const einfahrend = { ...createSignalGroup({ name: 'K2', mode: 'kfz', index: 1 }), id: 'sg-einfahrend', }; const konflikt = { ...createConflict('sg-raeumend', 'sg-einfahrend'), clearingDistance: 22, enteringDistance: 8, }; return { ...leer, signalGroups: [raeumend, einfahrend], phases: [createPhase('Phase 1', ['sg-raeumend']), createPhase('Phase 2', ['sg-einfahrend'])], conflicts: [konflikt, { ...createConflict('sg-einfahrend', 'sg-raeumend') }], }; } describe('Rueckfall der Fahrzeugart beim Einlesen - der Fachkern', () => { /* * Zuerst der Sachstand. Wuerde der Rueckfall eines Tages wieder fuer alle * Verkehrsarten Pkw lauten, faellt dieser Fall. */ it('faellt je Verkehrsart auf deren Standardart zurueck, nicht durchgaengig auf Pkw', () => { expect(defaultVehicleClass('kfz')).toBe('pkw'); expect(defaultVehicleClass('oepnv')).toBe('bus'); expect(defaultVehicleClass('rad')).toBe('rad'); expect(defaultVehicleClass('fuss')).toBe('keine'); // Drei der vier Verkehrsarten fallen also gerade NICHT auf Pkw zurueck. expect(MODI.filter((m) => defaultVehicleClass(m) === 'pkw')).toEqual(['kfz']); }); it('setzt das Einlesen die Standardart der Verkehrsart und meldet es', () => { for (const mode of MODI) { const projekt = eingelesenMitUnbekannterArt(mode); const gruppe = projekt.signalGroups.find((g) => g.id === 'sg-1'); expect(gruppe?.vehicleClass, TRAFFIC_MODE_LABELS[mode]).toBe(defaultVehicleClass(mode)); } const befund = parseProject( { schemaVersion: 13, signalGroups: [{ id: 'sg-1', name: 'B1', mode: 'oepnv', vehicleClass: 'einhorn' }], }, FIXED_DATE, ); expect(befund.issues.map((i) => i.message).join(' ')).toContain('bus'); }); }); describe('Zu kurzer Laengenzuschlag am Kfz-Strom - der Fachkern', () => { it('meldet der Fachkern einen Fehler mit Exportsperre unter "Zwischenzeiten"', () => { const projekt = mitZuKurzemZuschlag(); const bericht = validateProject(projekt, buildSignalPlan(projekt), FIXED_DATE); const treffer = bericht.findings.filter( (f) => f.rule === 'signalplan.fahrzeugart-laengenzuschlag-zu-kurz', ); expect(treffer).toHaveLength(1); expect(treffer[0]!.severity).toBe('fehler'); expect(treffer[0]!.category).toBe('zwischenzeiten'); expect(bericht.exportBlocked).toBe(true); }); /* * Der Sachstand zum Einleseweg. `fahrzeugart` in `model/schema.ts` prueft * die Art nicht nur gegen die Werteliste, sondern auch gegen die * Verkehrsart: An einem Kfz- oder OePNV-Strom wird eine Art mit zu kurzem * Zuschlag durch die Standardart ersetzt und gemeldet. Jeder Leseweg laeuft * ueber `parseProject` (`model/migrate.ts`), einen zweiten gibt es nicht. */ it('ersetzt das Einlesen auch eine bekannte Art mit zu kurzem Zuschlag und meldet es', () => { const kraftfahrzeugstroeme = [ ['kfz', 'keine'], ['kfz', 'rad'], ['oepnv', 'keine'], ['oepnv', 'rad'], ] as const; for (const [mode, art] of kraftfahrzeugstroeme) { const ergebnis = eingelesenMitZuKurzerArt(mode, art); const kennung = `${mode}/${art}`; expect(ergebnis.project.signalGroups[0]?.vehicleClass, kennung).toBe( defaultVehicleClass(mode), ); const gemeldet = ergebnis.issues.filter((i) => i.path.endsWith('.vehicleClass')); expect(gemeldet.length, kennung).toBe(1); expect(gemeldet[0]!.message, kennung).toContain('unzulässig'); } // Fussgaenger- und Radgruppen bleiben unberuehrt: Ein zu langer Zuschlag // ist nur unwirtschaftlich, ein zu kurzer dort nicht unsicher. for (const mode of ['rad', 'fuss'] as const) { const ergebnis = eingelesenMitZuKurzerArt(mode, 'keine'); expect(ergebnis.project.signalGroups[0]?.vehicleClass, mode).toBe('keine'); expect( ergebnis.issues.filter((i) => i.path.endsWith('.vehicleClass')), mode, ).toEqual([]); } }); });