lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ tests domain doppelmeldungen.test.ts

7,1 KB Rohdatei
tests/domain/doppelmeldungen.test.ts — 157 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { validateProject } from '@/domain/validation/engine';
3 import { buildSignalPlan } from '@/domain/plan/signalPlan';
4 import { createEmptyProject, createStandardIntersectionProject } from '@/domain/model/factory';
5 import type { Project } from '@/domain/model/project';
6
7 /*
8 * Fassung 5.9.0 - derselbe Sachverhalt zweimal im Pruefbericht
9 * (Befunde 14, 35, 36, 37).
10 *
11 * Der Pruefbericht wird gedruckt und traegt eine Fehler- und Warnungsbilanz.
12 * Wo eine Regel der Pruefung und eine Notiz des Planaufbaus DENSELBEN Zustand
13 * melden, steht die Aussage zweimal darin, und die Bilanz nennt eine Ursache
14 * zweimal. Fuer die Vorgabennotizen ist dieses Muster bereits behoben
15 * (istVorgabenNotiz in engine.ts); hier fehlte es an vier weiteren Stellen.
16 */
17
18 const DATUM = new Date('2026-01-01T00:00:00Z');
19
20 function bericht(project: Project) {
21 return validateProject(project, buildSignalPlan(project), DATUM);
22 }
23
24 describe('Befund 14 - Signalgruppe ohne Phase steht nur einmal im Bericht', () => {
25 /** Der Musterknotenpunkt, aus dem K1 aus jeder Phase entfernt ist. */
26 function ohneK1InPhasen(): Project {
27 const roh = createStandardIntersectionProject('Ohne Phase', DATUM);
28 const k1 = roh.signalGroups.find((g) => g.name === 'K1')!;
29 return {
30 ...roh,
31 phases: roh.phases.map((p) => ({
32 ...p,
33 signalGroupIds: p.signalGroupIds.filter((id) => id !== k1.id),
34 })),
35 };
36 }
37
38 it('meldet eine Gruppe ohne jede Freigabe nicht einmal als Fehler und einmal als Warnung', () => {
39 // Sonst liest der Pruefer in derselben Unterlage "Fehler: K1 ist keiner
40 // Phase zugeordnet" UND "Warnung: Signalgruppe ohne Freigabezeit" - eine
41 // Ursache, zwei Zeilen, zwei verschiedene Schweregrade.
42 const treffer = bericht(ohneK1InPhasen()).findings.filter((f) => f.rule.endsWith('ohne-phase'));
43 expect(treffer).toHaveLength(1);
44 expect(treffer[0]!.rule).toBe('signalgruppen.ohne-phase');
45 expect(treffer[0]!.severity).toBe('fehler');
46 });
47
48 it('laesst die Notiz im Plan stehen - er ist fuer sich vollstaendig', () => {
49 const plan = buildSignalPlan(ohneK1InPhasen());
50 expect(plan.notes.some((n) => n.code === 'signalgruppe-ohne-phase')).toBe(true);
51 });
52 });
53
54 describe('Befund 37 - Ein Projekt ohne Phasen meldet "Keine Phase angelegt" nur einmal', () => {
55 it('zaehlt fuer ein leeres Projekt zwei Ursachen und nicht drei Fehler', () => {
56 // Ohne die Behebung standen im leeren Projekt zwei wortgleiche
57 // Fehlerzeilen ("Keine Phase angelegt") in zwei verschiedenen Karten, und
58 // die Ausgabe war "gesperrt - 3 Fehler" fuer zwei Ursachen.
59 const ergebnis = bericht(createEmptyProject('Leer', DATUM));
60 const phasenzeilen = ergebnis.findings.filter((f) => f.title === 'Keine Phase angelegt');
61 expect(phasenzeilen).toHaveLength(1);
62 expect(phasenzeilen[0]!.rule).toBe('phasen.leer');
63 expect(ergebnis.findings.map((f) => f.rule)).not.toContain('signalplan.keine-phasen');
64 expect(ergebnis.errorCount).toBe(2);
65 });
66 });
67
68 describe('Befund 35 - Ein fehlender Raeumweg erzeugt nur einen Befund', () => {
69 /** Der Musterknotenpunkt mit dem Raeumweg 0 m an einer Beziehung. */
70 function ohneRaeumweg(manualIntergreen: number | null = null): Project {
71 const roh = createStandardIntersectionProject('Ohne Räumweg', DATUM);
72 const k1 = roh.signalGroups.find((g) => g.name === 'K1')!;
73 const k3 = roh.signalGroups.find((g) => g.name === 'K3')!;
74 return {
75 ...roh,
76 conflicts: roh.conflicts.map((c) =>
77 c.fromId === k1.id && c.toId === k3.id
78 ? { ...c, clearingDistance: 0, manualIntergreen }
79 : c,
80 ),
81 };
82 }
83
84 it('meldet den Raeumweg 0 m als Fehler und nicht zusaetzlich als Warnung aus dem Planaufbau', () => {
85 const ergebnis = bericht(ohneRaeumweg());
86 const regeln = ergebnis.findings.map((f) => f.rule);
87 expect(regeln).toContain('konflikte.raeumweg-fehlt');
88 expect(regeln).not.toContain('signalplan.raeumweg-null');
89 });
90
91 it('behaelt die Meldung, wo die Regel der Pruefung schweigt', () => {
92 // Bei einer unbrauchbaren Handvorgabe schweigt konflikte.raeumweg-fehlt
93 // (sie greift nur bei manualIntergreen === null), gerechnet wird aber
94 // trotzdem - und zwar mit 0 m Raeumweg. Dort ist die Notiz die EINZIGE
95 // Meldung dazu und darf nicht verschwinden.
96 const ergebnis = bericht(ohneRaeumweg(-1));
97 const regeln = ergebnis.findings.map((f) => f.rule);
98 expect(regeln).not.toContain('konflikte.raeumweg-fehlt');
99 expect(regeln).toContain('signalplan.raeumweg-null');
100 });
101 });
102
103 describe('Befund 36 - Enger Innenradius neben einer Raeumgeschwindigkeit von Hand', () => {
104 /** K1 biegt ab; an K1 nach F2 stehen Merkmal UND Handeingabe. */
105 function merkmalUndHandeingabe(manualIntergreen: number | null = null): Project {
106 const roh = createStandardIntersectionProject('Vorrang', DATUM);
107 const k1 = roh.signalGroups.find((g) => g.name === 'K1')!;
108 const f2 = roh.signalGroups.find((g) => g.name === 'F2')!;
109 return {
110 ...roh,
111 signalGroups: roh.signalGroups.map((g) =>
112 g.id === k1.id ? { ...g, movement: 'rechts' as const } : g,
113 ),
114 conflicts: roh.conflicts.map((c) =>
115 c.fromId === k1.id && c.toId === f2.id
116 ? { ...c, engerRadius: true, clearingSpeedOverride: 9, manualIntergreen }
117 : c,
118 ),
119 };
120 }
121
122 it('meldet den Vorrang der Handeingabe genau einmal', () => {
123 const treffer = bericht(merkmalUndHandeingabe()).findings.filter((f) =>
124 f.rule.includes('enger-radius'),
125 );
126 expect(treffer).toHaveLength(1);
127 expect(treffer[0]!.rule).toBe('zwischenzeiten.enger-radius-ohne-wirkung');
128 expect(treffer[0]!.category).toBe('zwischenzeiten');
129 });
130
131 it('behaelt den Satz zur Verkuerzung, der bisher nur an der Plan-Notiz stand', () => {
132 // Die verbleibende Meldung muss sagen, in welche Richtung die Handeingabe
133 // wirkt. 9 m/s gegen 5 m/s des Merkmals: Die Zwischenzeit faellt KUERZER
134 // aus - die gefaehrliche Richtung, und ohne diesen Satz stuende sie
135 // nirgends mehr im Bericht.
136 const treffer = bericht(merkmalUndHandeingabe()).findings.find(
137 (f) => f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung',
138 );
139 expect(treffer?.message).toContain('KÜRZER');
140 });
141
142 it('behauptet neben einer von Hand gesetzten Zwischenzeit keine Verkuerzung', () => {
143 // Merkmal, Raeumgeschwindigkeit 9 m/s UND eine Zwischenzeit von 20 s von
144 // Hand: Die Vorgabe ersetzt die Rechnung vollstaendig, es geht WEDER die
145 // eingetragene Raeumgeschwindigkeit NOCH die des Merkmals in eine Zahl
146 // ein. Der Satz zur Richtung sagte hier trotzdem, die Zwischenzeit falle
147 // KÜRZER aus - eine Aussage ueber eine Zwischenzeit, die niemand
148 // gerechnet hat, und sie steht in der gedruckten Planunterlage.
149 const treffer = bericht(merkmalUndHandeingabe(20)).findings.find(
150 (f) => f.rule === 'zwischenzeiten.enger-radius-ohne-wirkung',
151 );
152 expect(treffer?.message).not.toContain('KÜRZER');
153 expect(treffer?.message).not.toContain('kürzer');
154 // Stattdessen muss dort stehen, was tatsaechlich angesetzt wird.
155 expect(treffer?.message).toContain('20 s');
156 });
157 });