import { describe, expect, it, vi } from 'vitest'; import type { Conflict, Project } from '@/domain/model/project'; import type { PlannedSignalGroup } from '@/domain/plan/signalPlan'; /* * Fassung 5.12.0: `ueberlappenImUmlauf` wurde je Paar mehrfach gerechnet. * * Zwei Regeln stellen dieselbe Frage. `checkSimultaneousGreen` fragt sie, weil * gleichzeitige Freigabe feindlicher Signalgruppen ein Fehler ist; * `checkIntergreenGaps` fragt sie, um bei Ueberlappung zu schweigen - dort ist * kein Abstand messbar. Beide gehen ueber `project.conflicts`, und die stehen * paarweise; beide merken sich ein Paar erst, wenn sie es MELDEN. Am haeufigen * Regelfall - Paar erfasst, beide Richtungen, keine Ueberlappung - wurde * derselbe Umlauf deshalb VIERMAL in Schritten von 0,5 s abgetastet. * * GEMESSEN (Knotenpunkt mit 12 Signalgruppen, 4 Phasen, 108 Beziehungen, * Median aus 7 Runden zu je 200 Laeufen, derselbe Stand einmal mit und einmal * ohne Gedaechtnis): 2,17 ms je Pruefung ohne, 0,74 ms mit. Die mehrfache * Abtastung war dort rund zwei Drittel der Pruefungslaufzeit; die Befunde * bleiben in Zahl und Wortlaut dieselben. * * DIE WACHE ZAEHLT STATT ZU MESSEN - dieselbe Entscheidung wie in * pruefungLaufzeit.test.ts: Eine Zeitschranke haengt am Rechner, der sie * ausfuehrt. Gezaehlt werden die Abtastungen selbst, also die Aufrufe von * `isGreenAt` aus der Pruefung heraus. Sie sind vom Rechner unabhaengig. * * GEGEN DEN ALTSTAND schlagen die beiden ersten Faelle fehl (viermal statt * einmal abgetastet); der dritte besteht dort ebenso - am Ergebnis der Pruefung * darf sich nichts aendern, und das ist die Halbmiete dieses Punktes. */ const DATUM = new Date('2026-01-01T00:00:00Z'); /** Jede Abtastung als "Gruppe@Zeitpunkt" - Doppelungen bleiben stehen. */ const abtastungen: string[] = []; vi.mock('@/domain/plan/aspects', async (echt) => { const modul = await echt(); return { ...modul, isGreenAt: ( gruppe: Pick & { readonly groupId?: string }, zeit: number, umlauf: number, ): boolean => { abtastungen.push(`${gruppe.groupId ?? '?'}@${zeit}`); return modul.isGreenAt(gruppe, zeit, umlauf); }, }; }); const { createConflict, createEmptyProject, createPhase, createSignalGroup } = await import('@/domain/model/factory'); const { buildSignalPlan } = await import('@/domain/plan/signalPlan'); const { validateProject } = await import('@/domain/validation/engine'); /** * Zwei feindliche Signalgruppen in zwei Phasen, Beziehung in beiden Richtungen * erfasst - der Regelfall, in dem beide Regeln bis zur Abtastung kommen und * keine von beiden meldet. */ function zweiGruppen(): Project { const basis = createEmptyProject('Abtastung', DATUM, 'knotenpunkt'); const k1 = createSignalGroup({ name: 'K1', mode: 'kfz', armId: null, index: 0 }); const k2 = createSignalGroup({ name: 'K2', mode: 'kfz', armId: null, index: 1 }); const p1 = createPhase('Phase 1', [k1.id]); const p2 = createPhase('Phase 2', [k2.id]); const beziehung = (fromId: string, toId: string): Conflict => ({ ...createConflict(fromId, toId), clearingDistance: 22, enteringDistance: 8, }); return { ...basis, signalGroups: [k1, k2], phases: [p1, p2], conflicts: [beziehung(k1.id, k2.id), beziehung(k2.id, k1.id)], program: { ...basis.program, phaseOrder: [p1.id, p2.id] }, }; } function abtastungenEinerPruefung(project: Project, plan = buildSignalPlan(project)): string[] { abtastungen.length = 0; validateProject(project, plan, DATUM); return [...abtastungen]; } describe('Abtastung des Umlaufs auf gleichzeitige Freigabe', () => { it('tastet dasselbe Paar nur einmal ab', () => { const gezaehlt = abtastungenEinerPruefung(zweiGruppen()); // Voraussetzung des Aufbaus: Es wird ueberhaupt abgetastet. expect(gezaehlt.length).toBeGreaterThan(0); // Innerhalb EINER Abtastung ist jede Kombination aus Signalgruppe und // Zeitpunkt hoechstens einmal gefragt (die Schleife geht je Zeitpunkt // einmal ueber a und hoechstens einmal ueber b). Bei nur zwei Gruppen im // Projekt heisst eine Doppelung deshalb: derselbe Umlauf wurde fuer // dasselbe Paar ein zweites Mal abgetastet. expect(new Set(gezaehlt).size).toBe(gezaehlt.length); }); it('tastet nicht oefter ab, wenn dasselbe Paar zweimal in der Liste steht', () => { const beide = zweiGruppen(); // DERSELBE Plan fuer beide Faelle: Nimmt man der Anlage eine Richtung, wird // der Plan ein anderer (Zwischenzeiten, Umlaufzeit) und mit ihm die Zahl // der Zeitschritte. Verglichen werden soll aber allein, wie oft dieselbe // Abtastung angefordert wird. const plan = buildSignalPlan(beide); const einseitig: Project = { ...beide, conflicts: beide.conflicts.slice(0, 1) }; const mitEiner = abtastungenEinerPruefung(einseitig, plan); const mitBeiden = abtastungenEinerPruefung(beide, plan); expect(mitEiner.length).toBeGreaterThan(0); // Ohne Gedaechtnis stand hier das Doppelte: Jede Richtung brachte beide // Regeln erneut zur Abtastung desselben Umlaufs. expect(mitBeiden.length).toBe(mitEiner.length); }); it('meldet dabei unveraendert dieselben Befunde', () => { const project = zweiGruppen(); const bericht = validateProject(project, buildSignalPlan(project), DATUM); // Getrennte Phasen, kein Fehler aus diesen beiden Regeln. expect(bericht.findings.some((f) => f.rule === 'signalplan.feindliche-freigabe')).toBe(false); expect(bericht.findings.some((f) => f.rule === 'zwischenzeiten.abstand-zu-kurz')).toBe(false); // Beide Gruppen in EINE Phase: Jetzt zeigen sie gleichzeitig Freigabe, und // die Meldung darueber muss stehen - genau einmal fuer das Paar. const zusammen: Project = { ...project, phases: [{ ...project.phases[0]!, signalGroupIds: project.signalGroups.map((g) => g.id) }], program: { ...project.program, phaseOrder: [project.phases[0]!.id] }, }; const gemeinsam = validateProject(zusammen, buildSignalPlan(zusammen), DATUM); expect( gemeinsam.findings.filter( (f) => f.rule === 'phasen.feindlich-in-phase' || f.rule === 'signalplan.feindliche-freigabe', ), ).toHaveLength(1); // Und die Abstandsregel schweigt bei Ueberlappung weiterhin. expect(gemeinsam.findings.some((f) => f.rule === 'zwischenzeiten.abstand-zu-kurz')).toBe(false); }); });