import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation/engine'; import { festeWerteNachRegelwert, festeWerteZuschreibungSatz } from '@/domain/rilsa/ansaetze'; import { RILSA_DEFAULTS, SAFETY_FLOORS } from '@/domain/rilsa/constants'; import { resolveRilsaSettings, settingFields } from '@/domain/rilsa/settings'; import type { Project } from '@/domain/model/project'; /** * Fassung 5.43.0: Zuschreibungen, die der eigene Abgleich widerlegt. * * WORUM ES GEHT. Seit der Fassung 5.27.0 liegt die gekaufte RiLSA 2015 vor, und * der Abgleich hat die beiden festen Ueberfahrzeiten des Kraftfahrzeugverkehrs * (3 s geradeaus, 2 s abbiegend) als REGELWERKSWERTE belegt - Abschnitt 2.5.2, * Faelle 1 und 2; `quellen.ts` fuehrt sie seitdem unter 'gesichert' und mit * Fundstelle. Zwei Stellen haben das nicht mitbekommen und standen auf dem Stand * ohne Druckwerk: * * 1. Der PRUEFBERICHT (validation/engine.ts, Meldung * 'zwischenzeiten.ueberfahrzeit-ansatz-fest') nannte den festen Ansatz * "ein Praxisansatz nach behördlichen Leitfäden" und behauptete, der * Wortlaut der RiLSA zu den Faellen 1 und 2 sei "öffentlich nicht * abgleichbar". Beides steht im SELBEN Dokument neben dem * Fundstellenverzeichnis, das die Abschnittsnummer nennt - und der Satz * stand unbedingt da, auch wo der Plan eigene feste Werte fuehrt. * 2. Der Warnbalken der Projektansicht (ui/views/projectView.ts) schrieb * eingetragene Vorgabewerte unbedingt der RiLSA zu und nannte die * Gelbzeitkopplung "den längeren Ansatz dieses Programms". Bei 50 km/h * geradeaus ergeben beide Ansaetze 3 s - laenger ist sie dort nicht. Diese * Datei prueft den Fachkern dahinter, die Ansicht prueft * tests/ui/herkunftFesteWerte.test.ts. * * DAZU DER KOMMENTAR in rilsa/types.ts, der den Ansatz 'fest' ebenfalls als * Praxisansatz auswies. Grundsatz: Zusagen in Kommentaren sind Teil des * Programms. * * WARUM "NIE KUERZER" UND NICHT "LAENGER" die haltbare Aussage ist: Die * Gelbzeitstaffel ist einstellbar, aber nicht unter SAFETY_FLOORS.yellowKfz - * und der liegt mit 3 s genau auf dem Regelwert geradeaus und ueber dem * abbiegenden. Der letzte Fall rechnet das nach. */ const DATUM = new Date('2026-01-01T00:00:00Z'); /** Knotenpunkt mit festem Ueberfahrzeit-Ansatz und wahlweise eigenen Vorgaben. */ function mitFestemAnsatz(rilsa: Record = {}): Project { const basis = createStandardIntersectionProject('Überfahrzeit', DATUM); return { ...basis, settings: { ...basis.settings, ueberfahrzeitAnsatz: 'fest', rilsa }, }; } function befund(project: Project): { message: string; suggestion: string } { const plan = buildSignalPlan(project); const treffer = validateProject(project, plan, DATUM).findings.find( (f) => f.rule === 'zwischenzeiten.ueberfahrzeit-ansatz-fest', ); expect(treffer, 'Die Meldung zum festen Ansatz fehlt').toBeDefined(); return { message: treffer!.message, suggestion: treffer!.suggestion }; } /** Ein Kommentar ueber mehrere Zeilen, in einer Zeile - ohne Rand und Umbruch. */ function fliessend(quelltext: string): string { return quelltext .split('\n') .map((zeile) => zeile.replace(/^\s*(\/\*+|\*\/|\*|\/\/)?\s?/u, '')) .join(' ') .replace(/\s+/gu, ' '); } describe('Der Pruefbericht schreibt den festen Ansatz dem Regelwerk zu, das ihn setzt', () => { it('nennt bei den Regelwerten die Fundstelle statt behoerdlicher Leitfaeden', () => { const { message } = befund(mitFestemAnsatz()); expect(message, 'die Zuschreibung an die RiLSA').toContain( 'Die Werte sind die Regelwerte der RiLSA 2015 (Abschnitt 2.5.2, Fälle 1 und 2)', ); expect(message, 'die Richtung des Regelansatzes').toContain('rechnet nie kürzer als diese'); expect(message, 'der Stand ohne Druckwerk').not.toContain('Praxisansatz'); expect(message, 'der Stand ohne Druckwerk').not.toContain('öffentlich nicht abgleichbar'); }); it('schreibt eigene Vorgabewerte nicht der RiLSA zu', () => { // 6 s geradeaus und 6 s abbiegend: Die groesste Gelbzeit der Regelstaffel // ist 5 s, dieser Ansatz verlaengert also jede Zwischenzeit - und die // beiden Zahlen stehen in keinem Regelwerk. const { message } = befund( mitFestemAnsatz({ crossingTimeKfzGeradeaus: 6, crossingTimeKfzAbbiegend: 6 }), ); expect(message, 'Vorbedingung: die eingetragenen Werte stehen in der Meldung').toContain('6 s'); expect(message).toContain('Die Werte sind Vorgaben dieses Plans'); expect(message, 'die belegten Zahlen gehoeren daneben').toContain( `${RILSA_DEFAULTS.crossingTime.kfzGeradeaus} s geradeaus und ` + `${RILSA_DEFAULTS.crossingTime.kfzAbbiegend} s abbiegend`, ); expect(message).not.toContain('Die Werte sind die Regelwerte der RiLSA 2015'); }); it('behauptet in der Empfehlung keine laengere Zwischenzeit', () => { /* * Hier stand "Wählen Sie „Überfahrzeit = Gelbzeit", wenn die längere und * damit konservative Zwischenzeit gelten soll." Bei 50 km/h geradeaus sind * beide Ansaetze gleich lang, und bei 6 s festen Vorgaben ist die Gelbzeit * die KUERZERE. Die Richtung sagt der Satz aus dem Fachkern, der in der * Meldung darueber steht. */ for (const vorgaben of [{}, { crossingTimeKfzGeradeaus: 6, crossingTimeKfzAbbiegend: 6 }]) { const { suggestion } = befund(mitFestemAnsatz(vorgaben)); expect(suggestion, 'die Begruendungspflicht bleibt').toContain('Begründen Sie die Wahl'); expect(suggestion, JSON.stringify(vorgaben)).not.toContain('die längere'); } }); }); describe('Der Satz zur Herkunft der festen Werte steht im Fachkern', () => { it('unterscheidet Regelwerte und Vorgaben dieses Plans an derselben Antwort', () => { expect(festeWerteNachRegelwert(RILSA_DEFAULTS)).toBe(true); const eigene = resolveRilsaSettings({ crossingTimeKfzAbbiegend: 4 }).defaults; expect(eigene.crossingTime.kfzAbbiegend, 'Vorbedingung: die Vorgabe greift').toBe(4); expect(festeWerteNachRegelwert(eigene)).toBe(false); expect(festeWerteZuschreibungSatz(RILSA_DEFAULTS)).toContain( 'Die Werte sind die Regelwerte der RiLSA 2015', ); expect(festeWerteZuschreibungSatz(eigene)).toContain('Die Werte sind Vorgaben dieses Plans'); }); it('bleibt bei "nie kürzer" auch an der kleinsten zulaessigen Gelbzeitstaffel', () => { /* * Der Beleg fuer die Aussage. Alle drei Gelbzeiten werden auf 0 gesetzt; * angesetzt wird dann SAFETY_FLOORS.yellowKfz. Liegt der nicht unter den * beiden Regelwerten, ist "rechnet nie kürzer als diese" wahr - bei jeder * zulaessigen Staffel, denn kleiner geht es nicht. */ const kleinste = resolveRilsaSettings({ yellowKfzUpTo50: 0, yellowKfzUpTo60: 0, yellowKfzAbove60: 0, }).defaults; for (const stufe of kleinste.yellowBySpeed) { expect(stufe.yellow, 'die Untergrenze greift').toBe(SAFETY_FLOORS.yellowKfz); expect(stufe.yellow).toBeGreaterThanOrEqual(RILSA_DEFAULTS.crossingTime.kfzGeradeaus); expect(stufe.yellow).toBeGreaterThanOrEqual(RILSA_DEFAULTS.crossingTime.kfzAbbiegend); } }); }); describe('Die Kommentare sagen dasselbe wie der Abgleich', () => { it('weist den Ansatz "fest" in rilsa/types.ts nicht mehr als Praxisansatz aus', () => { /* * Gemessen wird die AUSSAGE, nicht das Vorkommen der Worte: Was der * Kommentar frueher sagte, darf er weiter berichten - das ist der Sinn * eines "BERICHTIGT"-Absatzes. Geprueft wird deshalb der Teil davor. */ const quelle = fliessend(readFileSync('src/domain/rilsa/types.ts', 'utf8')); const anfang = quelle.indexOf('Rechenansatz fuer die Ueberfahrzeit'); expect(anfang, 'der Docblock zu UeberfahrzeitAnsatz').toBeGreaterThanOrEqual(0); const ende = quelle.indexOf('Signalbild eines Signalgebers', anfang); const docblock = quelle.slice(anfang, ende); expect(docblock, 'Vorbedingung: der Zweig "fest" steht darin').toContain('tue = 3 s geradeaus'); expect(docblock, 'die Fundstelle des Abgleichs').toContain('Abschnitt 2.5.2'); const berichtigt = docblock.indexOf('BERICHTIGT'); expect(berichtigt, 'der Absatz, der den alten Stand berichtet').toBeGreaterThan(0); expect(docblock.slice(0, berichtigt), 'die Zuschreibung selbst').not.toContain( 'Praxisansatz nach behoerdlichen Leitfaeden', ); }); it('nennt den Gelbzeitansatz in rilsa/constants.ts nicht mehr pauschal den laengeren', () => { /* * NACHTRAG (Fassung 5.43.0): Befund 17 war an UEBERFAHRZEIT_ANSATZ_LABELS, * im Pruefbericht, im Warnbalken und in bewertung.ts behoben - nicht aber * im Docblock des Kennwerts selbst. Dort stand "'gelbzeit' ist der * laengere, 'fest' der regelkonforme". Bei 50 km/h geradeaus ergeben beide * Ansaetze 3 s, und mit festen Vorgaben ueber der groessten Gelbzeit ist * die Kopplung die kuerzere. Der Docblock ist die Stelle, an der der * naechste Bearbeiter nachliest, welche Richtung die sichere ist. * * Geprueft wird die AUSSAGE, nicht das Vorkommen der Worte: Der Absatz, der * den alten Stand berichtet, darf ihn weiter nennen. */ const quelle = fliessend(readFileSync('src/domain/rilsa/constants.ts', 'utf8')); const anfang = quelle.indexOf('Ueberfahrzeiten tue in s'); expect(anfang, 'der Docblock zu crossingTime').toBeGreaterThanOrEqual(0); const docblock = quelle.slice(anfang, quelle.indexOf('readonly crossingTime:', anfang)); expect(docblock, 'Vorbedingung: die Wahl steht darin').toContain( 'Project.settings.ueberfahrzeitAnsatz', ); const berichtigt = docblock.indexOf('NIE KUERZER'); expect(berichtigt, 'der Absatz, der den alten Stand berichtet').toBeGreaterThan(0); const zuschreibung = docblock.slice(0, berichtigt); expect(zuschreibung, 'die Uebertreibung selbst').not.toContain("'gelbzeit' ist der laengere"); expect(zuschreibung, 'die haltbare Aussage').toContain('nie kuerzer'); // Und die Begruendungspflicht haengt am Ansatz, nicht an "laenger" oder // "kuerzer": Der Pruefbericht meldet allein 'fest' (checkUeberfahrzeitAnsatz). expect(zuschreibung).not.toContain('fuer den laengeren keine Begruendung'); }); it('trennt in rilsa/constants.ts den Ansatz von seinen beiden Werten', () => { /* * NACHTRAG (Fassung 5.43.0): An die Stelle der Uebertreibung trat "'fest' * ist der Regelwerksansatz selbst" - richtig fuer den ANSATZ, zu stark fuer * seine WERTE. Beide Felder sind von 1 bis 10 s einstellbar, und genau * diese Verwechslung war Befund B5 der Fassung 5.5.0: Wer sie liest, * schreibt eingetragene Vorgaben der RiLSA zu. Der letzte Ausdruck dieses * Falls misst, dass die Werte wirklich einstellbar sind - ein Kommentar * ueber eine Schranke ist nur so lange richtig, wie die Schranke ihn * traegt. */ const quelle = fliessend(readFileSync('src/domain/rilsa/constants.ts', 'utf8')); const anfang = quelle.indexOf('Ueberfahrzeiten tue in s'); const docblock = quelle.slice(anfang, quelle.indexOf('readonly crossingTime:', anfang)); const zuschreibung = docblock.slice(0, docblock.indexOf('NIE KUERZER')); expect(zuschreibung, 'die Zahl des Ansatzes ist nicht der Ansatz').not.toContain( "'fest' ist der Regelwerksansatz selbst", ); expect(zuschreibung, 'der Verweis auf die Antwort im Fachkern').toContain( 'festeWerteNachRegelwert', ); expect(zuschreibung, 'und die Schranke, die es noetig macht').toContain('von 1 bis 10 s'); const felder = settingFields().filter( (f) => f.key === 'crossingTimeKfzGeradeaus' || f.key === 'crossingTimeKfzAbbiegend', ); expect(felder, 'Vorbedingung: beide Felder gibt es').toHaveLength(2); for (const feld of felder) { expect([feld.min, feld.max], feld.key).toEqual([1, 10]); } }); it('beschreibt in conflictsView.ts die Regel zwischenzeiten.null so, wie sie meldet', () => { /* * Der Kommentar an der Legende der Zwischenzeitenmatrix berief sich darauf, * die Pruefregel halte 0 s "bei langen Furten" fuer den Regelfall und * zulaessig. Diesen Satz gibt es seit 5.43.0 nicht mehr: Die Regel nennt * die Bedingung, unter der 0 s zulaessig sind, und schweigt an Beziehungen, * die schon als kuerzer als nach dem Lageplan gemeldet sind. Den Wortlaut * der Regel selbst bewacht tests/domain/furtrichtung.test.ts; hier steht, * dass der Kommentar der Ansicht ihn nicht mehr falsch wiedergibt. */ const quelle = fliessend(readFileSync('src/ui/views/conflictsView.ts', 'utf8')); expect(quelle, 'Vorbedingung: die Legende zur 0 s steht in der Datei').toContain( 'Zwischenzeit 0 s – nachzuprüfen', ); expect(quelle).not.toContain('bei langen Furten der Regelfall und zulaessig'); expect(quelle, 'was die Regel wirklich tut').toContain( 'nennt die Bedingung, unter der er zulaessig ist', ); }); });