import { describe, expect, it } from 'vitest'; import { validateProject } from '@/domain/validation/engine'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { TrafficSimulation } from '@/domain/simulation/simulation'; import { serviceLevelFor } from '@/domain/rilsa/delay'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; const STICHTAG = new Date('2026-01-01T00:00:00Z'); function vollstaendig(name: string): Project { const base = createStandardIntersectionProject(name, STICHTAG); return { ...base, meta: { ...base.meta, projectNumber: 'VP-2026-001', client: 'Stadt Musterhausen', planner: 'O. Willerding', location: name, }, }; } describe('Fehlende Gegenrichtung einer Konfliktbeziehung', () => { /* * Das war eine Warnung und liess den Export offen. Ohne erfasste * Gegenrichtung gibt es keine Zwischenzeit - und damit auch keinen Sollwert, * gegen den die Abstandspruefung des fertigen Plans messen koennte. Die * fehlende Haelfte ist die einzige Stelle, an der ein feindliches Paar * voellig ungesichert bleibt. */ it('sperrt den Export', () => { const roh = vollstaendig('Gegenrichtung'); const [k1, , k3] = roh.signalGroups; const projekt: Project = { ...roh, conflicts: roh.conflicts.filter((c) => !(c.fromId === k3!.id && c.toId === k1!.id)), }; const bericht = validateProject(projekt, buildSignalPlan(projekt), STICHTAG); const befund = bericht.findings.find((f) => f.rule === 'konflikte.gegenrichtung-fehlt'); expect(befund).toBeDefined(); expect(befund!.severity).toBe('fehler'); expect(befund!.title).toContain('K1 nach K3'); expect(befund!.message).toContain('K3'); expect(bericht.exportBlocked).toBe(true); }); it('schweigt beim Regelknotenpunkt, in dem jede Beziehung beidseitig erfasst ist', () => { const bericht = validateProject(vollstaendig('Regelfall'), undefined, STICHTAG); expect(bericht.findings.some((f) => f.rule === 'konflikte.gegenrichtung-fehlt')).toBe(false); expect(bericht.findings.filter((f) => f.severity === 'fehler')).toHaveLength(0); }); }); describe('Bewertungsskala in der Simulation', () => { /* * serviceLevelFor wurde ohne Skala gerufen und fiel damit auf HBS zurueck, * waehrend der Signalzeitenplan die eingestellte Skala korrekt weiterreicht. * Dieselbe Wartezeit erhielt in der Simulation eine andere Qualitaetsstufe * als in der Leistungsfaehigkeit - und im Ausdruck stand daneben "Bewertung * nach HCM". */ function laufen(skala: 'HBS' | 'HCM'): ReturnType { const roh = vollstaendig('Simulation'); const projekt: Project = { ...roh, settings: { ...roh.settings, serviceLevelScale: skala }, }; const sim = new TrafficSimulation(buildSignalPlan(projekt), projekt, { seed: 42 }); sim.advance(600); return sim.snapshot(); } it('bewertet nach der eingestellten Skala', () => { for (const skala of ['HBS', 'HCM'] as const) { const stand = laufen(skala); expect(stand.overallServiceLevel.scale, skala).toBe(skala); for (const gruppe of stand.groups) { expect(gruppe.serviceLevel.scale, `${skala} / ${gruppe.name}`).toBe(skala); // Die Stufe muss zu Wartezeit UND Skala passen, nicht nur die // Beschriftung der Skala. // (Ohne Ueberlastung entscheidet die Wartezeit allein; die Gruppen // dieses Projekts sind nicht ueberlastet.) expect(gruppe.serviceLevel.grade, `${skala} / ${gruppe.name}`).toBe( serviceLevelFor(gruppe.averageDelay, skala)?.grade, ); } } }); it('kommt bei derselben Wartezeit je nach Skala zu verschiedenen Stufen', () => { // Ohne diesen Nachweis koennte die Skala mitgefuehrt und doch wirkungslos // sein. 31,9 s sind nach HBS Stufe B, nach HCM Stufe C. expect(serviceLevelFor(31.9, 'HBS')?.grade).not.toBe(serviceLevelFor(31.9, 'HCM')?.grade); }); });