import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { TrafficSimulation } from '@/domain/simulation/simulation'; // Mit Fassung 5.31.0 aus der Simulation nach `rilsa/delay.ts` gezogen: Die // Umrechnung wird jetzt von zwei Rechnungen gebraucht, und zwei Fassungen // derselben Formel waeren der Fehler des Altbestands. import { rueckstaulaenge } from '@/domain/rilsa/delay'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { STAURAUMBEDARF_JE_FAHRZEUG } from '@/domain/rilsa/constants'; import { QUELLEN } from '@/domain/rilsa/quellen'; import type { Project } from '@/domain/model/project'; const FIXED_DATE = new Date('2026-01-01T00:00:00Z'); const SIMULATION_QUELLTEXT = readFileSync('src/domain/simulation/simulation.ts', 'utf8'); const DELAY_QUELLTEXT = readFileSync('src/domain/rilsa/delay.ts', 'utf8'); /** * Fassung 5.28.0, Punkte A4 und A5. * * A4: Der Stauraumbedarf je Fahrzeug stand modullokal in `simulation.ts` - * ohne Ausfuhr, ohne Eintrag in `constants.ts`, ohne Quelle, ohne * Pruefstand und ohne einen einzigen Test. Angezeigt wurde er trotzdem. * A5: Die daraus gebildete Laenge teilte nicht durch die Fahrstreifenzahl. An * einer zweistreifigen Zufahrt war die angezeigte Laenge doppelt so lang * wie die wirkliche - ein Fehler mit Faktor, kein Rundungsfehler. */ describe('Rueckstaulaenge - der Kennwert hat eine Herkunft (A4)', () => { it('steht in constants.ts und traegt 7 m', () => { expect(STAURAUMBEDARF_JE_FAHRZEUG).toBe(7); }); it('steht nicht mehr als eigene Zahl in der Simulation', () => { // Die Hausregel: constants.ts ist die einzige Quelle aller Kennwerte. expect(SIMULATION_QUELLTEXT).not.toContain('QUEUE_SPACING_METERS'); /* * Seit 5.31.0 nennt die Simulation den Kennwert gar nicht mehr selbst - * sie ruft `rueckstaulaenge` aus dem Fachkern, und dort steht er. * * GEPRUEFT WIRD DIE EINBINDUNG, NICHT DER NAME: Eine eigene Funktion * gleichen Namens in der Simulation waere genau der Rueckfall, den dieser * Fall verhindern soll - zwei Fassungen derselben Umrechnung. Ein * `toContain('rueckstaulaenge')` haette sie durchgelassen. */ expect(SIMULATION_QUELLTEXT).toMatch( /import \{[^}]*rueckstaulaenge[^}]*\} from '\.\.\/rilsa\/delay'/u, ); expect(SIMULATION_QUELLTEXT).not.toContain('STAURAUMBEDARF_JE_FAHRZEUG'); expect(DELAY_QUELLTEXT).toContain('STAURAUMBEDARF_JE_FAHRZEUG'); }); it('hat eine Quelle mit Pruefstand, und die behauptet kein Regelwerk', () => { const quelle = QUELLEN['praxis-stauraum-je-fahrzeug']; expect(quelle.pruefstand).toBe('abzugleichen'); // Weder RiLSA noch HBS nennen eine Laenge je wartendem Fahrzeug. Ein // Praxiswert, der sich als Regelwerkswert ausgibt, ist der Fehlertyp, den // dieses Verzeichnis verhindern soll. expect(quelle.regelwerk).toBe('Planungspraxis'); expect(quelle.fundstelle).toBeUndefined(); expect(quelle.hinweis).toContain('Kein Regelwerkswert'); }); }); describe('Rueckstaulaenge - die Fahrstreifen zaehlen mit (A5)', () => { it('verteilt die wartenden Fahrzeuge auf die Fahrstreifen', () => { expect(rueckstaulaenge(10, 1)).toBe(70); expect(rueckstaulaenge(10, 2)).toBe(35); // 10 * 7 / 3 = 23,333... - auf eine Stelle wie jede andere Anzeige. expect(rueckstaulaenge(10, 3)).toBe(23.3); }); it('nimmt eine fehlende Fahrstreifenangabe als einen Fahrstreifen', () => { // Dieselbe Behandlung wie in `saturationFlow`: keine Angabe heisst einer, // nicht null - sonst entstuende eine unendliche Laenge. expect(rueckstaulaenge(10, 0)).toBe(70); expect(rueckstaulaenge(10, -2)).toBe(70); expect(rueckstaulaenge(10, Number.NaN)).toBe(70); }); it('meldet fuer eine leere Warteschlange keine Laenge', () => { expect(rueckstaulaenge(0, 2)).toBe(0); expect(rueckstaulaenge(Number.NaN, 2)).toBe(0); }); it('zeigt am zweistreifigen Strom die halbe Laenge an', () => { const project = zweistreifig(createStandardIntersectionProject('Rueckstau', FIXED_DATE)); const plan = buildSignalPlan(project); const simulation = new TrafficSimulation(plan, project, { timeStep: 0.5 }); let gesehen = false; for (let i = 0; i < 12; i += 1) { simulation.advance(30); for (const gruppe of simulation.snapshot().groups) { if (gruppe.queue <= 0) continue; gesehen = true; /* * DIE WACHE: Bis zur Behebung stand hier `queue * 7`, und zwar * unabhaengig von der Zahl der Fahrstreifen. Bei zwei Fahrstreifen * war die Anzeige damit doppelt so lang wie der wirkliche Rueckstau. */ expect(gruppe.queueLength, gruppe.name).toBe(rueckstaulaenge(gruppe.queue, 2)); expect(gruppe.queueLength, gruppe.name).toBeLessThan( gruppe.queue * STAURAUMBEDARF_JE_FAHRZEUG, ); } } // Ein Test, der nie in die Pruefung laeuft, bewacht nichts. expect(gesehen, 'in keinem Schritt stand ein Fahrzeug im Rueckstau').toBe(true); }); it('laesst den einstreifigen Strom unveraendert', () => { // Die Behebung darf den Regelfall nicht verschieben: Bei einem // Fahrstreifen bleibt es bei Fahrzeuge mal Stauraumbedarf. const project = createStandardIntersectionProject('Rueckstau einstreifig', FIXED_DATE); const plan = buildSignalPlan(project); const simulation = new TrafficSimulation(plan, project, { timeStep: 0.5 }); let gesehen = false; for (let i = 0; i < 12; i += 1) { simulation.advance(30); for (const gruppe of simulation.snapshot().groups) { if (gruppe.queue <= 0) continue; gesehen = true; expect(gruppe.queueLength, gruppe.name).toBe(gruppe.queue * STAURAUMBEDARF_JE_FAHRZEUG); } } expect(gesehen, 'in keinem Schritt stand ein Fahrzeug im Rueckstau').toBe(true); }); }); /** Dasselbe Projekt, aber jede Signalgruppe auf zwei Fahrstreifen. */ function zweistreifig(project: Project): Project { return { ...project, signalGroups: project.signalGroups.map((gruppe) => ({ ...gruppe, lanes: 2 })), }; }