import { describe, expect, it } from 'vitest'; import { rueckstaubewertung, rueckstaulaenge } from '@/domain/rilsa/delay'; import { STAURAUMBEDARF_JE_FAHRZEUG } from '@/domain/rilsa/constants'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { loadProject } from '@/domain/model/migrate'; import type { Project } from '@/domain/model/project'; /** * Fassung 5.31.0 - der Rueckstau als Laenge, gehalten gegen den vorhandenen * Stauraum. * * WARUM: Die Leistungsfaehigkeitsrechnung fuehrte den Rueckstau seit jeher als * Fahrzeugzahl NGE, und die Planunterlage druckte sie. Fuer sich genommen ist * sie unbrauchbar - ob ein Aufstellstreifen ueberlaeuft, entscheidet eine * Laenge. * * DIE WICHTIGSTE ZUSAGE DIESER DATEI ist nicht die Umrechnung, sondern die * Ehrlichkeit ueber die verglichene Groesse: NGE ist ein MITTELWERT. Ein * Stauraum, der gerade eben reicht, reicht in der Haelfte der Umlaeufe nicht, * und das muss in jeder Ausgabe stehen. */ const DATUM = new Date('2026-01-01T00:00:00Z'); describe('Rueckstau gegen Stauraum - die reine Rechnung', () => { it('bildet die Länge aus NGE und den Fahrstreifen', () => { // 6 Kfz * 7 m / 2 Fahrstreifen = 21,0 m const bewertung = rueckstaubewertung(6, 2, 50); expect(bewertung.fahrzeuge).toBe(6); expect(bewertung.laenge).toBe(21); expect(bewertung.laenge).toBe(rueckstaulaenge(6, 2)); expect(bewertung.stauraum).toBe(50); expect(bewertung.auslastung).toBeCloseTo(0.42, 4); expect(bewertung.ueberlauf).toBe(false); }); it('meldet den Überlauf, sobald die Länge den Stauraum erreicht', () => { // Genau erreicht zaehlt schon: Der MITTLERE Rueckstau fuellt den Stauraum // dann aus, und in der Haelfte der Umlaeufe steht mehr. expect(rueckstaubewertung(10, 1, 70).ueberlauf).toBe(true); expect(rueckstaubewertung(10, 1, 70.1).ueberlauf).toBe(false); expect(rueckstaubewertung(10, 1, 60).ueberlauf).toBe(true); }); it('vergleicht nicht, wo kein Stauraum erfasst ist', () => { for (const stauraum of [null, 0, -5, Number.NaN]) { const bewertung = rueckstaubewertung(10, 1, stauraum); expect(bewertung.stauraum, String(stauraum)).toBeNull(); expect(bewertung.auslastung, String(stauraum)).toBeNull(); expect(bewertung.ueberlauf, String(stauraum)).toBe(false); // Die Laenge steht trotzdem da - sie ist die Auskunft, die auch ohne // Vergleich weiterhilft. expect(bewertung.laenge).toBe(70); } }); it('kommt mit einem leeren Rückstau zurecht', () => { const bewertung = rueckstaubewertung(0, 2, 40); expect(bewertung.fahrzeuge).toBe(0); expect(bewertung.laenge).toBe(0); expect(bewertung.ueberlauf).toBe(false); }); }); describe('Rueckstau gegen Stauraum - im Plan', () => { it('trägt die Bewertung an jeder bemessenen Signalgruppe', () => { const plan = buildSignalPlan(mitStauraum(null)); const gruppe = plan.groups.find((g) => g.delay !== null); expect(gruppe?.rueckstau, 'die Bewertung steht am Plan').toBeDefined(); expect(gruppe?.rueckstau?.laenge ?? -1).toBeGreaterThan(0); expect(gruppe?.rueckstau?.stauraum, 'ohne Eintrag kein Stauraum').toBeNull(); }); it('führt keine Bewertung ohne Rückstaugröße', () => { /* * NGE entsteht allein im HBS-Verfahren. Im HCM-Verfahren gibt es ihn * nicht, und eine 0 stuende dort fuer ein Ergebnis, das es nicht gibt. */ const basis = mitStauraum(null); const plan = buildSignalPlan({ ...basis, settings: { ...basis.settings, serviceLevelScale: 'HCM' }, }); for (const gruppe of plan.groups) expect(gruppe.rueckstau, gruppe.name).toBeNull(); }); it('rechnet die Länge je Fahrstreifen, nicht je Signalgruppe', () => { /* * GEPRUEFT WIRD DIE UMRECHNUNG IM PLAN, nicht ein Vergleich zweier * Plaene. Ein zweistreifiger Knotenpunkt hat die doppelte Kapazitaet und * dadurch ohnehin ein kleineres NGE; ein "kuerzer als einstreifig" waere * also auch dann erfuellt, wenn die Fahrstreifenzahl bei der Umrechnung * uebergangen wuerde. Genau so stand dieser Fall zuerst da. * * Der Fehler, den Punkt A5 an der Simulation behoben hat, darf * hier nicht wiederkehren. */ const basis = mitStauraum(null); const projekt: Project = { ...basis, signalGroups: basis.signalGroups.map((g) => ({ ...g, lanes: 2 })), }; const plan = buildSignalPlan(projekt); let geprueft = 0; for (const gruppe of plan.groups) { if (gruppe.rueckstau === null) continue; geprueft += 1; expect(gruppe.rueckstau.laenge, gruppe.name).toBe( rueckstaulaenge(gruppe.rueckstau.fahrzeuge, 2), ); // Kein zweiter Vergleich gegen `rueckstaulaenge(f, 1) / 2`: Beide Wege // runden auf eine Nachkommastelle, und 1,55 gegen 1,5 ist kein Befund, // sondern die Rundung. Die Zeile darueber ist die genaue Zusage. } expect(geprueft, 'kein Strom mit Rückstau').toBeGreaterThan(0); }); }); describe('Rueckstau gegen Stauraum - die Meldung', () => { it('warnt, wo der mittlere Rückstau den Stauraum erreicht', () => { const bericht = validateProject(mitStauraum(5)); const meldung = bericht.findings.find( (f) => f.rule === 'leistungsfaehigkeit.rueckstau-ueber-stauraum', ); expect(meldung, 'die Warnung fehlt').toBeDefined(); // Warnung und nicht Hinweis: Ein ueberlaufender Aufstellstreifen blockiert // den durchgehenden Fahrstreifen. expect(meldung?.severity).toBe('warnung'); expect(meldung?.target?.kind).toBe('signalgruppe'); }); it('sagt in der Meldung, dass ein Mittelwert verglichen wurde', () => { /* * DIE ZUSAGE, AN DER ALLES HAENGT. Ohne diesen Satz liest ein Bearbeiter * "Stauraum reicht" aus einer Zahl, die in der Haelfte der Umlaeufe * ueberschritten wird. */ const bericht = validateProject(mitStauraum(5)); const meldung = bericht.findings.find( (f) => f.rule === 'leistungsfaehigkeit.rueckstau-ueber-stauraum', ); expect(meldung?.message).toContain('MITTLERE'); expect(meldung?.message).toContain('Hälfte der Umläufe'); expect(meldung?.message).toContain('95-Prozent-Wert'); }); it('schweigt ohne Stauraum und bei ausreichendem Stauraum', () => { for (const stauraum of [null, 500]) { const bericht = validateProject(mitStauraum(stauraum)); expect( bericht.findings.some((f) => f.rule === 'leistungsfaehigkeit.rueckstau-ueber-stauraum'), `Stauraum ${String(stauraum)}`, ).toBe(false); } }); }); describe('Rueckstau gegen Stauraum - in der Datei', () => { it('trägt den Stauraum über Speichern und Laden', () => { const geladen = loadProject(JSON.parse(JSON.stringify(mitStauraum(45)))); expect(geladen.project.schemaVersion).toBe(19); expect(geladen.project.signalGroups[0]?.stauraumlaenge).toBe(45); expect(geladen.issues.filter((i) => i.path.includes('stauraumlaenge'))).toEqual([]); }); it('erfindet für eine Altdatei keinen Stauraum', () => { const geladen = loadProject(JSON.parse(JSON.stringify(mitStauraum(null)))); expect(geladen.project.signalGroups[0]?.stauraumlaenge).toBeUndefined(); }); it('meldet einen unlesbaren Eintrag mit der richtigen Folge', () => { /* * Die Folge ist hier eine ANDERE als bei den Geometriefeldern: Der * Stauraum geht in keine Rechnung ein, es entfaellt allein der Vergleich. * Ein gemeinsamer Satz haette an einer der beiden Stellen gelogen. */ const roh = JSON.parse(JSON.stringify(mitStauraum(45))) as Record; const gruppen = roh['signalGroups'] as Record[]; (gruppen[0] as Record)['stauraumlaenge'] = 'kurz'; const geladen = loadProject(roh); expect(geladen.project.signalGroups[0]?.stauraumlaenge).toBeUndefined(); const meldung = geladen.issues.find((i) => i.path.endsWith('.stauraumlaenge')); expect(meldung?.message).toContain('mit keiner Länge verglichen'); expect(meldung?.message).not.toContain('Sättigungsverkehrsstärke'); }); }); /** * Standardknotenpunkt mit hoher Auslastung - so entsteht ueberhaupt ein * nennenswerter Rueckstau. Der Stauraum steht an jeder Kfz-Gruppe. */ function mitStauraum(stauraum: number | null): Project { const roh = createStandardIntersectionProject('Stauraum', DATUM); return { ...roh, signalGroups: roh.signalGroups.map((gruppe) => gruppe.mode === 'kfz' && stauraum !== null ? { ...gruppe, stauraumlaenge: stauraum } : gruppe, ), demands: roh.signalGroups .filter((g) => g.mode === 'kfz') .map((g) => ({ signalGroupId: g.id, volume: 700, heavyVehicleShare: 0.05 })), }; } /** Der Kennwert steht dort, wo ihn beide Rechnungen finden. */ describe('Rueckstau gegen Stauraum - eine Umrechnung, nicht zwei', () => { it('nimmt für Simulation und Bemessung dieselbe Länge je Fahrzeug', () => { expect(STAURAUMBEDARF_JE_FAHRZEUG).toBe(7); expect(rueckstaubewertung(3, 1, null).laenge).toBe(3 * STAURAUMBEDARF_JE_FAHRZEUG); }); });