import { describe, expect, it } from 'vitest'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { computeDelay, sperrzeitenAusFenstern } from '@/domain/rilsa/delay'; import { createPhase, createStandardIntersectionProject } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; /* * Befund 26, im laufenden Programm nicht angekommen (Fassung 5.10.0). * * `computeDelay` bildet den Zaehler der Grundwartezeit seit Fassung * 5.10.0 aus Summe(r_j^2) / tU ueber die Sperrzeiten des Umlaufs; * ohne diese Sperrzeiten faellt es auf die geschlossene Einfensterform * tU * (1 - fA)^2 zurueck. Erhaelt eine Signalgruppe mehrere * Freigabezeitfenster, ist die Einfensterform zu gross - bei zwei gleich langen * Sperrzeiten genau um das Doppelte. * * `computeDelay` hat projektweit genau einen Aufrufer: den Planaufbau in * signalPlan.ts. Der uebergab die bereits gebildete Bindung `sperrzeiten` * nicht, sondern allein deren Groesstwert als `longestRed`. Damit blieb die * Behebung im Fachkern wirkungslos: Signalzeitenplan, Ausdruck und * Tabellenausgabe wiesen tW,G, tW und die Qualitaetsstufe weiter bis zum * Doppelten zu gross aus. * * Alle vorhandenen Wachen zu Befund 26 rufen `computeDelay` unmittelbar auf und * koennen deshalb nicht bemerken, dass der Weg dorthin unterbrochen ist. Diese * Datei geht ueber `buildSignalPlan`. * * GEGEN DEN ALTSTAND schlagen die beiden Faelle zum Zweifensterplan fehl; die * Gegenproben (ein Fenster, Rueckfall ohne brauchbare Sperrzeiten) bestehen * dort ebenso - sie halten fest, dass die Uebergabe den Regelfall nicht * verschiebt. */ const DATUM = new Date('2026-01-01T12:00:00Z'); /** * Vierphasiger Umlauf, in dem K1/K2 in Phase 1 und 3 und K3/K4 in Phase 2 und 4 * Freigabe haben - derselbe Aufbau wie in tests/export/pdfFreigabefenster.test.ts. * * Vier Phasen und nicht drei: Bei drei Phasen waeren Phase 1 und Phase 3 ueber * das Umlaufende benachbart, und der Planaufbau fasste beide zu einem einzigen * Fenster zusammen. */ function zweiFenster(): Project { const roh = createStandardIntersectionProject('Zwei Fenster', DATUM); const id = (name: string) => roh.signalGroups.find((g) => g.name === name)!.id; const nordSued = [id('K1'), id('K2'), id('F1')]; const ostWest = [id('K3'), id('K4'), id('F2')]; const phasen = [ createPhase('Phase 1 - Nord/Süd', nordSued), createPhase('Phase 2 - Ost/West', ostWest), createPhase('Phase 3 - Nord/Süd', nordSued), createPhase('Phase 4 - Ost/West', ostWest), ]; return { ...roh, phases: phasen, program: { ...roh.program, phaseOrder: phasen.map((p) => p.id) }, // Vier Phasen kosten doppelt so viele Uebergaenge; mit den Verkehrsstaerken // der Vorlage waere der Umlauf uebersaettigt und die Freigabezeiten stuenden // auf einem Ersatzwert. demands: roh.demands.map((d) => ({ ...d, volume: Math.round(d.volume / 2) })), }; } /** * Die Wartezeit, die `computeDelay` mit denselben Groessen liefert - einmal mit * den Sperrzeiten des Umlaufs, einmal ohne. * * Der Vergleich laeuft ueber den Fachkern selbst und nicht ueber eine im Test * nachgebaute Formel: Geprueft wird die Uebergabe, nicht die Rechnung. */ function ausFachkern(projekt: Project, gruppenname: string, mitSperrzeiten: boolean) { const plan = buildSignalPlan(projekt); const gruppe = plan.groups.find((g) => g.name === gruppenname)!; const demand = projekt.demands.find((d) => d.signalGroupId === gruppe.groupId)!; const sperrzeiten = sperrzeitenAusFenstern(gruppe.greens, plan.cycleTime); return computeDelay({ cycleTime: plan.cycleTime, greenTime: gruppe.totalGreen, capacity: gruppe.capacity!.capacity, demand: demand.volume, verfahren: projekt.settings.serviceLevelScale, mode: 'kfz', abflusszeit: gruppe.capacity!.abflusszeit, longestRed: sperrzeiten.length > 0 ? Math.max(...sperrzeiten) : 0, ...(mitSperrzeiten ? { sperrzeiten } : {}), instationaritaetsfaktor: plan.defaults.capacity.instationaritaetsfaktor, }); } describe('Befund 26 im Planaufbau: die Sperrzeiten erreichen die Wartezeitrechnung', () => { it('legt der Pruefung wirklich zwei getrennte Freigabezeitfenster vor', () => { // Vorbedingung: Mit einem Fenster prueft nichts darunter etwas. const plan = buildSignalPlan(zweiFenster()); const k1 = plan.groups.find((g) => g.name === 'K1')!; expect(k1.greens).toHaveLength(2); expect(k1.delay, 'ohne Wartezeit prüft der Fall nichts').not.toBeNull(); }); it('rechnet die Grundwartezeit ueber alle Fenster, nicht ueber eines', () => { const projekt = zweiFenster(); const plan = buildSignalPlan(projekt); const k1 = plan.groups.find((g) => g.name === 'K1')!; const mehrfenster = ausFachkern(projekt, 'K1', true); const einfenster = ausFachkern(projekt, 'K1', false); // Vorbedingung: Ohne Unterschied zwischen beiden Formen prueft der Fall // nichts. Die Einfensterform faellt zu gross aus. expect(einfenster.uniformDelay).toBeGreaterThan(mehrfenster.uniformDelay); expect(k1.delay!.uniformDelay).toBeCloseTo(mehrfenster.uniformDelay, 6); expect(k1.delay!.averageDelay).toBeCloseTo(mehrfenster.averageDelay, 6); expect(k1.delay!.uniformDelay).not.toBeCloseTo(einfenster.uniformDelay, 6); }); it('reicht die Sperrzeiten an jede Kfz-Gruppe mit mehreren Fenstern durch', () => { // Nicht "an K1", sondern "an jede": Eine zweite Uebergabestelle mit einer // anderen Wahl faellt hier auf. const projekt = zweiFenster(); const plan = buildSignalPlan(projekt); let geprueft = 0; for (const gruppe of plan.groups) { if (gruppe.delay === null || gruppe.capacity === null) continue; if (gruppe.greens.length < 2) continue; geprueft += 1; expect(gruppe.delay.uniformDelay, gruppe.name).toBeCloseTo( ausFachkern(projekt, gruppe.name, true).uniformDelay, 6, ); } expect(geprueft, 'die Schleife prüft wirklich etwas').toBeGreaterThan(1); }); it('laesst bei einem einzigen Fenster die geschlossene Form unveraendert', () => { // Gegenprobe: Bei einem Fenster je Umlauf gehen beide Formen ineinander // ueber - die Uebergabe darf den Regelfall nicht verschieben. const projekt = createStandardIntersectionProject('Ein Fenster', DATUM); const plan = buildSignalPlan(projekt); const k1 = plan.groups.find((g) => g.name === 'K1')!; expect(k1.greens).toHaveLength(1); expect(k1.delay!.uniformDelay).toBeCloseTo(ausFachkern(projekt, 'K1', false).uniformDelay, 6); expect(k1.delay!.uniformDelay).toBeCloseTo(ausFachkern(projekt, 'K1', true).uniformDelay, 6); }); });