lsa-planer

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

/ tests domain fundstellenMeldungstexte.test.ts

12,1 KB Rohdatei
tests/domain/fundstellenMeldungstexte.test.ts — 268 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { buildSignalPlan } from '@/domain/plan/signalPlan';
3 import { validateProject } from '@/domain/validation/engine';
4 import { abbiegerfaktorBefunde } from '@/domain/validation/rules';
5 import { ANLAGENART_GRENZEN } from '@/domain/rilsa/constants';
6 import {
7 createConflict,
8 createEmptyProject,
9 createPhase,
10 createSignalGroup,
11 createStandardIntersectionProject,
12 } from '@/domain/model/factory';
13 import type { Anlagenart, Project, SignalGroup } from '@/domain/model/project';
14
15 /*
16 * Fundstellen und Meldungstexte.
17 *
18 * Befund 3: Die Fundstelle der Plan-Notizen kam ueber einen Praefixtreffer
19 * (`code.startsWith('umlaufzeit')`). Damit trugen Meldungen die Herkunft der
20 * Umlaufzeitschranke, deren genannter Zahlenwert gerade NICHT diese Schranke
21 * ist: die Mindestumlaufzeit (Summe aus Mindestfreigabe- und Zwischenzeiten),
22 * die Uebersaettigung (Y gegen die Grenze des Verfahrens) und die unbrauchbare
23 * Eingabe. An einer Arbeitsstelle stand an der Uebersaettigung - einem Fehler
24 * mit Exportsperre - "Schwelle dieses Programms ...; Zahlenwert abzugleichen":
25 * Der Pruefer las, das Kriterium sei eine mit der Behoerde abzustimmende
26 * Eigenschwelle.
27 *
28 * Befund B-7 und Befund 4: Der Hinweis zum Abbiegerfaktor stand bei genau
29 * einem Partner im Plural und behauptete "Kraftfahrzeugstroeme anderer
30 * Zufahrten", ohne zu sagen, dass ueber den Gegenverkehr damit nichts
31 * festgestellt ist.
32 */
33
34 const DATUM = new Date('2026-01-01T00:00:00Z');
35
36 /** Der Musterknotenpunkt mit Verkehrsstaerken weit ueber der Kapazitaet. */
37 function uebersaettigt(anlagenart: Anlagenart): Project {
38 const roh = createStandardIntersectionProject('Uebersaettigt', DATUM);
39 return {
40 ...roh,
41 anlagenart,
42 demands: roh.signalGroups
43 .filter((g) => g.mode === 'kfz')
44 .map((g) => ({ signalGroupId: g.id, volume: 1800, heavyVehicleShare: 0.1 })),
45 };
46 }
47
48 /** Feste Umlaufzeit ueber der Schranke der Anlagenart. */
49 function festeUmlaufzeit(anlagenart: Anlagenart, wert: number | null): Project {
50 const roh = createStandardIntersectionProject('Umlaufzeit', DATUM);
51 return {
52 ...roh,
53 anlagenart,
54 program: { ...roh.program, method: 'manuell', manualCycleTime: wert },
55 };
56 }
57
58 function fundstelle(project: Project, regel: string): string {
59 const plan = buildSignalPlan(project);
60 const bericht = validateProject(project, plan, DATUM);
61 const treffer = bericht.findings.find((f) => f.rule === regel);
62 expect(treffer, `${regel} muss im Bericht stehen`).toBeDefined();
63 return treffer!.reference;
64 }
65
66 describe('Befund 3 - jede Meldung nennt die Herkunft IHRER Zahl', () => {
67 it('gibt der Uebersaettigung das Kriterium des Verfahrens, nicht die Umlaufzeitschranke', () => {
68 // Der nachgemessene Fall der Pruefung: einstreifige Fuehrung, 1800 Kfz/h.
69 const einstreifig = fundstelle(uebersaettigt('einstreifig'), 'signalplan.uebersaettigt');
70 expect(einstreifig).not.toContain('Schwelle dieses Programms');
71 expect(einstreifig).not.toContain('Zahlenwert abzugleichen');
72 // Der Musterknotenpunkt rechnet nach Webster; dort ist Y < 1 die
73 // Voraussetzung der Formel und nicht eine Schranke der Anlagenart.
74 expect(einstreifig).toContain('Webster 1958');
75 expect(einstreifig).toContain('Y < 1 als Voraussetzung der Formel');
76
77 const knoten = fundstelle(uebersaettigt('knotenpunkt'), 'signalplan.uebersaettigt');
78 expect(knoten).toBe(einstreifig);
79 // Vor der Fassung 5.4.0 war es an beiden Anlagenarten die Umlaufzeitzeile.
80 expect(knoten).not.toBe('RiLSA 2015 - Umlaufzeit');
81 });
82
83 it('nennt bei der Mindestumlaufzeit die Groessen, aus denen sie folgt', () => {
84 const erwartet =
85 'RiLSA 2015 - Mindestfreigabezeiten und Zwischenzeiten (daraus folgt die Mindestumlaufzeit)';
86 for (const art of ['knotenpunkt', 'einstreifig'] as const) {
87 // Feste Umlaufzeit gewaehlt, aber keine eingetragen: Angesetzt wird die
88 // Mindestumlaufzeit, und genau die nennt die Meldung.
89 expect(fundstelle(festeUmlaufzeit(art, null), 'signalplan.umlaufzeit-fehlt'), art).toBe(
90 erwartet,
91 );
92 }
93 });
94
95 it('behaelt die Herkunft der Schranke dort, wo die Zahl auch die Schranke ist', () => {
96 // Waechter fuer Befund B9: Die Meldung "Umlaufzeit ueberschreitet den
97 // Hoechstwert" nennt die Schranke der Anlagenart - sie MUSS deren Herkunft
98 // tragen.
99 expect(ANLAGENART_GRENZEN.knotenpunkt.cycleTime.max).toBe(120);
100 expect(fundstelle(festeUmlaufzeit('knotenpunkt', 200), 'signalplan.umlaufzeit-zu-lang')).toBe(
101 'RiLSA 2015 - Umlaufzeit',
102 );
103
104 expect(ANLAGENART_GRENZEN.einstreifig.cycleTime.max).toBe(300);
105 const engstelle = fundstelle(
106 festeUmlaufzeit('einstreifig', 400),
107 'signalplan.umlaufzeit-zu-lang',
108 );
109 expect(engstelle).toContain('Schwelle dieses Programms');
110 expect(engstelle).toContain('kein RSA-Wert');
111 });
112
113 it('nennt beim Regelbereich weiterhin dessen eigene Herkunft', () => {
114 // Waechter fuer Befund B8/B9: Der empfohlene Bereich hat eine eigene
115 // Herkunft und darf sie ueber die Umstellung nicht verlieren.
116 const knoten = fundstelle(
117 festeUmlaufzeit('knotenpunkt', 110),
118 'signalplan.umlaufzeit-ausserhalb-regelbereich',
119 );
120 expect(knoten).toBe(ANLAGENART_GRENZEN.knotenpunkt.herkunft.regelbereich.text);
121 });
122
123 /*
124 * UMGESCHRIEBEN - der Fall lief nie durch seinen Rumpf.
125 *
126 * Er baute den Musterknotenpunkt mit `manualCycleTime: Number.NaN`, lief
127 * ueber `bericht.findings` und uebersprang mit
128 * `if (finding.rule !== 'signalplan.umlaufzeit-ungueltig') continue;` jeden
129 * Befund. Diese Regel entsteht dort aber gar nicht: resolveCycleTime
130 * (plan/signalPlan.ts) ersetzt jede unbrauchbare Handvorgabe durch die
131 * Mindestumlaufzeit und meldet 'umlaufzeit-fehlt'. GEMESSEN: Der Bericht
132 * jenes Projekts fuehrt 12 Befunde, keinen mit dieser Regel; ein
133 * `expect.fail()` als erste Zeile des Rumpfes liess alle neun Faelle der
134 * Datei gruen bleiben. Der Fall war seit jeher gruen und bewachte nichts.
135 *
136 * 'umlaufzeit-ungueltig' entsteht, wo die ANGESETZTE Umlaufzeit wirklich
137 * kein brauchbarer Wert ist: distributeGreenTimes (rilsa/greenSplit.ts)
138 * meldet sie bei einer Umlaufzeit <= 0 s. Dahin fuehrt eine Phase ohne
139 * Signalgruppen - ihre Mindestfreigabezeit ist 0 s, es gibt keine
140 * Uebergaenge, und damit ist auch die Mindestumlaufzeit 0 s, die
141 * resolveCycleTime fuer die unbrauchbare Handvorgabe ansetzt.
142 *
143 * Die Fundstelle des Programms ist in Ordnung: referenceFor
144 * (validation/engine.ts) faengt den Kode vor jedem Rueckfall ab und
145 * antwortet mit "Kein Regelwerksbezug ...". Der Fall misst das jetzt.
146 */
147 function umlaufzeitNull(): Project {
148 const basis = createEmptyProject('Ungueltig', DATUM, 'knotenpunkt');
149 const leer = createPhase('Phase 1', []);
150 return {
151 ...basis,
152 phases: [leer],
153 program: {
154 ...basis.program,
155 phaseOrder: [leer.id],
156 method: 'manuell',
157 manualCycleTime: Number.NaN,
158 },
159 };
160 }
161
162 it('erfindet fuer eine unbrauchbare Umlaufzeit keine Fundstelle', () => {
163 // Kein Regelwerk sagt etwas ueber einen ungueltigen Zahlenwert. Eine
164 // erfundene Fundstelle waere hier schlimmer als keine.
165 const kaputt = umlaufzeitNull();
166 // Beleg, dass der Kode ueberhaupt entstehen kann - sonst prueft der Fall
167 // wieder nichts.
168 expect(buildSignalPlan(kaputt).cycleTime, 'angesetzte Umlaufzeit').toBe(0);
169 const gefunden = fundstelle(kaputt, 'signalplan.umlaufzeit-ungueltig');
170 /*
171 * Der Wortlaut steht exakt. Erlaeuterung dazu, kein zweiter Pruefschritt:
172 * Der Rueckfall von referenceFor (validation/engine.ts) lautet
173 * "RiLSA 2015" - genau der waere hier die erfundene Fundstelle, und gegen
174 * ihn haelt bereits dieses `toBe`.
175 *
176 * ENTFERNT (Fassung 5.16.0): Darunter stand eine Schleife ueber sieben
177 * Regelwerksnamen mit `not.toContain`. Sie lief zwar - anders als die
178 * beiden Schleifen, die mit dem Umschreiben entfielen -, konnte aber nie
179 * die Ursache eines roten Laufs sein: Haelt das `toBe`, ist `gefunden`
180 * genau diese eine Zeichenkette und enthaelt keinen der sieben Namen; haelt
181 * es nicht, bricht der Fall vorher ab. GEMESSEN an der Mutation
182 * "referenceFor gibt fuer 'umlaufzeit-ungueltig' den Rueckfall 'RiLSA
183 * 2015 - Umlaufzeit'": Vitest meldete ausschliesslich das `toBe`, die
184 * Schleife kam nicht zum Zug. Sie hatte damit keine Unterscheidungskraft,
185 * las sich neben ihrem Kommentar aber wie eine eigenstaendige Pruefung -
186 * dieselbe Gattung Irrefuehrung, gegen die dieser Fall geschrieben ist. Das
187 * `toBe` zu einem `toContain` abzuschwaechen, nur damit die Schleife etwas
188 * zu tun bekommt, waere ein Rueckschritt gewesen.
189 */
190 expect(gefunden).toBe('Kein Regelwerksbezug - die Umlaufzeit ist kein gültiger Zahlenwert');
191 });
192
193 it('setzt fuer eine unbrauchbare Handvorgabe die Mindestumlaufzeit an, nicht den Ungueltig-Kode', () => {
194 /*
195 * Wache gegen die Rueckkehr der Blindstelle: Solange dieser Fall gilt,
196 * fuehrt `manualCycleTime: Number.NaN` am Musterknotenpunkt zu
197 * 'umlaufzeit-fehlt' - und ein Fall, der dort 'umlaufzeit-ungueltig'
198 * sucht, laeuft leer. Er haelt zugleich fest, was das Programm mit einer
199 * unbrauchbaren Zahl tut: Es rechnet nicht mit ihr weiter.
200 */
201 const roh = createStandardIntersectionProject('Ungueltig', DATUM);
202 const kaputt: Project = {
203 ...roh,
204 program: { ...roh.program, method: 'manuell', manualCycleTime: Number.NaN },
205 };
206 const bericht = validateProject(kaputt, buildSignalPlan(kaputt), DATUM);
207 const regeln = bericht.findings.map((f) => f.rule);
208 expect(regeln).toContain('signalplan.umlaufzeit-fehlt');
209 expect(regeln).not.toContain('signalplan.umlaufzeit-ungueltig');
210 });
211
212 it('laesst keine Fundstelle leer', () => {
213 for (const art of ['knotenpunkt', 'fussgaengerschutzanlage', 'einstreifig'] as const) {
214 const project = uebersaettigt(art);
215 const bericht = validateProject(project, buildSignalPlan(project), DATUM);
216 for (const finding of bericht.findings) {
217 expect(finding.reference, finding.rule).not.toBe('');
218 }
219 }
220 });
221 });
222
223 /** Linksabbieger plus `anzahl` Kfz-Gruppen anderer Arme in derselben Phase. */
224 function linksabbiegerMitPartnern(anzahl: number): Project {
225 const basis = createEmptyProject('Abbieger', DATUM, 'knotenpunkt');
226 const links: SignalGroup = {
227 ...createSignalGroup({ name: 'K1', mode: 'kfz', index: 0 }),
228 movement: 'links',
229 armId: 'nord',
230 };
231 const partner = Array.from({ length: anzahl }, (_, i) => ({
232 ...createSignalGroup({ name: `P${i + 1}`, mode: 'kfz', index: i + 1 }),
233 movement: 'rechts' as const,
234 armId: `arm-${i}`,
235 }));
236 const phase = createPhase('Phase 1', [links.id, ...partner.map((p) => p.id)]);
237 return {
238 ...basis,
239 signalGroups: [links, ...partner],
240 conflicts: partner.map((p) => createConflict(links.id, p.id)),
241 phases: [phase],
242 program: { ...basis.program, phaseOrder: [phase.id] },
243 demands: [{ signalGroupId: links.id, volume: 400, heavyVehicleShare: 0 }],
244 };
245 }
246
247 describe('Befund B-7 und 4 - Abbiegerfaktor: Numerus und Trefferschaerfe', () => {
248 it('setzt den Singular, wenn es genau einen Partner gibt', () => {
249 const befunde = abbiegerfaktorBefunde(linksabbiegerMitPartnern(1));
250 expect(befunde).toHaveLength(1);
251 expect(befunde[0]!.message).toContain('ein Kraftfahrzeugstrom einer anderen Zufahrt');
252 expect(befunde[0]!.message).not.toContain('Kraftfahrzeugströme anderer Zufahrten');
253 });
254
255 it('setzt den Plural erst ab zwei Partnern', () => {
256 const befunde = abbiegerfaktorBefunde(linksabbiegerMitPartnern(2));
257 expect(befunde).toHaveLength(1);
258 expect(befunde[0]!.message).toContain('Kraftfahrzeugströme anderer Zufahrten');
259 });
260
261 it('behauptet keinen Gegenverkehr, den die Regel nicht geprueft hat', () => {
262 // Ein Rechtseinbieger aus einem anderen Arm ist fuer einen Linksabbieger
263 // kein Gegenverkehr. Die Regel meldet ihn bewusst weiterhin - der Text
264 // sagt jetzt aber, was er nicht weiss.
265 const befunde = abbiegerfaktorBefunde(linksabbiegerMitPartnern(1));
266 expect(befunde[0]!.message).toContain('ob darunter der Gegenverkehr ist, zeigt der Lageplan');
267 });
268 });