lsa-planer

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

/ tests domain zeichenrepertoireKennungen.test.ts

20,0 KB Rohdatei
tests/domain/zeichenrepertoireKennungen.test.ts — 519 Zeilen
1 import { describe, expect, it } from 'vitest';
2 import { istZulaessigeKennung, parseProject } from '@/domain/model/schema';
3 import { CURRENT_SCHEMA_VERSION } from '@/domain/model/project';
4
5 /*
6 * Zeichenrepertoire fuer Kennungen (Fassung 5.12.0).
7 *
8 * Das Einlesen prueft an einer Kennung bis dahin nur zweierlei: leer und
9 * doppelt vergeben. Sonst kommt sie mit beliebigem Inhalt aus der Datei. Drei
10 * Befunde sind daraus geworden - 61 und seine Folge (Paarschluessel
11 * aus Signalgruppenkennungen) und 43 (Wegschluessel aus Planlinienkennungen);
12 * behoben war jedes Mal die Schluesselbildung, nie die Kennung.
13 *
14 * WAS DIESE DATEI BEWACHT: dass die Quelle geschlossen ist. Eine Kennung
15 * ausserhalb des Repertoires wird ERSETZT und GEMELDET, und jeder Verweis
16 * darauf zieht mit - Konfliktbeziehungen, Phasen, Verkehrsstaerken, Fahrlinien,
17 * Haltlinien und die Zuordnung zur Zufahrt. Aufgeloest wird nichts: Die Kennung
18 * gehoert genau einem Datensatz, es ist nichts zu raten, und ein Verlust der
19 * Konfliktbeziehungen waere die gefaehrliche Richtung (`intergreenBetween`
20 * liefert fuer eine fehlende Beziehung 0).
21 *
22 * GEGEN DEN ALTSTAND (Fassung 5.11.0) sind 11 der 15 Faelle
23 * rot - gemessen, indem die HEAD-Fassung von schema.ts ueber ein Vite-Plugin
24 * untergeschoben wurde; der Arbeitsbaum blieb unangetastet. Was dabei
25 * herauskommt, ist nicht einerlei:
26 *
27 * - Rot ueber das VERHALTEN sind neun Faelle - jeder, der eine Ersetzung,
28 * eine Meldung oder deren Wortlaut erwartet. Dazu gehoert auch der Fall
29 * zur Doppelvergabe: Im Altstand bleibt die ERSTE, unzulaessige Kennung
30 * stehen. Er prueft das unmittelbar am Trennzeichen und nicht ueber
31 * `istZulaessigeKennung`, damit er dort nicht schon ueber die fehlende
32 * Ausfuhr faellt.
33 * - Rot ueber die fehlende AUSFUHR sind die beiden Faelle, die
34 * `istZulaessigeKennung` selbst festhalten - die Funktion gibt es im
35 * Altstand nicht. Sie bewachen das Repertoire, nicht den Einleseweg.
36 * - GRUEN bleiben dort vier: die Gegenprobe mit gewoehnlichen Kennungen; der
37 * Fall zum Nachziehen der Verweise (ohne Ersetzung faellt die alte Kennung
38 * mit der neuen zusammen, die Erwartung trifft dann von selbst zu); und die
39 * beiden Faelle zum Zusammentreffen von unzulaessig und doppelt, die das
40 * Aufloesen der Verweise und das Fehlen einer beschoenigenden Meldung
41 * festhalten - beides tut der Altstand ebenso, er meldet dort ueberhaupt
42 * nichts.
43 *
44 * GEGEN DEN STAND VOR DEM NACHTRAG (Ersatzmeldung sofort abgesetzt,
45 * `kennungImKlartext` nur ueber \p{Cc}) sind drei der vier nachgereichten
46 * Faelle rot: die beiden zur zurueckgenommenen Umschluesselung und der zu den
47 * unsichtbaren Zeichen. Gruen bleiben dort die Gegenprobe zum gewoehnlichen
48 * Leerzeichen und der Fall, der das Aufloesen der Verweise festhaelt - am
49 * Verhalten hat der Nachtrag nichts geaendert, nur an den Meldungen.
50 */
51
52 const DATUM = new Date('2026-01-01T00:00:00Z');
53
54 /** Meldungen zu einem Pfad, damit sich die Erwartung ablesen laesst. */
55 function meldungen(
56 issues: readonly { path: string; message: string }[],
57 path: string,
58 ): readonly string[] {
59 return issues.filter((i) => i.path === path).map((i) => i.message);
60 }
61
62 describe('E.1 - Zeichenrepertoire der Kennungen', () => {
63 it('nimmt an, was das Programm selbst schreibt', () => {
64 // createId: "<Praefix>-<Zaehler zur Basis 36>-<8 Stellen hexadezimal>".
65 expect(istZulaessigeKennung('sg-1z-4bb787ea')).toBe(true);
66 // tools/vierarmigerKnotenpunkt.ts und die eingefrorene Beispieldatei.
67 expect(istZulaessigeKennung('arm-nord')).toBe(true);
68 expect(istZulaessigeKennung('arm-nord-geradeaus')).toBe(true);
69 expect(istZulaessigeKennung('hl-arm-nord')).toBe(true);
70 expect(istZulaessigeKennung('ph-1')).toBe(true);
71 // Von Hand oder aus einem Fremdwerkzeug: erlaubt, weil sie nichts brechen.
72 expect(istZulaessigeKennung('SG_1')).toBe(true);
73 expect(istZulaessigeKennung('sg.1')).toBe(true);
74 });
75
76 it('weist die Zeichen ab, die belegt etwas brechen', () => {
77 // Die Trennzeichen der Paarschluessel (intergreenKey, maskiereKennung).
78 expect(istZulaessigeKennung('a|b')).toBe(false);
79 expect(istZulaessigeKennung('a~b')).toBe(false);
80 // Das Anfuehrungszeichen bricht [data-hervorheben="<Kennung>"].
81 expect(istZulaessigeKennung('a"b')).toBe(false);
82 // Steuerzeichen und Leerraum zerreissen jede zeilenweise Ausgabe.
83 expect(istZulaessigeKennung('a b')).toBe(false);
84 expect(istZulaessigeKennung('a\nb')).toBe(false);
85 // Der alte Trennstrich aus parseConflicts und wegSchluessel.
86 expect(istZulaessigeKennung('a->b')).toBe(false);
87 // Die Laenge gehoert zum Repertoire: 64 ja, 65 nein.
88 expect(istZulaessigeKennung('a'.repeat(64))).toBe(true);
89 expect(istZulaessigeKennung('a'.repeat(65))).toBe(false);
90 // Die leere Kennung ist nicht Sache des Repertoires, sondern der
91 // Neuvergabe an der Fundstelle - dort ohne Meldung.
92 expect(istZulaessigeKennung('')).toBe(false);
93 });
94 });
95
96 describe('E.1 - Signalgruppenkennung ausserhalb des Repertoires', () => {
97 /** Ein Projekt, in dem alles auf die Signalgruppe "sg|a" verweist. */
98 const datei = {
99 schemaVersion: CURRENT_SCHEMA_VERSION,
100 intersection: { arms: [{ id: 'arm-nord', name: 'Nord' }] },
101 signalGroups: [
102 { id: 'sg|a', name: 'K1', mode: 'kfz', movement: 'geradeaus', armId: 'arm-nord' },
103 { id: 'sg-b', name: 'F1', mode: 'fuss', movement: 'querung', vehicleClass: 'keine' },
104 ],
105 conflicts: [
106 { id: 'cf-1', fromId: 'sg|a', toId: 'sg-b', clearingDistance: 12, enteringDistance: 4 },
107 ],
108 phases: [{ id: 'ph-1', name: 'Phase 1', signalGroupIds: ['sg|a'] }],
109 demands: [{ signalGroupId: 'sg|a', volume: 600, heavyVehicleShare: 0.05 }],
110 lageplan: {
111 haltlinien: [
112 {
113 id: 'hl-1',
114 name: 'Zufahrt Nord',
115 art: 'zufahrt',
116 armId: 'arm-nord',
117 von: { x: 0, y: 0 },
118 bis: { x: 20, y: 0 },
119 },
120 ],
121 linien: [
122 {
123 id: 'pl-1',
124 name: 'L1',
125 mode: 'kfz',
126 movement: 'geradeaus',
127 punkte: [
128 { x: 10, y: 0 },
129 { x: 10, y: 40 },
130 ],
131 haltlinieId: 'hl-1',
132 startT: 0.5,
133 signalGroupId: 'sg|a',
134 },
135 ],
136 },
137 program: { phaseOrder: ['ph-1'] },
138 };
139
140 it('ersetzt die Kennung und meldet es', () => {
141 const { project, issues, repaired } = parseProject(datei, DATUM);
142
143 const ersetzt = project.signalGroups[0]!;
144 expect(ersetzt.name).toBe('K1');
145 expect(ersetzt.id).not.toBe('sg|a');
146 expect(istZulaessigeKennung(ersetzt.id)).toBe(true);
147 expect(repaired).toBe(true);
148 expect(meldungen(issues, 'signalGroups[0]')).toHaveLength(1);
149 expect(meldungen(issues, 'signalGroups[0]')[0]).toContain('sg|a');
150 });
151
152 it('zieht alle Verweise auf die neue Kennung nach, statt sie aufzuloesen', () => {
153 const { project, issues } = parseProject(datei, DATUM);
154 const neu = project.signalGroups[0]!.id;
155
156 // Die feindliche Beziehung bleibt - ihr Verlust waere die gefaehrliche
157 // Richtung: intergreenBetween liefert fuer eine fehlende Beziehung 0.
158 expect(project.conflicts).toHaveLength(1);
159 expect(project.conflicts[0]!.fromId).toBe(neu);
160 expect(project.conflicts[0]!.toId).toBe('sg-b');
161 expect(project.conflicts[0]!.clearingDistance).toBe(12);
162
163 expect(project.phases[0]!.signalGroupIds).toEqual([neu]);
164 expect(project.demands).toHaveLength(1);
165 expect(project.demands[0]!.signalGroupId).toBe(neu);
166 expect(project.demands[0]!.volume).toBe(600);
167 expect(project.lageplan.linien[0]!.signalGroupId).toBe(neu);
168
169 // Und kein Verweis meldet einen Verlust: Gemeldet wird einmal, an der
170 // Kennung selbst.
171 const verluste = issues.filter((i) => /nicht vorhandene|aufgehoben|entfernt/.test(i.message));
172 expect(verluste).toEqual([]);
173 });
174 });
175
176 describe('E.1 - die uebrigen Kennungen', () => {
177 it('ersetzt eine Zufahrtskennung und zieht Signalgruppe und Haltlinie nach', () => {
178 const { project, issues } = parseProject(
179 {
180 schemaVersion: CURRENT_SCHEMA_VERSION,
181 intersection: { arms: [{ id: 'arm nord', name: 'Nord' }] },
182 signalGroups: [{ id: 'sg-1', name: 'K1', armId: 'arm nord' }],
183 lageplan: {
184 haltlinien: [
185 {
186 id: 'hl-1',
187 name: 'Zufahrt',
188 art: 'zufahrt',
189 armId: 'arm nord',
190 von: { x: 0, y: 0 },
191 bis: { x: 20, y: 0 },
192 },
193 ],
194 linien: [],
195 },
196 },
197 DATUM,
198 );
199
200 const neu = project.intersection.arms[0]!.id;
201 expect(neu).not.toBe('arm nord');
202 expect(istZulaessigeKennung(neu)).toBe(true);
203 expect(project.signalGroups[0]!.armId).toBe(neu);
204 expect(project.lageplan.haltlinien[0]!.armId).toBe(neu);
205 expect(meldungen(issues, 'intersection.arms[0]')).toHaveLength(1);
206 });
207
208 it('ersetzt eine Phasenkennung und zieht die Phasenfolge nach', () => {
209 const { project, issues } = parseProject(
210 {
211 schemaVersion: CURRENT_SCHEMA_VERSION,
212 signalGroups: [{ id: 'sg-1', name: 'K1' }],
213 phases: [
214 // Ein Steuerzeichen in der Kennung - in jeder Ansicht unsichtbar.
215 { id: 'ph\u0001', name: 'Phase 1', signalGroupIds: ['sg-1'] },
216 { id: 'ph-2', name: 'Phase 2', signalGroupIds: [] },
217 ],
218 program: { phaseOrder: ['ph-2', 'ph\u0001'] },
219 },
220 DATUM,
221 );
222
223 const neu = project.phases[0]!.id;
224 expect(neu).not.toBe('ph\u0001');
225 expect(project.program.phaseOrder).toEqual(['ph-2', neu]);
226 // Das Steuerzeichen steht sichtbar in der Meldung, nicht als Zeilenbruch.
227 expect(meldungen(issues, 'phases[0]')[0]).toContain('\\u0001');
228 });
229
230 it('ersetzt eine Haltlinienkennung und zieht die Fahrlinie nach', () => {
231 const { project, issues } = parseProject(
232 {
233 schemaVersion: CURRENT_SCHEMA_VERSION,
234 signalGroups: [],
235 lageplan: {
236 haltlinien: [
237 {
238 id: 'hl->1',
239 name: 'Zufahrt',
240 art: 'zufahrt',
241 von: { x: 0, y: 0 },
242 bis: { x: 20, y: 0 },
243 },
244 ],
245 linien: [
246 {
247 id: 'pl-1',
248 name: 'L1',
249 mode: 'kfz',
250 movement: 'geradeaus',
251 punkte: [
252 { x: 10, y: 0 },
253 { x: 10, y: 40 },
254 ],
255 haltlinieId: 'hl->1',
256 startT: 0.5,
257 },
258 ],
259 },
260 },
261 DATUM,
262 );
263
264 const neu = project.lageplan.haltlinien[0]!.id;
265 expect(neu).not.toBe('hl->1');
266 expect(project.lageplan.linien[0]!.haltlinieId).toBe(neu);
267 expect(meldungen(issues, 'lageplan.haltlinien[0]')).toHaveLength(1);
268 });
269
270 it('ersetzt Konflikt- und Fahrlinienkennung, auf die in der Datei nichts verweist', () => {
271 const { project, issues } = parseProject(
272 {
273 schemaVersion: CURRENT_SCHEMA_VERSION,
274 signalGroups: [
275 { id: 'sg-1', name: 'K1' },
276 { id: 'sg-2', name: 'K2' },
277 ],
278 conflicts: [{ id: 'cf|1', fromId: 'sg-1', toId: 'sg-2', clearingDistance: 9 }],
279 lageplan: {
280 haltlinien: [],
281 linien: [
282 {
283 id: 'pl|1',
284 name: 'L1',
285 mode: 'kfz',
286 movement: 'geradeaus',
287 punkte: [
288 { x: 0, y: 0 },
289 { x: 0, y: 40 },
290 ],
291 },
292 ],
293 },
294 },
295 DATUM,
296 );
297
298 expect(project.conflicts).toHaveLength(1);
299 expect(project.conflicts[0]!.id).not.toBe('cf|1');
300 expect(project.conflicts[0]!.clearingDistance).toBe(9);
301 expect(istZulaessigeKennung(project.conflicts[0]!.id)).toBe(true);
302 expect(meldungen(issues, 'conflicts[0]')).toHaveLength(1);
303
304 expect(project.lageplan.linien[0]!.id).not.toBe('pl|1');
305 expect(istZulaessigeKennung(project.lageplan.linien[0]!.id)).toBe(true);
306 expect(meldungen(issues, 'lageplan.linien[0]')).toHaveLength(1);
307 });
308 });
309
310 describe('E.1 - unzulaessig UND doppelt vergeben', () => {
311 it('behandelt die zweite Vergabe als mehrdeutig und loest die Verweise auf', () => {
312 // Jetzt ist wieder offen, welche der beiden Gruppen gemeint ist - hier
313 // wird aufgeloest statt umgeschluesselt.
314 const { project, issues } = parseProject(
315 {
316 schemaVersion: CURRENT_SCHEMA_VERSION,
317 signalGroups: [
318 { id: 'sg|a', name: 'K1' },
319 { id: 'sg|a', name: 'K2' },
320 { id: 'sg-b', name: 'F1', mode: 'fuss', movement: 'querung', vehicleClass: 'keine' },
321 ],
322 conflicts: [{ id: 'cf-1', fromId: 'sg|a', toId: 'sg-b', clearingDistance: 12 }],
323 demands: [{ signalGroupId: 'sg|a', volume: 600 }],
324 },
325 DATUM,
326 );
327
328 expect(project.signalGroups.map((g) => g.name)).toEqual(['K1', 'K2', 'F1']);
329 expect(new Set(project.signalGroups.map((g) => g.id)).size).toBe(3);
330 // Nicht ueber `istZulaessigeKennung` geprueft, sondern unmittelbar am
331 // Trennzeichen: Im Altstand fehlt die Ausfuhr, und der Fall fiele dort
332 // ueber einen TypeError statt ueber das, was er bewacht - dass auch die
333 // erste, unzulaessige Kennung ersetzt ist und nicht bloss die zweite.
334 for (const gruppe of project.signalGroups) {
335 expect(gruppe.id).not.toContain('|');
336 }
337 expect(project.conflicts).toEqual([]);
338 expect(project.demands).toEqual([]);
339 expect(issues.some((i) => i.message.includes('kommt mehrfach vor'))).toBe(true);
340 expect(
341 issues.some((i) => i.path === 'demands[0]' && i.message.includes('mehrfach vergebene')),
342 ).toBe(true);
343 });
344
345 /**
346 * Alle vier Kennungsarten mit Umschluesselungskarte, jede zweimal mit
347 * derselben unzulaessigen Kennung: Zufahrt, Signalgruppe, Phase, Haltlinie.
348 * Konfliktbeziehung und Fahrlinie fuehren keine Karte - auf ihre Kennungen
349 * verweist in der Datei nichts, dort ist nichts zurueckzunehmen.
350 */
351 const doppeltUndUnzulaessig = {
352 schemaVersion: CURRENT_SCHEMA_VERSION,
353 intersection: {
354 arms: [
355 { id: 'arm|n', name: 'Nord' },
356 { id: 'arm|n', name: 'Nord zwei' },
357 ],
358 },
359 signalGroups: [
360 { id: 'sg|a', name: 'K1', mode: 'kfz', movement: 'geradeaus', armId: 'arm|n' },
361 { id: 'sg|a', name: 'K2', mode: 'kfz', movement: 'geradeaus' },
362 { id: 'sg-b', name: 'F1', mode: 'fuss', movement: 'querung', vehicleClass: 'keine' },
363 ],
364 conflicts: [
365 { id: 'cf-1', fromId: 'sg|a', toId: 'sg-b', clearingDistance: 12, enteringDistance: 4 },
366 ],
367 phases: [
368 { id: 'ph|1', name: 'Phase 1', signalGroupIds: ['sg|a'] },
369 { id: 'ph|1', name: 'Phase 2', signalGroupIds: [] },
370 ],
371 demands: [{ signalGroupId: 'sg|a', volume: 600 }],
372 lageplan: {
373 haltlinien: [
374 {
375 id: 'hl|1',
376 name: 'Zufahrt Nord',
377 art: 'zufahrt',
378 armId: 'arm|n',
379 von: { x: 0, y: 0 },
380 bis: { x: 20, y: 0 },
381 },
382 {
383 id: 'hl|1',
384 name: 'Zufahrt Nord zwei',
385 art: 'zufahrt',
386 von: { x: 0, y: 30 },
387 bis: { x: 20, y: 30 },
388 },
389 ],
390 linien: [
391 {
392 id: 'pl-1',
393 name: 'L1',
394 mode: 'kfz',
395 movement: 'geradeaus',
396 punkte: [
397 { x: 10, y: 0 },
398 { x: 10, y: 40 },
399 ],
400 haltlinieId: 'hl|1',
401 startT: 0.5,
402 signalGroupId: 'sg|a',
403 },
404 ],
405 },
406 program: { phaseOrder: ['ph|1'] },
407 };
408
409 it('behauptet nicht mehr, an den Verweisen aendere sich nichts', () => {
410 // Der Einlesebericht sagte dem Planer an genau der Stelle, an der eine
411 // Konfliktbeziehung verlorengeht, dass nichts verlorengeht: Die
412 // Ersatzmeldung war schon abgesetzt, als die zweite Vergabe die
413 // Umschluesselung wieder zuruecknahm.
414 const { issues } = parseProject(doppeltUndUnzulaessig, DATUM);
415
416 expect(issues.filter((i) => i.message.includes('ändert sich nichts'))).toEqual([]);
417 });
418
419 it('sagt an jeder der vier Kennungen, dass die Umschluesselung zurueckgenommen ist', () => {
420 const { issues } = parseProject(doppeltUndUnzulaessig, DATUM);
421
422 for (const path of [
423 'intersection.arms[0]',
424 'signalGroups[0]',
425 'phases[0]',
426 'lageplan.haltlinien[0]',
427 ]) {
428 // An "phases[0]" steht daneben noch die aufgeloeste Zuordnung zur
429 // mehrfach vergebenen Signalgruppe; gemeint ist hier die Ersatzmeldung.
430 const zurKennung = meldungen(issues, path).filter((m) => m.includes('Sie wurde durch'));
431 expect(zurKennung).toHaveLength(1);
432 expect(zurKennung[0]).toContain('zurückgenommen');
433 }
434 });
435
436 it('loest die Verweise auf alle vier Kennungen tatsaechlich auf', () => {
437 // Die Meldung und das, was geschieht, muessen dasselbe sagen - deshalb
438 // steht hier neben dem Wortlaut auch das Ergebnis.
439 const { project } = parseProject(doppeltUndUnzulaessig, DATUM);
440
441 expect(project.signalGroups[0]!.armId).toBeNull();
442 expect(project.lageplan.haltlinien[0]!.armId).toBeNull();
443 expect(project.conflicts).toEqual([]);
444 expect(project.demands).toEqual([]);
445 expect(project.phases[0]!.signalGroupIds).toEqual([]);
446 expect(project.lageplan.linien[0]!.haltlinieId).toBeNull();
447 expect(project.lageplan.linien[0]!.signalGroupId).toBeNull();
448 });
449 });
450
451 describe('E.1 - die Meldung macht unsichtbare Zeichen sichtbar', () => {
452 // Ueber die Zeichennummer gebildet, damit im Quelltext dieser Datei kein
453 // unsichtbares Zeichen steht - man saehe hier so wenig wie in der Meldung.
454 /** U+200B, das Zeichen ohne Breite. */
455 const OHNE_BREITE = String.fromCharCode(0x200b);
456 /** U+00A0, das geschuetzte Leerzeichen. */
457 const GESCHUETZT = String.fromCharCode(0x00a0);
458
459 /** Die Meldung, die das Einlesen zur Signalgruppenkennung `roh` absetzt. */
460 function meldungZurKennung(roh: string): string {
461 const { issues } = parseProject(
462 { schemaVersion: CURRENT_SCHEMA_VERSION, signalGroups: [{ id: roh, name: 'K1' }] },
463 DATUM,
464 );
465 return meldungen(issues, 'signalGroups[0]')[0] ?? '';
466 }
467
468 it('schreibt Zeichen aus, die man sonst nicht saehe', () => {
469 // Ohne die Ersetzung stuende auf dem Schirm "sga" - eine Kennung, die
470 // aussieht wie eine zulaessige, und der Anwender staende vor einer
471 // Beanstandung, die er nicht nachvollziehen kann.
472 expect(meldungZurKennung(`sg${OHNE_BREITE}a`)).toContain('sg\\u200ba');
473 // Das geschuetzte Leerzeichen saehe wie ein gewoehnliches aus.
474 expect(meldungZurKennung(`sg${GESCHUETZT}a`)).toContain('sg\\u00a0a');
475 // Der Zeilenumbruch zerrisse die Meldung.
476 expect(meldungZurKennung('sg\na')).toContain('sg\\u000aa');
477 });
478
479 it('laesst das gewoehnliche Leerzeichen stehen', () => {
480 // Zwischen den Anfuehrungszeichen ist es zu erkennen; eine Zahlenfolge
481 // waere hier schwerer zu lesen als die Kennung selbst.
482 expect(meldungZurKennung('arm nord')).toContain('"arm nord"');
483 });
484 });
485
486 describe('E.1 - Gegenprobe', () => {
487 it('laesst eine Datei mit gewoehnlichen Kennungen unveraendert und ohne Meldung', () => {
488 const { project, issues, repaired } = parseProject(
489 {
490 schemaVersion: CURRENT_SCHEMA_VERSION,
491 intersection: { arms: [{ id: 'arm-nord', name: 'Nord' }] },
492 signalGroups: [
493 { id: 'sg-1z-4bb787ea', name: 'K1', armId: 'arm-nord' },
494 { id: 'arm-nord-geradeaus', name: 'K2' },
495 ],
496 conflicts: [
497 {
498 id: 'cf-10-fa4aa6f5',
499 fromId: 'sg-1z-4bb787ea',
500 toId: 'arm-nord-geradeaus',
501 clearingDistance: 12,
502 enteringDistance: 4,
503 },
504 ],
505 phases: [{ id: 'ph-1', name: 'Phase 1', signalGroupIds: ['sg-1z-4bb787ea'] }],
506 program: { phaseOrder: ['ph-1'] },
507 },
508 DATUM,
509 );
510
511 expect(project.signalGroups.map((g) => g.id)).toEqual(['sg-1z-4bb787ea', 'arm-nord-geradeaus']);
512 expect(project.signalGroups[0]!.armId).toBe('arm-nord');
513 expect(project.conflicts[0]!.id).toBe('cf-10-fa4aa6f5');
514 expect(project.phases[0]!.id).toBe('ph-1');
515 expect(project.program.phaseOrder).toEqual(['ph-1']);
516 expect(issues).toEqual([]);
517 expect(repaired).toBe(false);
518 });
519 });