import { describe, expect, it } from 'vitest'; import { computeIntergreen } from '@/domain/rilsa/intergreen'; import { schwerverkehrsfaktor } from '@/domain/rilsa/capacity'; import { HBS_PKW_GLEICHWERTE, RILSA_DEFAULTS } from '@/domain/rilsa/constants'; import { QUELLEN } from '@/domain/rilsa/quellen'; import { resolveRilsaSettings, settingFields, wirkungslosigkeit } from '@/domain/rilsa/settings'; import type { ClearingStream, EnteringStream } from '@/domain/rilsa/types'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation/engine'; import { loadProject } from '@/domain/model/migrate'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { CURRENT_SCHEMA_VERSION, type Project } from '@/domain/model/project'; /** * Fassung 5.5.0, Fragen E1, E2, E5 und E6 - die Ersatzbelege aus oeffentlichen * Quellen. * * Diese vier Fragen galten bis zum 15.08.2026 als "nur gegen die Druckausgabe * pruefbar". Eine Recherche hat oeffentliche Ersatzbelege gehoben; massgebend * ist, was einer kritischen Pruefung jedes einzelnen Belegs standhielt - * mehrere Belege hielten ihr nicht stand. * * E6 (Rechenaenderung) - BASt-Bericht V 400 (2025), S. 10, woertlich: * "fSV = (qLV + 1,75 * qLkw+Bus + 2,5 * qLkwK) / qKfz (2-5)" * "fSV = (qLV + 1,9 * qSV) / qKfz (2-6)" * "tB,j = fSV * f1 * f2 * 1,8" - "qS,j = 3600 / tB,j" * und S. 68 zur Herkunft des bisherigen Programmwerts: * "Im HCM 2010 (TRB, 2010) wurde an LSA ein konstanter Pkw-Gleichwert von * 2,00 verwendet." * Der Altstand rechnete `qS = 2000 / (1 + pSV)` - genau dieser HCM-Wert, unter * der HBS-Quelle gefuehrt. * * E2 (waehlbar, Vorgabe unveraendert) - Hamburger LSA-Leitfaden, Anlage A: * "geradeausfahrende Kfz (schnell): vr 10 m/s, tue 3 s, lFZ 6 m" * "abbiegende Kfz (langsam): vr 7 m/s, tue 2 s, lFZ 6 m" * Die 60-km/h-Spalte liest bei s0 = 14 m den Wert 5,0 s - mit tue = 4 s * muessten dort 6,0 s stehen. Eine geschwindigkeitsabhaengige Ueberfahrzeit ist * damit arithmetisch ausgeschlossen. Der Wortlaut der RiLSA zu den Faellen 1 * und 2 bleibt oeffentlich unzugaenglich; deshalb waehlbar, nicht umgestellt. * * E1 (waehlbar, Vorgabe AUS) - eine einzige Quellenlinie fuer "5 m/s bei * Kurvenradius unter 10 m". Aus dem Regelwerk woertlich belegt ist nur die * qualitative Radfahrerregel (Fall 5, verdeckte Textschicht des * FGSV-Aenderungsblatts): "Sie ist zu verringern, wenn Radfahrer vor oder der * Radfahrerfurt sehr enge Boegen durchfahren." * * E5 (Beleglage) - die drei QSV-Tafeln sind zweifach herausgeberseitig belegt * (FGSV-Einfuehrungskolloquium 2015, Folie "Qualitaetskriterien"; BASt V 400 * Tab. 2-2). */ const DATUM = new Date('2026-01-01T00:00:00Z'); // --- E6: Schwerverkehrsfaktor ---------------------------------------------- describe('E6 - Schwerverkehr nach HBS 2015 statt Pkw-Gleichwert 2,0', () => { it('haelt die belegten Pkw-Gleichwerte fest', () => { // Die drei Zahlen aus V 400, S. 68. Sie stehen als benannte Konstanten und // nicht mehr hart in plan/signalPlan.ts. expect(HBS_PKW_GLEICHWERTE.lkwUndBus).toBe(1.75); expect(HBS_PKW_GLEICHWERTE.lkwMitAnhaenger).toBe(2.5); expect(HBS_PKW_GLEICHWERTE.schwerverkehrPauschal).toBe(1.9); }); it('rechnet Gl. 2-6 als fSV = 1 + 0,90 * pSV', () => { expect(schwerverkehrsfaktor(0)).toBe(1); expect(schwerverkehrsfaktor(0.25)).toBeCloseTo(1.225, 12); expect(schwerverkehrsfaktor(1)).toBeCloseTo(1.9, 12); }); it('trifft die Belegzahlen der Saettigungsverkehrsstaerke', () => { // Standardbedingungen, reiner Pkw-Verkehr: 1,8 s je Kfz -> 2000 Kfz/h. const grund = RILSA_DEFAULTS.capacity.saturationFlow; expect(grund).toBe(2000); expect(grund / schwerverkehrsfaktor(0)).toBe(2000); // 25 % Schwerverkehr: 2000 / 1,225 = 1632,7 Kfz/h. expect(grund / schwerverkehrsfaktor(0.25)).toBeCloseTo(1632.653, 3); }); it('bestaetigt Gl. 2-6 aus Gl. 2-5 fuer den in V 400 genannten Lastzuganteil', () => { /* * V 400 zu Bild 2-1 d): "Bei einem Anteil der Lkw mit Anhaenger und * Sattel-Kfz (Fahrzeugklasse LkwK) am Schwerverkehr von 20 % ergibt sich * der Funktionsverlauf der Gleichung (2-6)." * * Nachgerechnet: mit 20 % LkwK am Schwerverkehr ist * fSV = 1 + 0,75*0,8*p + 1,50*0,2*p = 1 + 0,9*p - Gl. 2-5 und Gl. 2-6 * fallen dort exakt zusammen. Das ist die Gegenprobe darauf, dass beide * Gleichungen richtig umgesetzt sind: Ein Vorzeichen- oder * Bezugsgroessenfehler in einer der beiden wuerde sie auseinanderfallen * lassen. */ for (const pSV of [0, 0.05, 0.1, 0.2, 0.35, 0.5]) { const ausAufteilung = schwerverkehrsfaktor(pSV, { lkwUndBus: 0.8 * pSV, lkwMitAnhaenger: 0.2 * pSV, }); expect(ausAufteilung, `pSV ${pSV}`).toBeCloseTo(schwerverkehrsfaktor(pSV), 12); } }); it('laesst den Schwerverkehr nicht groesser werden als den Verkehr', () => { // Unmoegliche Aufteilung: 80 % Lkw + 80 % Lastzuege. Ohne Begrenzung // ergaebe sich fSV = 2,8 - eine Saettigungsverkehrsstaerke von 714 Kfz/h, // die kein Fahrstreifen hat. Gekuerzt wird der leichtere Anteil. expect(schwerverkehrsfaktor(0, { lkwUndBus: 0.8, lkwMitAnhaenger: 0.8 })).toBeCloseTo( 1 + 0.75 * 0.2 + 1.5 * 0.8, 12, ); expect(schwerverkehrsfaktor(0, { lkwUndBus: 1, lkwMitAnhaenger: 1 })).toBeCloseTo(2.5, 12); }); it('rechnet den Lastzugfall mit Gl. 2-5 deutlich unguenstiger', () => { // 20 % Schwerverkehr, davon alles Lastzuege: fSV = 1 + 1,50*0,20 = 1,300. const nurLastzuege = schwerverkehrsfaktor(0.2, { lkwUndBus: 0, lkwMitAnhaenger: 0.2 }); expect(nurLastzuege).toBeCloseTo(1.3, 12); expect(2000 / nurLastzuege).toBeCloseTo(1538.46, 2); // Der Altstand kam hier auf 2000/1,2 = 1667 Kfz/h, also 8,3 % zu gross - // die eigentliche unsichere Richtung, die Gl. 2-5 abfaengt. expect(2000 / 1.2 / (2000 / nurLastzuege)).toBeCloseTo(1.083, 3); }); it('nennt die Fehlerrichtung gegenueber dem Altstand ehrlich', () => { /* * MUTATIONSNACHWEIS zugleich: Der Altstand rechnete 2000/(1+pSV). Diese * Erwartungen schlagen gegen ihn fehl, weil er bei jedem pSV > 0 einen * anderen Wert liefert. */ for (const pSV of [0.05, 0.1, 0.2, 0.3]) { const neu = 2000 / schwerverkehrsfaktor(pSV); const alt = 2000 / (1 + pSV); // Weniger konservativ: die Kapazitaet faellt groesser aus als bisher. expect(neu, `pSV ${pSV}`).toBeGreaterThan(alt); // Und zwar um hoechstens 2,3 % - die Zahl, die in die Unterlagen gehoert. expect(neu / alt).toBeLessThanOrEqual(1.024); } expect(2000 / schwerverkehrsfaktor(0.3) / (2000 / 1.3)).toBeCloseTo(1.0236, 4); }); it('setzt den Faktor im Signalzeitenplan ein - nicht mehr das feste 2,0', () => { const roh = createStandardIntersectionProject('Schwerverkehr', DATUM); const k1 = roh.signalGroups.find((g) => g.name === 'K1')!; expect(k1.movement).toBe('geradeaus'); expect(k1.lanes).toBe(1); const project: Project = { ...roh, demands: [{ signalGroupId: k1.id, volume: 600, heavyVehicleShare: 0.25 }], }; const plan = buildSignalPlan(project); const gruppe = plan.groups.find((g) => g.groupId === k1.id); expect(gruppe?.capacity).not.toBeNull(); // 2000 / 1,225 = 1632,65 - der Altstand lieferte hier 2000/1,25 = 1600. expect(gruppe!.capacity!.saturationFlow).toBeCloseTo(1632.653, 3); expect(gruppe!.capacity!.saturationFlow).not.toBeCloseTo(1600, 1); }); it('fuehrt Gl. 2-5 nicht als wirkungslosen Kennwert in der Vorgabenverwaltung', () => { // Befund C20: keine Zahl unter der Ueberschrift "Vorgaben", die in kein // Ergebnis eingeht. Die Pkw-Gleichwerte sind deshalb Konstanten mit Beleg // und kein Vorgabenfeld. const schluessel = settingFields().map((f) => String(f.key)); for (const verboten of ['pkwGleichwertLkw', 'pkwGleichwertLastzug', 'schwerverkehrsfaktor']) { expect(schluessel).not.toContain(verboten); } }); it('sagt im Fundstellenverzeichnis, welche Gleichung gerechnet wird', () => { const hinweis = QUELLEN['hbs-kapazitaet'].hinweis ?? ''; expect(hinweis).toContain('1,75'); expect(hinweis).toContain('2,5'); expect(hinweis).toContain('1,9'); // Die falsche Zuschreibung ist benannt, nicht spurlos verschwunden. expect(hinweis).toContain('HCM 2010'); // Und die Grenzen der Rechnung stehen dabei. expect(hinweis).toContain('Standardbedingungen'); expect(hinweis).toContain('Mischfahrstreifen'); }); }); // --- E2: Ueberfahrzeit-Ansatz ---------------------------------------------- /** Abbiegender Kfz-Strom, 20 m Raeumweg, ohne Einfahrweg. */ function abbieger(overrides: Partial = {}): ClearingStream { return { mode: 'kfz', movement: 'rechts', clearingDistance: 20, vehicleClass: 'pkw', vZul: 70, ...overrides, }; } const OHNE_EINFAHRWEG: EnteringStream = { mode: 'kfz', movement: 'geradeaus', enteringDistance: 0 }; describe('E2 - Ueberfahrzeit als waehlbarer Ansatz', () => { it('rechnet ohne Angabe wie bisher mit der Gelbzeit', () => { // Kein Feld gesetzt: der Stand bis Schema 11. Bei 70 km/h sind das 5 s. const ohne = computeIntergreen(abbieger(), OHNE_EINFAHRWEG); const ausdruecklich = computeIntergreen( abbieger({ ueberfahrzeitAnsatz: 'gelbzeit' }), OHNE_EINFAHRWEG, ); expect(ohne.crossingTime).toBe(5); expect(ausdruecklich.crossingTime).toBe(5); // tue 5 s + (20 + 6)/7 = 8,714 -> 9 s expect(ohne.intergreen).toBe(9); expect(ohne.raw).toBeCloseTo(8.714, 3); }); it('rechnet bei "fest" mit 3 s geradeaus und 2 s abbiegend', () => { const abbiegend = computeIntergreen(abbieger({ ueberfahrzeitAnsatz: 'fest' }), OHNE_EINFAHRWEG); expect(abbiegend.crossingTime).toBe(2); // tue 2 s + 26/7 = 5,714 -> 6 s, also 3 s kuerzer als mit Gelbzeitkopplung. expect(abbiegend.intergreen).toBe(6); const geradeaus = computeIntergreen( abbieger({ movement: 'geradeaus', ueberfahrzeitAnsatz: 'fest' }), OHNE_EINFAHRWEG, ); expect(geradeaus.crossingTime).toBe(3); // vr geradeaus 10 m/s: 3 + 26/10 = 5,6 -> 6 s expect(geradeaus.intergreen).toBe(6); }); it('bleibt bei 50 km/h geradeaus in beiden Ansaetzen gleich', () => { // Dort faellt die Gelbzeit (3 s) mit dem festen Wert zusammen - die Wahl // aendert nichts, und der Test haelt fest, dass sie es auch nicht soll. const gemeinsam = { movement: 'geradeaus' as const, vZul: 50 }; const gelb = computeIntergreen(abbieger(gemeinsam), OHNE_EINFAHRWEG); const fest = computeIntergreen( abbieger({ ...gemeinsam, ueberfahrzeitAnsatz: 'fest' }), OHNE_EINFAHRWEG, ); expect(fest.crossingTime).toBe(gelb.crossingTime); expect(fest.intergreen).toBe(gelb.intergreen); }); it('haelt die Untergrenze tue + tr >= tG + 1 s ein', () => { /* * UMGEDREHT MIT 5.27.0. Hier stand das Gegenteil: Die Regel sei "NICHT * eingebaut", weil sie "als woertliche Quelle nur eine Webseite" habe, * "die sich im selben Text widerspricht". Sie steht in der RiLSA 2015, * Abschnitt 2.5.2, unmittelbar vor der Aufzaehlung der sechs Faelle, und * gilt den Faellen 1 und 2. * * Bei 70 km/h ist tG = 5 s, die Untergrenze also 6 s. Mit festem Ansatz * und 1 m Raeumweg ergibt die Rechnung 2 + 7/7 = 3,0 s; angesetzt werden * 6 s. Dieselbe Beziehung lieferte bis 5.26.0 3 s. */ const kurz = computeIntergreen( abbieger({ clearingDistance: 1, ueberfahrzeitAnsatz: 'fest' }), OHNE_EINFAHRWEG, ); expect(kurz.intergreen).toBe(6); expect(kurz.ueberfahrRaeumzeitUntergrenze).toBe(6); // Die Einzelwerte bleiben, was sie sind - angehoben wird die SUMME. expect(kurz.crossingTime).toBe(2); expect(kurz.clearingTime).toBe(1); const meldung = kurz.notes.find((n) => n.code === 'ueberfahr-raeumzeit-untergrenze'); expect(meldung?.severity).toBe('hinweis'); expect(meldung?.message).toContain('2.5.2'); }); it('meldet die Untergrenze nicht, wo sie nicht greift', () => { // Derselbe Strom mit langem Raeumweg: 2 + 26/7 = 5,71 s liegt unter 6 s // und wird angehoben - mit 40 m dagegen 2 + 46/7 = 8,57 s, darueber. const lang = computeIntergreen( abbieger({ clearingDistance: 40, ueberfahrzeitAnsatz: 'fest' }), OHNE_EINFAHRWEG, ); expect(lang.ueberfahrRaeumzeitUntergrenze).toBeNull(); expect(lang.intergreen).toBe(9); expect(lang.notes.some((n) => n.code === 'ueberfahr-raeumzeit-untergrenze')).toBe(false); }); it('laesst Rad, Fuss und OePNV von der Untergrenze unberuehrt', () => { /* * Die RiLSA nennt die Bedingung ausdruecklich nur fuer die Faelle 1 und 2 - * geradeaus fahrende und abbiegende Kraftfahrzeuge. Sie auf die uebrigen * Verkehrsarten zu uebertragen hiesse, das Regelwerk zu ergaenzen; beim * OePNV gaebe es das tG dafuer nicht einmal. */ const rad = computeIntergreen( { mode: 'rad', movement: 'geradeaus', clearingDistance: 1, vehicleClass: 'rad', vZul: 70 }, OHNE_EINFAHRWEG, ); expect(rad.ueberfahrRaeumzeitUntergrenze).toBeNull(); // 1 s Ueberfahrzeit + 1/4 s = 1,25 -> 2 s. Mit Untergrenze waeren es 6 s. expect(rad.intergreen).toBe(2); }); it('laesst die Ueberfahrzeit auch bei "fest" nicht unterschreiten', () => { // Die harte Kfz-Schranke gilt in beiden Ansaetzen - sonst waere ein // Wechsel des Ansatzes der Weg, aus einem Fehler eine Warnung zu machen. const ergebnis = computeIntergreen( abbieger({ ueberfahrzeitAnsatz: 'fest', crossingTimeOverride: 0 }), OHNE_EINFAHRWEG, ); expect(ergebnis.crossingTime).toBe(2); const meldung = ergebnis.notes.find((n) => n.code === 'ueberfahrzeit-unter-gelbzeitschranke'); expect(meldung?.severity).toBe('fehler'); // Und sie behauptet nicht mehr, der Wert sei die Gelbzeit. expect(meldung?.message).toContain('feste'); expect(meldung?.message).not.toContain('die Gelbzeit von'); }); it('nimmt die Projektwahl in den Plan und meldet sie als Abweichung', () => { const roh = createStandardIntersectionProject('Ansatz', DATUM); const gelbzeit = buildSignalPlan(roh); const fest = buildSignalPlan({ ...roh, settings: { ...roh.settings, ueberfahrzeitAnsatz: 'fest' }, }); // Der Musterknotenpunkt faehrt 50 km/h geradeaus; dort sind beide Ansaetze // gleich. Massgebend ist deshalb, dass die Wahl ueberhaupt ankommt: const k1 = roh.signalGroups.find((g) => g.name === 'K1')!; const f2 = roh.signalGroups.find((g) => g.name === 'F2')!; const schluessel = `${k1.id}|${f2.id}`; expect(gelbzeit.intergreens.get(schluessel)?.calculation?.crossingTime).toBe(3); expect(fest.intergreens.get(schluessel)?.calculation?.crossingTime).toBe(3); // Bei 70 km/h trennen sich die Wege um 2 s. const schnell: Project = { ...roh, signalGroups: roh.signalGroups.map((g) => (g.id === k1.id ? { ...g, vZul: 70 } : g)), settings: { ...roh.settings, ueberfahrzeitAnsatz: 'fest' }, }; const schnellPlan = buildSignalPlan(schnell); expect(schnellPlan.intergreens.get(schluessel)?.calculation?.crossingTime).toBe(3); const schnellGelb = buildSignalPlan({ ...schnell, settings: roh.settings }); expect(schnellGelb.intergreens.get(schluessel)?.calculation?.crossingTime).toBe(5); const bericht = validateProject( { ...roh, settings: { ...roh.settings, ueberfahrzeitAnsatz: 'fest' } }, fest, DATUM, ); const befund = bericht.findings.find( (f) => f.rule === 'zwischenzeiten.ueberfahrzeit-ansatz-fest', ); expect(befund?.severity).toBe('warnung'); expect(befund?.message).toContain('VERKÜRZT'); expect(befund?.suggestion).toContain('Begründen Sie die Wahl'); // Die Vorgabe erzeugt keinen Befund. const ohne = validateProject(roh, gelbzeit, DATUM); expect(ohne.findings.map((f) => f.rule)).not.toContain( 'zwischenzeiten.ueberfahrzeit-ansatz-fest', ); }); it('kennzeichnet die festen Werte als wirkungslos, solange die Gelbzeit gilt', () => { // Befund C20 in neuer Gestalt: Die beiden Vorgabenfelder gibt es, aber sie // geben nicht vor zu wirken, wenn der Ansatz sie nicht abruft. expect( wirkungslosigkeit('praxis-ueberfahrzeit-fest', 'HBS', { ueberfahrzeitAnsatz: 'gelbzeit' }), ).not.toBeNull(); expect( wirkungslosigkeit('praxis-ueberfahrzeit-fest', 'HBS', { ueberfahrzeitAnsatz: 'fest' }), ).toBeNull(); // Ohne Kenntnis des Ansatzes wird NICHTS behauptet - lieber eine // Begruendung zu viel verlangen als eine Wirkung zu Unrecht bestreiten. expect(wirkungslosigkeit('praxis-ueberfahrzeit-fest', 'HBS')).toBeNull(); }); /* * Gegenstueck zum Vorherigen (Fassung 5.5.0, Befund B6): * Dieselbe Lage, dieselbe Auskunft - bis dahin bekam der eine Kennwert einen * Vermerk und der andere keinen, ohne dass der Quelltext den Unterschied * begruendete. */ it('kennzeichnet die Raeumgeschwindigkeit bei engem Innenradius als wirkungslos, solange kein Merkmal gesetzt ist', () => { expect( wirkungslosigkeit('praxis-enger-radius', 'HBS', { engerRadiusGesetzt: false }), ).not.toBeNull(); expect( wirkungslosigkeit('praxis-enger-radius', 'HBS', { engerRadiusGesetzt: true }), ).toBeNull(); expect(wirkungslosigkeit('praxis-enger-radius', 'HBS')).toBeNull(); }); it('haelt die belegten Werte in den Vorgaben und laesst sie anpassen', () => { expect(RILSA_DEFAULTS.crossingTime.kfzGeradeaus).toBe(3); expect(RILSA_DEFAULTS.crossingTime.kfzAbbiegend).toBe(2); const { defaults } = resolveRilsaSettings({ crossingTimeKfzAbbiegend: 2.5 }); expect(defaults.crossingTime.kfzAbbiegend).toBe(2.5); const ergebnis = computeIntergreen( abbieger({ ueberfahrzeitAnsatz: 'fest' }), OHNE_EINFAHRWEG, defaults, ); expect(ergebnis.crossingTime).toBe(2.5); }); }); // --- E1: enger Innenradius -------------------------------------------------- describe('E1 - enger Innenradius als Merkmal der Konfliktbeziehung', () => { it('raeumt mit 5,0 m/s statt 7,0 m/s und verlaengert die Zwischenzeit', () => { const ohne = computeIntergreen(abbieger(), OHNE_EINFAHRWEG); const mit = computeIntergreen(abbieger({ engerRadius: true }), OHNE_EINFAHRWEG); expect(ohne.clearingSpeed).toBe(7); expect(mit.clearingSpeed).toBe(5); // 5 s + 26/5 = 10,2 -> 11 s gegen 9 s: die sichere Richtung. expect(mit.intergreen).toBe(11); expect(mit.intergreen).toBeGreaterThan(ohne.intergreen); }); it('wirkt nur beim Abbiegen', () => { const geradeaus = computeIntergreen( abbieger({ movement: 'geradeaus', engerRadius: true }), OHNE_EINFAHRWEG, ); expect(geradeaus.clearingSpeed).toBe(RILSA_DEFAULTS.clearingSpeed.kfzStraight); }); it('ist ohne Angabe aus - Altdateien rechnen unveraendert', () => { expect(RILSA_DEFAULTS.clearingSpeed.kfzTurningEngerRadius).toBe(5); const ohneFeld = computeIntergreen(abbieger(), OHNE_EINFAHRWEG); const ausdruecklichAus = computeIntergreen(abbieger({ engerRadius: false }), OHNE_EINFAHRWEG); expect(ohneFeld.clearingSpeed).toBe(7); expect(ausdruecklichAus.clearingSpeed).toBe(7); }); it('meldet das gesetzte Merkmal als begruendungspflichtige Abweichung', () => { const roh = createStandardIntersectionProject('Radius', DATUM); const k1 = roh.signalGroups.find((g) => g.name === 'K1')!; const f2 = roh.signalGroups.find((g) => g.name === 'F2')!; const project: Project = { ...roh, // K1 biegt hier rechts ab und quert dabei die Furt F2. signalGroups: roh.signalGroups.map((g) => g.id === k1.id ? { ...g, movement: 'rechts' } : g, ), conflicts: roh.conflicts.map((c) => c.fromId === k1.id && c.toId === f2.id ? { ...c, engerRadius: true } : c, ), }; const bericht = validateProject(project, buildSignalPlan(project), DATUM); const befund = bericht.findings.find((f) => f.rule === 'zwischenzeiten.enger-radius'); expect(befund?.severity).toBe('hinweis'); // Dieselbe Schreibweise wie in Ansicht und Ausdruck (ansaetze.ts): "5,0 m/s" // und nicht "5 m/s" - eine Geschwindigkeit, die je Ausgabe anders aussieht, // liest sich wie zwei verschiedene Werte. expect(befund?.message).toContain('5,0 m/s'); expect(befund?.message).toContain('7,0 m/s'); expect(befund?.message).toContain('länger'); expect(befund?.message).toContain('RiLSA 2015'); // Seit 5.27.0 die RiLSA und nicht mehr "Praxisansatz": Abschnitt 2.5.2, // Fall 2 nennt die 5 m/s und die Radiusschwelle. expect(befund?.reference).toContain('RiLSA 2015'); expect(befund?.reference).not.toContain('Praxisansatz'); }); it('meldet ein Merkmal, das an dieser Beziehung nichts bewirkt', () => { // Gesetzt an einem geradeausfahrenden Strom: Das Kaestchen steht an, die // Zwischenzeit aendert sich nicht. Schweigen waere hier die stille // Ersetzung, gegen die die Befunde C7 bis C12 angetreten sind. const roh = createStandardIntersectionProject('Radius ohne Wirkung', DATUM); const k1 = roh.signalGroups.find((g) => g.name === 'K1')!; const f2 = roh.signalGroups.find((g) => g.name === 'F2')!; const project: Project = { ...roh, conflicts: roh.conflicts.map((c) => c.fromId === k1.id && c.toId === f2.id ? { ...c, engerRadius: true } : c, ), }; const bericht = validateProject(project, buildSignalPlan(project), DATUM); const befund = bericht.findings.find( (f) => f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung', ); expect(befund?.severity).toBe('warnung'); expect(befund?.message).toContain('geht'); }); it('laesst den Wert nicht ueber die Raeumgeschwindigkeit des Regelfalls steigen', () => { // Ein "enger Innenradius", der schneller raeumt als der Regelabbieger, // waere eine Verkuerzung durch die Hintertuer. const feld = settingFields().find((f) => f.key === 'clearingSpeedKfzTurningEngerRadius'); expect(feld?.max).toBe(RILSA_DEFAULTS.clearingSpeed.kfzTurning); const { defaults, notes } = resolveRilsaSettings({ clearingSpeedKfzTurningEngerRadius: 9 }); expect(defaults.clearingSpeed.kfzTurningEngerRadius).toBe(7); expect(notes.some((n) => n.code.includes('clearingSpeedKfzTurningEngerRadius'))).toBe(true); }); }); // --- Schemastand 12 --------------------------------------------------------- describe('Schemastand 12 - Rundreise und Altdateien', () => { it('steht inzwischen auf 13 - die Felder aus Schema 12 bleiben', () => { // Die Felder engerRadius und ueberfahrzeitAnsatz kamen mit // Schema 12. Mit dem Lastzuganteil ist die Zahl auf 13 gestiegen // (TrafficDemand.lastzugAnteil); geprueft wird hier weiter, dass die // Angaben aus 12 Speichern und Laden unveraendert ueberstehen. expect(CURRENT_SCHEMA_VERSION).toBe(19); }); it('traegt beide neuen Angaben durch Speichern und Laden', () => { const roh = createStandardIntersectionProject('Rundreise', DATUM); const erster = roh.conflicts[0]!; const project: Project = { ...roh, conflicts: roh.conflicts.map((c) => (c.id === erster.id ? { ...c, engerRadius: true } : c)), settings: { ...roh.settings, ueberfahrzeitAnsatz: 'fest' }, }; const geladen = loadProject(JSON.parse(JSON.stringify(project)), DATUM); expect(geladen.issues).toEqual([]); expect(geladen.project.schemaVersion).toBe(CURRENT_SCHEMA_VERSION); expect(geladen.project.settings.ueberfahrzeitAnsatz).toBe('fest'); expect(geladen.project.conflicts.find((c) => c.id === erster.id)?.engerRadius).toBe(true); // Und die uebrigen Beziehungen bekommen das Feld NICHT hinzugeschrieben. expect(geladen.project.conflicts.filter((c) => 'engerRadius' in c)).toHaveLength(1); }); it('liest eine Datei aus Schemastand 11 ohne Meldung und ohne Rechenaenderung', () => { const roh = createStandardIntersectionProject('Altdatei', DATUM); const alt = JSON.parse(JSON.stringify(roh)) as Record; alt['schemaVersion'] = 11; // Genau der Zustand einer Datei bis Fassung 5.4.0: kein Ansatz, kein // Radiusmerkmal. delete (alt['settings'] as Record)['ueberfahrzeitAnsatz']; for (const conflict of alt['conflicts'] as Record[]) { delete conflict['engerRadius']; } const geladen = loadProject(alt, DATUM); expect(geladen.issues).toEqual([]); expect(geladen.project.settings.ueberfahrzeitAnsatz).toBe('gelbzeit'); expect(geladen.project.conflicts.every((c) => c.engerRadius === undefined)).toBe(true); // Und die Zwischenzeiten sind dieselben wie vor der Umstellung. const vorher = buildSignalPlan(roh); const nachher = buildSignalPlan(geladen.project); for (const [schluessel, wert] of vorher.intergreens) { expect(nachher.intergreens.get(schluessel)?.value, schluessel).toBe(wert.value); } const bericht = validateProject(geladen.project, nachher, DATUM); for (const regel of [ 'zwischenzeiten.ueberfahrzeit-ansatz-fest', 'zwischenzeiten.enger-radius', 'zwischenzeiten.enger-radius-ohne-wirkung', ]) { expect(bericht.findings.map((f) => f.rule)).not.toContain(regel); } }); it('faellt bei unlesbarem Ansatz auf die konservative Seite zurueck', () => { const roh = createStandardIntersectionProject('Unlesbar', DATUM); const kaputt = JSON.parse(JSON.stringify(roh)) as Record; (kaputt['settings'] as Record)['ueberfahrzeitAnsatz'] = 'irgendwas'; const geladen = loadProject(kaputt, DATUM); expect(geladen.project.settings.ueberfahrzeitAnsatz).toBe('gelbzeit'); // Nicht stillschweigend: Der Bericht der Datei sagt, was ersetzt wurde. expect(geladen.issues.some((i) => i.path === 'settings.ueberfahrzeitAnsatz')).toBe(true); }); }); // --- E5 und E7: Beleglage --------------------------------------------------- describe('E5 und E7 - Pruefstaende nach der neuen Beleglage', () => { it('fuehrt die Qualitaetsstufen als gesichert und sagt, worauf das beruht', () => { const quelle = QUELLEN['hbs-qualitaetsstufen']; expect(quelle.pruefstand).toBe('gesichert'); const hinweis = quelle.hinweis ?? ''; // Zwei Herausgeberquellen, zehn Jahre auseinander. expect(hinweis).toContain('FGSV-Einführungskolloquium'); expect(hinweis).toContain('V 400'); // Alle drei Tafeln. expect(hinweis).toContain('20/35/50/70'); expect(hinweis).toContain('5/15/25/40/60'); expect(hinweis).toContain('30/40/55/70/85'); // Und die Einschraenkung: zwei Wiedergaben EINES Originals. expect(hinweis).toContain('Wiedergaben eines Originals'); // Die Tabelle D-4 des BASt-QM-Leitfadens ist ausdruecklich ausgeschlossen. expect(hinweis).toContain('HBS 2001'); }); it('nennt die belegte Ermittlung der maximalen Wartezeit', () => { // V 400 unmittelbar unter Tab. 2-2 - das stuetzt die Umsetzung aus B4. expect(QUELLEN['hbs-qualitaetsstufen'].hinweis).toContain('Sperrzeit'); }); it('traegt die 95-Prozent-Regel des FGSV-Dialogs am Abbiegerfaktor', () => { expect(QUELLEN['hbs-abminderung'].hinweis).toContain('x ≥ 95 %'); expect(QUELLEN['hbs-abminderung'].hinweis).toContain('0 Kfz/h'); }); it('sagt bei den Rad- und Fusswerten, was woertlich belegt ist', () => { const hinweis = QUELLEN['rilsa-zwischenzeiten'].hinweis ?? ''; expect(hinweis).toContain('vr = 4 m/s'); expect(hinweis).toContain('tü = 0 s'); expect(hinweis).toContain('vr = 1,2 m/s'); // Und dass dieses Programm strenger ist als das Zitat. expect(hinweis).toContain('1,5 m/s'); expect(hinweis).toContain('1,2 m/s'); }); it('nimmt den RiLSA-Bezug vom empfohlenen Umlaufzeitbereich', () => { const hinweis = QUELLEN['rilsa-umlaufzeit-regelbereich'].hinweis ?? ''; expect(QUELLEN['rilsa-umlaufzeit-regelbereich'].pruefstand).toBe('abzugleichen'); expect(hinweis).toContain('50 bis 75'); expect(hinweis).toContain('an keine weiteren Vorgaben gebunden'); }); });