import { describe, expect, it } from 'vitest'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import type { Conflict, Project } from '@/domain/model/project'; import { buildSignalPlan, intergreenKey, massgebendeRaeumbeziehung, } from '@/domain/plan/signalPlan'; import { raeumbeziehungSatz } from '@/domain/rilsa/ansaetze'; import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; import { validateProject } from '@/domain/validation/engine'; /* * Fassung 5.11.0 - der massgebende Strom im PRUEFBERICHT. * * Eine fruehere Behebung hat die Entscheidung, mit welcher Fahrbeziehung eine Beziehung * raeumt, an genau eine Stelle gelegt: `massgebendeRaeumbeziehung(from, * conflict)` in signalPlan.ts. Der Planaufbau liest sie. Der Pruefbericht * bildete den Ausdruck bis zu diesem Befund weiterhin selbst und nahm * `from.movement` - die Fahrbeziehung der SIGNALGRUPPE. * * Was der Anwender davon merkt: An einer Sammelsignalgruppe "geradeaus UND * rechts" - dem Regelfall des gemeinsamen Fahrstreifens - raeumt der Plan die * Beziehung als Rechtsabbieger und setzt das Merkmal "enger Innenradius" an; * der Bericht derselben Rechnung meldet daneben, das Merkmal gehe "in keine * Zwischenzeit ein", weil der raeumende Strom kein abbiegender * Kraftfahrzeugstrom sei. Zwei Aussagen zu EINER Beziehung, und die des * Berichts raet dazu, ein Merkmal zu entfernen, das gerechnet hat. */ const DATUM = new Date('2026-01-01T00:00:00Z'); /** * Standardknotenpunkt, an dem "K1" geradeaus UND rechts fuehrt: Die * Signalgruppe bleibt bei 'geradeaus', die Beziehung K1 -> K3 traegt die * Fahrbeziehung ihres massgebenden Stroms ('rechts') - genau das, was * `uebernimmWege` aus einem Lageplan schreibt. */ function sammelgruppe(zusatz: Partial = {}): Project { const basis = createStandardIntersectionProject('Sammelgruppe', DATUM); const k1 = basis.signalGroups.find((g) => g.name === 'K1')!; const k3 = basis.signalGroups.find((g) => g.name === 'K3')!; return { ...basis, conflicts: basis.conflicts.map((c) => c.fromId === k1.id && c.toId === k3.id ? { ...c, raeumbeziehung: 'rechts' as const, ...zusatz } : c, ), }; } function beziehung(project: Project): Conflict { const k1 = project.signalGroups.find((g) => g.name === 'K1')!; const k3 = project.signalGroups.find((g) => g.name === 'K3')!; return project.conflicts.find((c) => c.fromId === k1.id && c.toId === k3.id)!; } describe('N1/1: Der Bericht beurteilt den engen Innenradius am massgebenden Strom', () => { it('meldet ihn als wirksam, wo der Plan mit ihm rechnet', () => { const project = sammelgruppe({ engerRadius: true }); const konflikt = beziehung(project); const plan = buildSignalPlan(project); const gerechnet = plan.intergreens.get(intergreenKey(konflikt.fromId, konflikt.toId)); // Der Plan raeumt als Rechtsabbieger mit engem Innenradius: 5,0 m/s. expect(gerechnet?.calculation?.clearingSpeed).toBe( RILSA_DEFAULTS.clearingSpeed.kfzTurningEngerRadius, ); const bericht = validateProject(project, plan, DATUM); const zurBeziehung = bericht.findings.filter( (f) => f.target?.kind === 'konflikt' && f.target.id === konflikt.id, ); expect(zurBeziehung.map((f) => f.rule)).toContain('zwischenzeiten.enger-radius'); expect(zurBeziehung.map((f) => f.rule)).not.toContain( 'zwischenzeiten.enger-radius-ohne-wirkung', ); }); it('nennt nicht "kein abbiegender Kraftfahrzeugstrom", wo der Plan abbiegend raeumt', () => { const project = sammelgruppe({ engerRadius: true }); const konflikt = beziehung(project); const bericht = validateProject(project, buildSignalPlan(project), DATUM); const texte = bericht.findings .filter((f) => f.target?.kind === 'konflikt' && f.target.id === konflikt.id) .map((f) => f.message) .join(' '); expect(texte).not.toContain('kein abbiegender Kraftfahrzeugstrom'); }); it('bleibt bei der Gruppenbeziehung, wo die Beziehung keine eigene fuehrt', () => { // Gegenprobe: ohne `raeumbeziehung` gilt die Fahrbeziehung der Gruppe // ('geradeaus'). Dort wirkt das Merkmal wirklich nicht - der Bericht muss // weiter warnen, sonst prueft der Fall oben nichts. const basis = sammelgruppe({ engerRadius: true }); const project: Project = { ...basis, conflicts: basis.conflicts.map((c) => { const { raeumbeziehung: _weg, ...ohne } = c; return ohne; }), }; const konflikt = beziehung(project); const plan = buildSignalPlan(project); expect( plan.intergreens.get(intergreenKey(konflikt.fromId, konflikt.toId))?.calculation ?.clearingSpeed, ).toBe(RILSA_DEFAULTS.clearingSpeed.kfzStraight); expect( validateProject(project, plan, DATUM) .findings.filter((f) => f.target?.kind === 'konflikt' && f.target.id === konflikt.id) .map((f) => f.rule), ).toContain('zwischenzeiten.enger-radius-ohne-wirkung'); }); it('vergleicht die Handeingabe mit der Raeumgeschwindigkeit des massgebenden Stroms', () => { /* * Der dritte Zweig der Regel (Handeingabe hat Vorrang) nennt die * Raeumgeschwindigkeit, die das Merkmal ergaebe. Mit der Fahrbeziehung der * Gruppe waren das 10,0 m/s - der Geradeauswert -, und der Satz "Geraeumt * wird mit 9 m/s, nicht mit 10 m/s" behauptete eine Verkuerzung, wo die * Eingabe die Zwischenzeit in Wahrheit VERLAENGERT (5,0 m/s waeren * langsamer). */ const project = sammelgruppe({ engerRadius: true, clearingSpeedOverride: 9 }); const konflikt = beziehung(project); const befund = validateProject(project, buildSignalPlan(project), DATUM).findings.find( (f) => f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung' && f.target?.kind === 'konflikt' && f.target.id === konflikt.id, ); expect(befund?.message).toContain('nicht mit 5 m/s'); expect(befund?.message).toContain('KÜRZER'); }); }); /* * NACHTRAG (Fassung 5.11.0): Der Bericht urteilt am massgebenden Strom, sagte * als einzige der vier Auskunftsstellen aber nicht, dass er es tut. Im * Spiegelfall widersprach er damit der Signalgruppentabelle: "der raeumende * Strom "K1" ist kein abbiegender Kraftfahrzeugstrom" ueber einer Gruppe, die * als rechts abbiegend erfasst ist. */ describe('N1/7: Der Bericht sagt, dass er am massgebenden Strom urteilt', () => { /** Meldungen zur Beziehung K1 -> K3. */ function meldungen(project: Project): { rule: string; message: string }[] { const konflikt = beziehung(project); return validateProject(project, buildSignalPlan(project), DATUM) .findings.filter((f) => f.target?.kind === 'konflikt' && f.target.id === konflikt.id) .map((f) => ({ rule: f.rule, message: f.message })); } it('nennt den Strom, wo das Merkmal an ihm wirkt', () => { const project = sammelgruppe({ engerRadius: true }); const befund = meldungen(project).find((f) => f.rule === 'zwischenzeiten.enger-radius'); expect(befund?.message).toContain( 'Den Räumweg dieser Beziehung fährt ein Strom der Signalgruppe „K1“ mit der ' + 'Fahrbeziehung „rechts abbiegend“; geräumt wird mit ihr.', ); }); it('nennt ihn im Spiegelfall, in dem die Gruppe abbiegt und der Strom geradeaus faehrt', () => { /* * K1 ist als rechts abbiegend erfasst, den Raeumweg dieser Beziehung faehrt * aber der Geradeausstrom. Das Merkmal wirkt an ihm nicht - der Bericht * warnt zu Recht und muss daneben sagen, warum er die Gruppe anders * beurteilt, als die Signalgruppentabelle sie fuehrt. */ const basis = sammelgruppe({ engerRadius: true }); const project: Project = { ...basis, signalGroups: basis.signalGroups.map((g) => g.name === 'K1' ? { ...g, movement: 'rechts' as const } : g, ), conflicts: basis.conflicts.map((c) => c.raeumbeziehung === undefined ? c : { ...c, raeumbeziehung: 'geradeaus' as const }, ), }; const befund = meldungen(project).find( (f) => f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung', ); expect(befund?.message).toContain('kein abbiegender Kraftfahrzeugstrom'); expect(befund?.message).toContain( 'Den Räumweg dieser Beziehung fährt ein Strom der Signalgruppe „K1“ mit der ' + 'Fahrbeziehung „geradeaus“; geräumt wird mit ihr.', ); }); it('nennt ihn auch dort, wo eine Handeingabe dem Merkmal vorgeht', () => { const project = sammelgruppe({ engerRadius: true, clearingSpeedOverride: 9 }); const befund = meldungen(project).find( (f) => f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung', ); expect(befund?.message).toContain('Den Räumweg dieser Beziehung fährt ein Strom'); }); it('schweigt dazu, wo Strom und Signalgruppe dieselbe Fahrbeziehung haben', () => { // Gegenprobe: ohne `raeumbeziehung` gibt es nichts zu erklaeren, und ein // Satz darueber waere eine Behauptung. const basis = sammelgruppe({ engerRadius: true }); const project: Project = { ...basis, conflicts: basis.conflicts.map((c) => { const { raeumbeziehung: _weg, ...ohne } = c; return ohne; }), }; expect( meldungen(project) .map((f) => f.message) .join(' '), ).not.toContain('Den Räumweg dieser Beziehung fährt'); }); it('formuliert den Satz einmal - Bericht und Konfliktfenster teilen ihn', () => { // `raeumbeziehungSatz` ist die eine Quelle; das Konfliktfenster setzt // denselben Satz in seinen Kopf (tests/ui/massgebenderStromKonfliktfenster.test.ts). const project = sammelgruppe({ engerRadius: true }); const konflikt = beziehung(project); const from = project.signalGroups.find((g) => g.id === konflikt.fromId)!; const satz = raeumbeziehungSatz(from, konflikt); expect(satz).not.toBe(''); expect( meldungen(project).find((f) => f.rule === 'zwischenzeiten.enger-radius')?.message, ).toContain(satz); }); }); describe('N1/2: Plan und Bericht legen dieselbe Fahrbeziehung zugrunde', () => { it('beurteilt jede Beziehung mit der Fahrbeziehung, die massgebendeRaeumbeziehung nennt', () => { const project = sammelgruppe({ engerRadius: true }); const plan = buildSignalPlan(project); const bericht = validateProject(project, plan, DATUM); const abweichend = project.conflicts .filter((c) => c.engerRadius === true) .map((c) => { const from = project.signalGroups.find((g) => g.id === c.fromId)!; const massgebend = massgebendeRaeumbeziehung(from, c); const abbiegend = massgebend === 'links' || massgebend === 'rechts'; const gemeldet = bericht.findings.some( (f) => f.rule === 'zwischenzeiten.enger-radius' && f.target?.kind === 'konflikt' && f.target.id === c.id, ); return { beziehung: `${c.fromId} -> ${c.toId}`, massgebend, abbiegend, gemeldet }; }) .filter((z) => z.abbiegend !== z.gemeldet); expect(abweichend).toEqual([]); }); });