import { describe, expect, it } from 'vitest'; import { buildSignalPlan, intergreenBetween, type GreenInterval } from '@/domain/plan/signalPlan'; import { createPhase, createStandardIntersectionProject } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; /* * Befund F6: Eine Phase ohne Signalgruppen setzte die Zwischenzeit des * Phasenwechsels auf 0 s. * * Wer in der Phasenansicht auf "Phase hinzufügen" klickt und der neuen Phase * noch keine Signalgruppe zuordnet, erhielt einen Signalzeitenplan, in dem die * Freigabe der Folgephase in derselben Sekunde begann, in der die Freigabe der * Vorphase endete - die gerechnete Zwischenzeit von mehreren Sekunden fiel * ersatzlos weg, weil beide angrenzenden Uebergaenge auf 0 s standen. */ const FESTES_DATUM = new Date('2026-01-01T00:00:00Z'); interface Prueffall { readonly project: Project; readonly leerePhaseId: string; readonly vorphaseId: string; } /** Standardknotenpunkt mit einer zusaetzlich angehaengten Phase ohne Signalgruppe. */ function mitLeererPhase(): Prueffall { const basis = createStandardIntersectionProject('Prueffall', FESTES_DATUM); const leer = createPhase('Phase 3 - ohne Zuordnung'); return { project: { ...basis, phases: [...basis.phases, leer], program: { ...basis.program, phaseOrder: [...basis.program.phaseOrder, leer.id] }, }, leerePhaseId: leer.id, vorphaseId: basis.phases[1]!.id, }; } /** Kleinster Abstand vom Freigabeende der einen zum Freigabebeginn der anderen Gruppe. */ function kleinsterAbstand( raeumend: readonly GreenInterval[], einfahrend: readonly GreenInterval[], umlauf: number, ): number { let kleinster = Number.POSITIVE_INFINITY; for (const a of raeumend) { const ende = a.start + a.duration; for (const b of einfahrend) { const luecke = (((b.start - ende) % umlauf) + umlauf) % umlauf; if (luecke < kleinster) kleinster = luecke; } } return kleinster; } describe('Phase ohne Signalgruppen im Signalzeitenplan', () => { it('haelt jede geforderte Zwischenzeit ein, obwohl eine leere Phase dazwischen steht', () => { const { project } = mitLeererPhase(); const plan = buildSignalPlan(project); for (const conflict of project.conflicts) { const gefordert = intergreenBetween(plan.intergreens, conflict.fromId, conflict.toId); if (gefordert <= 0) continue; const raeumend = plan.groups.find((g) => g.groupId === conflict.fromId)!; const einfahrend = plan.groups.find((g) => g.groupId === conflict.toId)!; if (raeumend.greens.length === 0 || einfahrend.greens.length === 0) continue; const ist = kleinsterAbstand(raeumend.greens, einfahrend.greens, plan.cycleTime); expect( ist + 1e-6, `${raeumend.name} räumt, ${einfahrend.name} fährt ein`, ).toBeGreaterThanOrEqual(gefordert); } }); it('bemisst den Uebergang in die leere Phase nach der naechsten belegten Phase', () => { const { project, leerePhaseId, vorphaseId } = mitLeererPhase(); const plan = buildSignalPlan(project); const hinein = plan.transitions.find((t) => t.toPhaseId === leerePhaseId)!; const heraus = plan.transitions.find((t) => t.fromPhaseId === leerePhaseId)!; // Massgebend ist die groesste Zwischenzeit zwischen den in der Vorphase // endenden und den in der naechsten belegten Phase beginnenden Gruppen. const vorphase = project.phases.find((p) => p.id === vorphaseId)!; const folgephase = project.phases[0]!; let erwartet = 0; for (const fromId of vorphase.signalGroupIds) { if (folgephase.signalGroupIds.includes(fromId)) continue; for (const toId of folgephase.signalGroupIds) { if (vorphase.signalGroupIds.includes(toId)) continue; erwartet = Math.max(erwartet, intergreenBetween(plan.intergreens, fromId, toId)); } } expect(erwartet).toBeGreaterThan(0); expect(hinein.duration).toBe(erwartet); expect(hinein.critical?.intergreen).toBe(erwartet); // Aus der leeren Phase heraus endet keine Freigabe - dort ist nichts zu sichern. expect(heraus.duration).toBe(0); }); });