lsa-planer

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

/ tests domain ueberholteZuschreibungen.test.ts

12,9 KB Rohdatei
tests/domain/ueberholteZuschreibungen.test.ts — 254 Zeilen
1 import { readFileSync } from 'node:fs';
2 import { describe, expect, it } from 'vitest';
3 import { createStandardIntersectionProject } from '@/domain/model/factory';
4 import { buildSignalPlan } from '@/domain/plan/signalPlan';
5 import { validateProject } from '@/domain/validation/engine';
6 import { festeWerteNachRegelwert, festeWerteZuschreibungSatz } from '@/domain/rilsa/ansaetze';
7 import { RILSA_DEFAULTS, SAFETY_FLOORS } from '@/domain/rilsa/constants';
8 import { resolveRilsaSettings, settingFields } from '@/domain/rilsa/settings';
9 import type { Project } from '@/domain/model/project';
10
11 /**
12 * Fassung 5.43.0: Zuschreibungen, die der eigene Abgleich widerlegt.
13 *
14 * WORUM ES GEHT. Seit der Fassung 5.27.0 liegt die gekaufte RiLSA 2015 vor, und
15 * der Abgleich hat die beiden festen Ueberfahrzeiten des Kraftfahrzeugverkehrs
16 * (3 s geradeaus, 2 s abbiegend) als REGELWERKSWERTE belegt - Abschnitt 2.5.2,
17 * Faelle 1 und 2; `quellen.ts` fuehrt sie seitdem unter 'gesichert' und mit
18 * Fundstelle. Zwei Stellen haben das nicht mitbekommen und standen auf dem Stand
19 * ohne Druckwerk:
20 *
21 * 1. Der PRUEFBERICHT (validation/engine.ts, Meldung
22 * 'zwischenzeiten.ueberfahrzeit-ansatz-fest') nannte den festen Ansatz
23 * "ein Praxisansatz nach behördlichen Leitfäden" und behauptete, der
24 * Wortlaut der RiLSA zu den Faellen 1 und 2 sei "öffentlich nicht
25 * abgleichbar". Beides steht im SELBEN Dokument neben dem
26 * Fundstellenverzeichnis, das die Abschnittsnummer nennt - und der Satz
27 * stand unbedingt da, auch wo der Plan eigene feste Werte fuehrt.
28 * 2. Der Warnbalken der Projektansicht (ui/views/projectView.ts) schrieb
29 * eingetragene Vorgabewerte unbedingt der RiLSA zu und nannte die
30 * Gelbzeitkopplung "den längeren Ansatz dieses Programms". Bei 50 km/h
31 * geradeaus ergeben beide Ansaetze 3 s - laenger ist sie dort nicht. Diese
32 * Datei prueft den Fachkern dahinter, die Ansicht prueft
33 * tests/ui/herkunftFesteWerte.test.ts.
34 *
35 * DAZU DER KOMMENTAR in rilsa/types.ts, der den Ansatz 'fest' ebenfalls als
36 * Praxisansatz auswies. Grundsatz: Zusagen in Kommentaren sind Teil des
37 * Programms.
38 *
39 * WARUM "NIE KUERZER" UND NICHT "LAENGER" die haltbare Aussage ist: Die
40 * Gelbzeitstaffel ist einstellbar, aber nicht unter SAFETY_FLOORS.yellowKfz -
41 * und der liegt mit 3 s genau auf dem Regelwert geradeaus und ueber dem
42 * abbiegenden. Der letzte Fall rechnet das nach.
43 */
44
45 const DATUM = new Date('2026-01-01T00:00:00Z');
46
47 /** Knotenpunkt mit festem Ueberfahrzeit-Ansatz und wahlweise eigenen Vorgaben. */
48 function mitFestemAnsatz(rilsa: Record<string, number> = {}): Project {
49 const basis = createStandardIntersectionProject('Überfahrzeit', DATUM);
50 return {
51 ...basis,
52 settings: { ...basis.settings, ueberfahrzeitAnsatz: 'fest', rilsa },
53 };
54 }
55
56 function befund(project: Project): { message: string; suggestion: string } {
57 const plan = buildSignalPlan(project);
58 const treffer = validateProject(project, plan, DATUM).findings.find(
59 (f) => f.rule === 'zwischenzeiten.ueberfahrzeit-ansatz-fest',
60 );
61 expect(treffer, 'Die Meldung zum festen Ansatz fehlt').toBeDefined();
62 return { message: treffer!.message, suggestion: treffer!.suggestion };
63 }
64
65 /** Ein Kommentar ueber mehrere Zeilen, in einer Zeile - ohne Rand und Umbruch. */
66 function fliessend(quelltext: string): string {
67 return quelltext
68 .split('\n')
69 .map((zeile) => zeile.replace(/^\s*(\/\*+|\*\/|\*|\/\/)?\s?/u, ''))
70 .join(' ')
71 .replace(/\s+/gu, ' ');
72 }
73
74 describe('Der Pruefbericht schreibt den festen Ansatz dem Regelwerk zu, das ihn setzt', () => {
75 it('nennt bei den Regelwerten die Fundstelle statt behoerdlicher Leitfaeden', () => {
76 const { message } = befund(mitFestemAnsatz());
77 expect(message, 'die Zuschreibung an die RiLSA').toContain(
78 'Die Werte sind die Regelwerte der RiLSA 2015 (Abschnitt 2.5.2, Fälle 1 und 2)',
79 );
80 expect(message, 'die Richtung des Regelansatzes').toContain('rechnet nie kürzer als diese');
81 expect(message, 'der Stand ohne Druckwerk').not.toContain('Praxisansatz');
82 expect(message, 'der Stand ohne Druckwerk').not.toContain('öffentlich nicht abgleichbar');
83 });
84
85 it('schreibt eigene Vorgabewerte nicht der RiLSA zu', () => {
86 // 6 s geradeaus und 6 s abbiegend: Die groesste Gelbzeit der Regelstaffel
87 // ist 5 s, dieser Ansatz verlaengert also jede Zwischenzeit - und die
88 // beiden Zahlen stehen in keinem Regelwerk.
89 const { message } = befund(
90 mitFestemAnsatz({ crossingTimeKfzGeradeaus: 6, crossingTimeKfzAbbiegend: 6 }),
91 );
92 expect(message, 'Vorbedingung: die eingetragenen Werte stehen in der Meldung').toContain('6 s');
93 expect(message).toContain('Die Werte sind Vorgaben dieses Plans');
94 expect(message, 'die belegten Zahlen gehoeren daneben').toContain(
95 `${RILSA_DEFAULTS.crossingTime.kfzGeradeaus} s geradeaus und ` +
96 `${RILSA_DEFAULTS.crossingTime.kfzAbbiegend} s abbiegend`,
97 );
98 expect(message).not.toContain('Die Werte sind die Regelwerte der RiLSA 2015');
99 });
100
101 it('behauptet in der Empfehlung keine laengere Zwischenzeit', () => {
102 /*
103 * Hier stand "Wählen Sie „Überfahrzeit = Gelbzeit", wenn die längere und
104 * damit konservative Zwischenzeit gelten soll." Bei 50 km/h geradeaus sind
105 * beide Ansaetze gleich lang, und bei 6 s festen Vorgaben ist die Gelbzeit
106 * die KUERZERE. Die Richtung sagt der Satz aus dem Fachkern, der in der
107 * Meldung darueber steht.
108 */
109 for (const vorgaben of [{}, { crossingTimeKfzGeradeaus: 6, crossingTimeKfzAbbiegend: 6 }]) {
110 const { suggestion } = befund(mitFestemAnsatz(vorgaben));
111 expect(suggestion, 'die Begruendungspflicht bleibt').toContain('Begründen Sie die Wahl');
112 expect(suggestion, JSON.stringify(vorgaben)).not.toContain('die längere');
113 }
114 });
115 });
116
117 describe('Der Satz zur Herkunft der festen Werte steht im Fachkern', () => {
118 it('unterscheidet Regelwerte und Vorgaben dieses Plans an derselben Antwort', () => {
119 expect(festeWerteNachRegelwert(RILSA_DEFAULTS)).toBe(true);
120 const eigene = resolveRilsaSettings({ crossingTimeKfzAbbiegend: 4 }).defaults;
121 expect(eigene.crossingTime.kfzAbbiegend, 'Vorbedingung: die Vorgabe greift').toBe(4);
122 expect(festeWerteNachRegelwert(eigene)).toBe(false);
123 expect(festeWerteZuschreibungSatz(RILSA_DEFAULTS)).toContain(
124 'Die Werte sind die Regelwerte der RiLSA 2015',
125 );
126 expect(festeWerteZuschreibungSatz(eigene)).toContain('Die Werte sind Vorgaben dieses Plans');
127 });
128
129 it('bleibt bei "nie kürzer" auch an der kleinsten zulaessigen Gelbzeitstaffel', () => {
130 /*
131 * Der Beleg fuer die Aussage. Alle drei Gelbzeiten werden auf 0 gesetzt;
132 * angesetzt wird dann SAFETY_FLOORS.yellowKfz. Liegt der nicht unter den
133 * beiden Regelwerten, ist "rechnet nie kürzer als diese" wahr - bei jeder
134 * zulaessigen Staffel, denn kleiner geht es nicht.
135 */
136 const kleinste = resolveRilsaSettings({
137 yellowKfzUpTo50: 0,
138 yellowKfzUpTo60: 0,
139 yellowKfzAbove60: 0,
140 }).defaults;
141 for (const stufe of kleinste.yellowBySpeed) {
142 expect(stufe.yellow, 'die Untergrenze greift').toBe(SAFETY_FLOORS.yellowKfz);
143 expect(stufe.yellow).toBeGreaterThanOrEqual(RILSA_DEFAULTS.crossingTime.kfzGeradeaus);
144 expect(stufe.yellow).toBeGreaterThanOrEqual(RILSA_DEFAULTS.crossingTime.kfzAbbiegend);
145 }
146 });
147 });
148
149 describe('Die Kommentare sagen dasselbe wie der Abgleich', () => {
150 it('weist den Ansatz "fest" in rilsa/types.ts nicht mehr als Praxisansatz aus', () => {
151 /*
152 * Gemessen wird die AUSSAGE, nicht das Vorkommen der Worte: Was der
153 * Kommentar frueher sagte, darf er weiter berichten - das ist der Sinn
154 * eines "BERICHTIGT"-Absatzes. Geprueft wird deshalb der Teil davor.
155 */
156 const quelle = fliessend(readFileSync('src/domain/rilsa/types.ts', 'utf8'));
157 const anfang = quelle.indexOf('Rechenansatz fuer die Ueberfahrzeit');
158 expect(anfang, 'der Docblock zu UeberfahrzeitAnsatz').toBeGreaterThanOrEqual(0);
159 const ende = quelle.indexOf('Signalbild eines Signalgebers', anfang);
160 const docblock = quelle.slice(anfang, ende);
161 expect(docblock, 'Vorbedingung: der Zweig "fest" steht darin').toContain('tue = 3 s geradeaus');
162 expect(docblock, 'die Fundstelle des Abgleichs').toContain('Abschnitt 2.5.2');
163
164 const berichtigt = docblock.indexOf('BERICHTIGT');
165 expect(berichtigt, 'der Absatz, der den alten Stand berichtet').toBeGreaterThan(0);
166 expect(docblock.slice(0, berichtigt), 'die Zuschreibung selbst').not.toContain(
167 'Praxisansatz nach behoerdlichen Leitfaeden',
168 );
169 });
170
171 it('nennt den Gelbzeitansatz in rilsa/constants.ts nicht mehr pauschal den laengeren', () => {
172 /*
173 * NACHTRAG (Fassung 5.43.0): Befund 17 war an UEBERFAHRZEIT_ANSATZ_LABELS,
174 * im Pruefbericht, im Warnbalken und in bewertung.ts behoben - nicht aber
175 * im Docblock des Kennwerts selbst. Dort stand "'gelbzeit' ist der
176 * laengere, 'fest' der regelkonforme". Bei 50 km/h geradeaus ergeben beide
177 * Ansaetze 3 s, und mit festen Vorgaben ueber der groessten Gelbzeit ist
178 * die Kopplung die kuerzere. Der Docblock ist die Stelle, an der der
179 * naechste Bearbeiter nachliest, welche Richtung die sichere ist.
180 *
181 * Geprueft wird die AUSSAGE, nicht das Vorkommen der Worte: Der Absatz, der
182 * den alten Stand berichtet, darf ihn weiter nennen.
183 */
184 const quelle = fliessend(readFileSync('src/domain/rilsa/constants.ts', 'utf8'));
185 const anfang = quelle.indexOf('Ueberfahrzeiten tue in s');
186 expect(anfang, 'der Docblock zu crossingTime').toBeGreaterThanOrEqual(0);
187 const docblock = quelle.slice(anfang, quelle.indexOf('readonly crossingTime:', anfang));
188 expect(docblock, 'Vorbedingung: die Wahl steht darin').toContain(
189 'Project.settings.ueberfahrzeitAnsatz',
190 );
191
192 const berichtigt = docblock.indexOf('NIE KUERZER');
193 expect(berichtigt, 'der Absatz, der den alten Stand berichtet').toBeGreaterThan(0);
194 const zuschreibung = docblock.slice(0, berichtigt);
195 expect(zuschreibung, 'die Uebertreibung selbst').not.toContain("'gelbzeit' ist der laengere");
196 expect(zuschreibung, 'die haltbare Aussage').toContain('nie kuerzer');
197 // Und die Begruendungspflicht haengt am Ansatz, nicht an "laenger" oder
198 // "kuerzer": Der Pruefbericht meldet allein 'fest' (checkUeberfahrzeitAnsatz).
199 expect(zuschreibung).not.toContain('fuer den laengeren keine Begruendung');
200 });
201
202 it('trennt in rilsa/constants.ts den Ansatz von seinen beiden Werten', () => {
203 /*
204 * NACHTRAG (Fassung 5.43.0): An die Stelle der Uebertreibung trat "'fest'
205 * ist der Regelwerksansatz selbst" - richtig fuer den ANSATZ, zu stark fuer
206 * seine WERTE. Beide Felder sind von 1 bis 10 s einstellbar, und genau
207 * diese Verwechslung war Befund B5 der Fassung 5.5.0: Wer sie liest,
208 * schreibt eingetragene Vorgaben der RiLSA zu. Der letzte Ausdruck dieses
209 * Falls misst, dass die Werte wirklich einstellbar sind - ein Kommentar
210 * ueber eine Schranke ist nur so lange richtig, wie die Schranke ihn
211 * traegt.
212 */
213 const quelle = fliessend(readFileSync('src/domain/rilsa/constants.ts', 'utf8'));
214 const anfang = quelle.indexOf('Ueberfahrzeiten tue in s');
215 const docblock = quelle.slice(anfang, quelle.indexOf('readonly crossingTime:', anfang));
216 const zuschreibung = docblock.slice(0, docblock.indexOf('NIE KUERZER'));
217
218 expect(zuschreibung, 'die Zahl des Ansatzes ist nicht der Ansatz').not.toContain(
219 "'fest' ist der Regelwerksansatz selbst",
220 );
221 expect(zuschreibung, 'der Verweis auf die Antwort im Fachkern').toContain(
222 'festeWerteNachRegelwert',
223 );
224 expect(zuschreibung, 'und die Schranke, die es noetig macht').toContain('von 1 bis 10 s');
225
226 const felder = settingFields().filter(
227 (f) => f.key === 'crossingTimeKfzGeradeaus' || f.key === 'crossingTimeKfzAbbiegend',
228 );
229 expect(felder, 'Vorbedingung: beide Felder gibt es').toHaveLength(2);
230 for (const feld of felder) {
231 expect([feld.min, feld.max], feld.key).toEqual([1, 10]);
232 }
233 });
234
235 it('beschreibt in conflictsView.ts die Regel zwischenzeiten.null so, wie sie meldet', () => {
236 /*
237 * Der Kommentar an der Legende der Zwischenzeitenmatrix berief sich darauf,
238 * die Pruefregel halte 0 s "bei langen Furten" fuer den Regelfall und
239 * zulaessig. Diesen Satz gibt es seit 5.43.0 nicht mehr: Die Regel nennt
240 * die Bedingung, unter der 0 s zulaessig sind, und schweigt an Beziehungen,
241 * die schon als kuerzer als nach dem Lageplan gemeldet sind. Den Wortlaut
242 * der Regel selbst bewacht tests/domain/furtrichtung.test.ts; hier steht,
243 * dass der Kommentar der Ansicht ihn nicht mehr falsch wiedergibt.
244 */
245 const quelle = fliessend(readFileSync('src/ui/views/conflictsView.ts', 'utf8'));
246 expect(quelle, 'Vorbedingung: die Legende zur 0 s steht in der Datei').toContain(
247 'Zwischenzeit 0 s – nachzuprüfen',
248 );
249 expect(quelle).not.toContain('bei langen Furten der Regelfall und zulaessig');
250 expect(quelle, 'was die Regel wirklich tut').toContain(
251 'nennt die Bedingung, unter der er zulaessig ist',
252 );
253 });
254 });