lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests domain fahrzeugartRueckfall.test.ts

6,7 KB Rohdatei
tests/domain/fahrzeugartRueckfall.test.ts — 174 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { defaultVehicleClass } from '@/domain/model/factory';
3 import { parseProject, type ParseResult } from '@/domain/model/schema';
4 import { validateProject } from '@/domain/validation/engine';
5 import { buildSignalPlan } from '@/domain/plan/signalPlan';
6 import {
7 createConflict,
8 createPhase,
9 createSignalGroup,
10 createEmptyProject,
11 } from '@/domain/model/factory';
12 import { TRAFFIC_MODE_LABELS } from '@/domain/rilsa/constants';
13 import type { Project } from '@/domain/model/project';
14 import type { TrafficMode } from '@/domain/rilsa/types';
15
16 /*
17 * Fassung 5.9.0 - der Fachkern.
18 *
19 * Das Einlesen setzte eine unbrauchbare Fahrzeugart zur Zeit des Befundes C8
20 * "auf Pkw". Seit der Fassung 5.9.0 ist der Rueckfall
21 * `defaultVehicleClass(mode)` - am OePNV-Strom Bus, an der Fussgaengergruppe
22 * "ohne Laengenzuschlag", an der Radgruppe Fahrrad. Ein OePNV-Strom, dessen
23 * Fahrzeugart auf Pkw zurueckgefallen waere, raeumte mit 6 m statt mit den
24 * 6 m des Busses - hier zufaellig dieselbe Zahl, aber der Weg ist ein anderer.
25 *
26 * Dieselbe Behebung hat einen zweiten Riegel eingezogen: Eine BEKANNTE Art
27 * mit zu kurzem Laengenzuschlag ("ohne Laengenzuschlag", "Fahrrad" - beide
28 * 0 m) ist an einem Kfz- oder OePNV-Strom ein Fehler mit Exportsperre, und das
29 * Einlesen ersetzt sie: `fahrzeugart` in `model/schema.ts` meldet es, und
30 * jeder Leseweg laeuft ueber `parseProject`. Sichtbar bleibt sie nur an einem
31 * von Hand zusammengesetzten Projekt.
32 *
33 * Diese Faelle fragen den Fachkern, was gilt.
34 */
35
36 const FIXED_DATE = new Date('2026-01-01T00:00:00Z');
37
38 const MODI: readonly TrafficMode[] = ['kfz', 'oepnv', 'rad', 'fuss'];
39
40 /**
41 * Ein Projekt aus einer Datei, deren OePNV-Signalgruppe eine unbekannte
42 * Fahrzeugart traegt. Nur ueber `parseProject` erreichbar - die Oberflaeche
43 * bietet keinen Weg dorthin.
44 */
45 function eingelesenMitUnbekannterArt(mode: TrafficMode): Project {
46 const roh = {
47 schemaVersion: 13,
48 signalGroups: [
49 { id: 'sg-1', name: 'X1', mode, vehicleClass: 'einhorn' },
50 { id: 'sg-2', name: 'X2', mode },
51 ],
52 };
53 return parseProject(roh, FIXED_DATE).project;
54 }
55
56 /**
57 * Ein Projekt aus einer Datei, deren Signalgruppe eine BEKANNTE Art mit zu
58 * kurzem Laengenzuschlag traegt.
59 */
60 function eingelesenMitZuKurzerArt(mode: TrafficMode, vehicleClass: string): ParseResult {
61 return parseProject(
62 {
63 schemaVersion: 13,
64 signalGroups: [{ id: 'sg-1', name: 'X1', mode, vehicleClass }],
65 },
66 FIXED_DATE,
67 );
68 }
69
70 /** Zwei Kfz-Gruppen im Konflikt; die raeumende traegt eine zu kurze Art. */
71 function mitZuKurzemZuschlag(): Project {
72 const leer = createEmptyProject('Prueffall', FIXED_DATE);
73 const raeumend = {
74 ...createSignalGroup({ name: 'K1', mode: 'kfz', index: 0 }),
75 id: 'sg-raeumend',
76 vehicleClass: 'keine' as const,
77 };
78 const einfahrend = {
79 ...createSignalGroup({ name: 'K2', mode: 'kfz', index: 1 }),
80 id: 'sg-einfahrend',
81 };
82 const konflikt = {
83 ...createConflict('sg-raeumend', 'sg-einfahrend'),
84 clearingDistance: 22,
85 enteringDistance: 8,
86 };
87 return {
88 ...leer,
89 signalGroups: [raeumend, einfahrend],
90 phases: [createPhase('Phase 1', ['sg-raeumend']), createPhase('Phase 2', ['sg-einfahrend'])],
91 conflicts: [konflikt, { ...createConflict('sg-einfahrend', 'sg-raeumend') }],
92 };
93 }
94
95 describe('Rueckfall der Fahrzeugart beim Einlesen - der Fachkern', () => {
96 /*
97 * Zuerst der Sachstand. Wuerde der Rueckfall eines Tages wieder fuer alle
98 * Verkehrsarten Pkw lauten, faellt dieser Fall.
99 */
100 it('faellt je Verkehrsart auf deren Standardart zurueck, nicht durchgaengig auf Pkw', () => {
101 expect(defaultVehicleClass('kfz')).toBe('pkw');
102 expect(defaultVehicleClass('oepnv')).toBe('bus');
103 expect(defaultVehicleClass('rad')).toBe('rad');
104 expect(defaultVehicleClass('fuss')).toBe('keine');
105 // Drei der vier Verkehrsarten fallen also gerade NICHT auf Pkw zurueck.
106 expect(MODI.filter((m) => defaultVehicleClass(m) === 'pkw')).toEqual(['kfz']);
107 });
108
109 it('setzt das Einlesen die Standardart der Verkehrsart und meldet es', () => {
110 for (const mode of MODI) {
111 const projekt = eingelesenMitUnbekannterArt(mode);
112 const gruppe = projekt.signalGroups.find((g) => g.id === 'sg-1');
113 expect(gruppe?.vehicleClass, TRAFFIC_MODE_LABELS[mode]).toBe(defaultVehicleClass(mode));
114 }
115 const befund = parseProject(
116 {
117 schemaVersion: 13,
118 signalGroups: [{ id: 'sg-1', name: 'B1', mode: 'oepnv', vehicleClass: 'einhorn' }],
119 },
120 FIXED_DATE,
121 );
122 expect(befund.issues.map((i) => i.message).join(' ')).toContain('bus');
123 });
124 });
125
126 describe('Zu kurzer Laengenzuschlag am Kfz-Strom - der Fachkern', () => {
127 it('meldet der Fachkern einen Fehler mit Exportsperre unter "Zwischenzeiten"', () => {
128 const projekt = mitZuKurzemZuschlag();
129 const bericht = validateProject(projekt, buildSignalPlan(projekt), FIXED_DATE);
130 const treffer = bericht.findings.filter(
131 (f) => f.rule === 'signalplan.fahrzeugart-laengenzuschlag-zu-kurz',
132 );
133 expect(treffer).toHaveLength(1);
134 expect(treffer[0]!.severity).toBe('fehler');
135 expect(treffer[0]!.category).toBe('zwischenzeiten');
136 expect(bericht.exportBlocked).toBe(true);
137 });
138
139 /*
140 * Der Sachstand zum Einleseweg. `fahrzeugart` in `model/schema.ts` prueft
141 * die Art nicht nur gegen die Werteliste, sondern auch gegen die
142 * Verkehrsart: An einem Kfz- oder OePNV-Strom wird eine Art mit zu kurzem
143 * Zuschlag durch die Standardart ersetzt und gemeldet. Jeder Leseweg laeuft
144 * ueber `parseProject` (`model/migrate.ts`), einen zweiten gibt es nicht.
145 */
146 it('ersetzt das Einlesen auch eine bekannte Art mit zu kurzem Zuschlag und meldet es', () => {
147 const kraftfahrzeugstroeme = [
148 ['kfz', 'keine'],
149 ['kfz', 'rad'],
150 ['oepnv', 'keine'],
151 ['oepnv', 'rad'],
152 ] as const;
153 for (const [mode, art] of kraftfahrzeugstroeme) {
154 const ergebnis = eingelesenMitZuKurzerArt(mode, art);
155 const kennung = `${mode}/${art}`;
156 expect(ergebnis.project.signalGroups[0]?.vehicleClass, kennung).toBe(
157 defaultVehicleClass(mode),
158 );
159 const gemeldet = ergebnis.issues.filter((i) => i.path.endsWith('.vehicleClass'));
160 expect(gemeldet.length, kennung).toBe(1);
161 expect(gemeldet[0]!.message, kennung).toContain('unzulässig');
162 }
163 // Fussgaenger- und Radgruppen bleiben unberuehrt: Ein zu langer Zuschlag
164 // ist nur unwirtschaftlich, ein zu kurzer dort nicht unsicher.
165 for (const mode of ['rad', 'fuss'] as const) {
166 const ergebnis = eingelesenMitZuKurzerArt(mode, 'keine');
167 expect(ergebnis.project.signalGroups[0]?.vehicleClass, mode).toBe('keine');
168 expect(
169 ergebnis.issues.filter((i) => i.path.endsWith('.vehicleClass')),
170 mode,
171 ).toEqual([]);
172 }
173 });
174 });