lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain ueberholteZuschreibungen.test.ts
| 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 | }); |