import { describe, expect, it } from 'vitest'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import * as actions from '@/app/actions'; /* * Befund 13, behoben in Fassung 5.10.0 - Freigabefenster am Umlaufende. * * `buildGroupWindows` bildete den Fensterbeginn `rawStart` und mass die Dauer * gegen den UNGEFALTETEN Wert, waehrend die Zeile darunter den Beginn bereits * in den Umlauf zurueckfaltete. Endet der Lauf einer Gruppe genau an der * Umlaufgrenze - alle folgenden Phasen sind leer (0 s Dauer), und der * Uebergang aus der letzten belegten Phase betraegt 0 s -, gilt * rawStart == end == tU: Der Nicht-Ueberlauf-Zweig rechnete dann 0 s statt des * ganzen Umlaufs. * * Folge: Die Phasentabelle wies 30 s aus, die Signalgruppenzeile 0 s; dazu * kamen Scheinbefunde (Mindestfreigabezeit unterschritten, Auslastungsgrad * unendlich), die es fachlich nicht gibt. * * GEGEN DEN ALTSTAND schlagen die ersten drei Faelle fehl (Freigabezeit 0 s * statt des ganzen Umlaufs, dazu die Scheinmeldung). Der vierte Fall ist die * Gegenprobe und besteht auch dort: Bei zwei belegten und zwei leeren Phasen * endet kein Lauf an der Umlaufgrenze, die Faltung aendert dort nichts. */ const DATUM = new Date('2026-01-01T00:00:00Z'); /** Standardknotenpunkt, dessen zweite Phase durch eine leere ersetzt ist. */ function eineBelegtePhaseUndEineLeere() { const roh = createStandardIntersectionProject('Leere Phase', DATUM); const ohne = actions.removePhase(roh, roh.phases[1]!.id); return actions.addPhase(ohne); } describe('Befund 13 - Freigabefenster, dessen Lauf an der Umlaufgrenze endet', () => { it('gibt der Gruppe der einzigen belegten Phase den ganzen Umlauf', () => { const plan = buildSignalPlan(eineBelegtePhaseUndEineLeere()); const k1 = plan.groups.find((g) => g.name === 'K1')!; // Die leere Phase hat 0 s Dauer und beide Uebergaenge 0 s: Phase 1 traegt // den gesamten Umlauf, ihre Gruppen sind durchgehend freigegeben. expect(plan.phases[0]?.duration).toBe(plan.cycleTime); expect(k1.greens).toHaveLength(1); expect(k1.greens[0]?.start).toBe(0); expect(k1.greens[0]?.duration).toBe(plan.cycleTime); expect(k1.totalGreen).toBe(plan.cycleTime); }); it('erzeugt dabei keinen Scheinbefund zur Mindestfreigabezeit', () => { const plan = buildSignalPlan(eineBelegtePhaseUndEineLeere()); // Eine Gruppe mit 30 s Freigabezeit unterschreitet keine // Mindestfreigabezeit - die Meldung entstand allein aus dem Fenster 0 s. const betroffen = plan.notes.filter( (n) => n.code === 'mindestfreigabezeit-unterschritten' && n.signalGroupId !== undefined, ); expect(betroffen).toEqual([]); // Und der Auslastungsgrad ist endlich, weil die Kapazitaet nicht 0 ist. const k1 = plan.groups.find((g) => g.name === 'K1')!; expect(k1.capacity?.capacity ?? 0).toBeGreaterThan(0); }); it('faltet auch einen Lauf ueber mehrere belegte Phasen', () => { // K1 laeuft durch Phase 1 UND Phase 2; erst danach steht die leere Phase. const roh = createStandardIntersectionProject('Zwei Laeufe', DATUM); const k1 = roh.signalGroups.find((g) => g.name === 'K1')!; const mitK1 = actions.updatePhase(roh, roh.phases[1]!.id, { signalGroupIds: [k1.id] }); const plan = buildSignalPlan(actions.addPhase(mitK1)); const geplant = plan.groups.find((g) => g.name === 'K1')!; expect(geplant.totalGreen).toBe(plan.cycleTime); // Die uebrigen Gruppen behalten ihr gewoehnliches Fenster. expect(plan.groups.find((g) => g.name === 'K2')?.totalGreen).toBe(plan.phases[0]?.duration); }); it('aendert nichts, wenn kein Lauf an der Umlaufgrenze endet', () => { // Gegenprobe: zwei belegte Phasen, danach zwei leere. Der Uebergang aus // Phase 2 wird gegen Phase 1 bemessen und ist groesser als 0; kein Lauf // endet an der Umlaufgrenze. const roh = createStandardIntersectionProject('Gegenprobe', DATUM); const plan = buildSignalPlan(actions.addPhase(actions.addPhase(roh))); for (const gruppe of plan.groups) { expect(gruppe.totalGreen).toBeGreaterThan(0); expect(gruppe.totalGreen).toBeLessThan(plan.cycleTime); } }); });