import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { leiteAb, uebernimmWege, wegSchluessel } from '@/domain/geometrie/ableitung'; import { parseProject } from '@/domain/model/schema'; import { createConflict, createEmptyProject, createPhase, createSignalGroup, createStandardIntersectionProject, } from '@/domain/model/factory'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { koordinierungslage } from '@/domain/plan/koordinierung'; import { distributeGreenTimes } from '@/domain/rilsa/greenSplit'; import * as actions from '@/app/actions'; import type { SignalPlan } from '@/domain/plan/signalPlan'; import type { Project } from '@/domain/model/project'; /** * Gleichstand entscheidet nicht mehr die Reihenfolge. * * WORUM ES GEHT. Fassung 5.23.0 hat einen Fall behoben, in dem zwei in allem * gleiche Richtungen einer Baustellenampel verschieden lange Freigabezeiten * bekamen, weil bei gleichem Gewicht der Array-Index entschied. Die Frage * danach war, ob es weitere solche Stellen gibt. Eine Suche ueber den Baum hat * vier gefunden - alle von derselben Gestalt: ein Vergleich mit `>`, bei dem * der Gleichstand still an die Feldreihenfolge faellt. Eine FUENFTE ist mit * Fassung 5.43.0 dazugekommen (Abschnitt E): Die Suche vom * 10.09.2026 konnte sie nicht finden, denn die Koordinierungskette ist erst mit * 5.35.0 in den Baum gekommen. Diese Datei bewacht alle fuenf. * * DREI DER VIER SIND ENTSCHIEDEN, EINER IST AUSGESPROCHEN, und der Unterschied * ist der Kern der Sache: * * A Uebernahme aus dem Lageplan (geometrie/ableitung.ts). Zwei Strompaare * desselben Gruppenpaares runden auf dieselbe Zwischenzeit auf. Jetzt * entscheidet der ungerundete Wert - eine fachliche Auskunft, die vorher * ungenutzt blieb. * * B Massgebende Beziehung eines Phasenuebergangs (plan/signalPlan.ts). Erst * die aufgerundete Zwischenzeit, dann der ungerundete Wert, dann der * Raeumweg - und zuletzt der Name, der nichts bedeutet, sich aber nicht * verschiebt, wenn jemand eine Signalgruppe neu zuordnet. * * C Warnung zur Hoechstfreigabezeit (plan/signalPlan.ts). Tragen zwei * Signalgruppen derselben Phase dieselbe wirkungslose Vorgabe, werden jetzt * beide genannt. Es gibt nichts zu waehlen - es waren nur nicht alle * genannt. * * D Die zusaetzliche Sekunde beim Summenausgleich (rilsa/greenSplit.ts). Hier * laesst sich nichts entscheiden: Ist die verfuegbare Zeit ungerade, MUSS * eine von zwei gleichgestellten Phasen eine Sekunde mehr bekommen. Jede * Regel dafuer waere so beliebig wie die jetzige. Geaendert ist, dass es * dasteht. * * E Das Freigabefenster der koordinierten Hauptrichtung * (plan/koordinierung.ts, Fassung 5.43.0). Hier GIBT es eine * feinere fachliche Auskunft, und sie entscheidet jetzt: welches der beiden * gleich langen Fenster ueberhaupt ein durchgehendes Band ergibt. Erst bei * gleicher Bandbreite eine stabile Ordnung, und dass DIESE nichts bedeutet, * steht im Quelltext. Die Ansicht meldet den Fall ausserdem. * * MUTATIONSNACHWEIS (10.09.2026, acht Mutationen an drei Dateien): Jede wird * gefunden. Zwei Mutationen an der Uebernahme, zwei am Rang des Uebergangs, * zwei an der Warnung, zwei an der Restsekunde - je einer bis zwei rote Faelle. * * EINE BLIEB IM ERSTEN ANLAUF GRUEN, und das gehoert hierher: Laesst man den * Rang des Uebergangs auf die aufgerundete Zwischenzeit zusammenfallen, meldete * sich nichts. Der Fall an der Beispieldatei prueft die Regel dort naemlich * nicht - das Umdrehen der Gruppenzuordnung kehrt die Reihenfolge der beiden * gleichauf liegenden Paare gar nicht um. Erst der eigens gebaute Fall mit zwei * Beziehungen (4,2 s und 4,8 s, beide 5 s) hat Zaehne. */ const DATEI = 'beispiele/vierarmiger-knotenpunkt.lsap'; const DATUM = new Date('2026-01-01T00:00:00Z'); function beispiel(): Project { return parseProject(JSON.parse(readFileSync(DATEI, 'utf8')), DATUM).project; } /** Dieselbe Datei nach "Ausgewählte übernehmen" ueber alle Beziehungen. */ function nachUebernahme(basis: Project): Project { const ableitung = leiteAb(basis, basis.lageplan); return uebernimmWege(basis, basis.lageplan, ableitung, ableitung.wege.map(wegSchluessel), DATUM) .project; } /** Dasselbe Projekt mit umgekehrter Zeichenreihenfolge der Fahrlinien. */ function mitUmgekehrtenLinien(basis: Project): Project { return { ...basis, lageplan: { ...basis.lageplan, linien: [...basis.lageplan.linien].reverse() }, }; } /** Die uebernommene Geometrie je Beziehung, in vergleichbarer Form. */ function geometrie(project: Project): string[] { return project.conflicts .map( (c) => `${c.fromId}->${c.toId} sr=${c.clearingDistance.toFixed(3)} se=${c.enteringDistance.toFixed(3)} rb=${String(c.raeumbeziehung)}`, ) .sort(); } describe('A: Die Uebernahme aus dem Lageplan haengt nicht an der Zeichenreihenfolge', () => { it('liefert dieselbe Geometrie, wenn die Fahrlinien umgekehrt gezeichnet sind', () => { /* * DER EIGENTLICHE FALL. Bis 5.23.0 gewann bei gleicher aufgerundeter * Zwischenzeit der zuerst gezeichnete Strom, und mit ihm wurden sein * Raeumweg, sein Einfahrweg und seine Fahrbeziehung gespeichert. Wer * dieselbe Oertlichkeit in anderer Reihenfolge zeichnete, bekam eine * andere Projektdatei und eine andere gedruckte Konflikttabelle. */ const vorwaerts = nachUebernahme(beispiel()); const rueckwaerts = nachUebernahme(mitUmgekehrtenLinien(beispiel())); expect(geometrie(rueckwaerts)).toEqual(geometrie(vorwaerts)); }); it('nimmt bei gleicher Zwischenzeit den anspruchsvolleren Strom', () => { /* * Die eigene Furt: "Nord geradeaus" fordert roh 4,100 s (bis 5.42.1 * 4,013 s; seither faehrt der Fussgaenger mit te = 0 s ein), "Nord rechts" * fordert 4,591 s, beide runden auf 5 s. Gespeichert wird seit 5.24.0 der * Rechtsabbieger - der Strom, der massgebend BLEIBT, wenn sich Gelbzeit * oder zulaessige Hoechstgeschwindigkeit spaeter aendern. Die gedruckte * Zwischenzeit ist in beiden Faellen dieselbe. */ const project = nachUebernahme(beispiel()); const furt = project.conflicts.find((c) => c.fromId === 'sg-k-nord' && c.toId === 'sg-f-nord'); expect(furt, 'Beziehung K Nord -> F Nord').toBeDefined(); expect(furt!.raeumbeziehung, 'der Rechtsabbieger gibt den Ausschlag').toBe('rechts'); const plan = buildSignalPlan(project); expect( [...plan.intergreens.values()].find((i) => i.fromId === 'sg-k-nord' && i.toId === 'sg-f-nord') ?.value, 'die gedruckte Zahl bleibt dieselbe', ).toBe(5); }); }); describe('B: Die massgebende Beziehung eines Uebergangs haengt nicht an der Gruppenreihenfolge', () => { /** Dasselbe Projekt mit umgekehrter Zuordnung der Gruppen in jeder Phase. */ function mitUmgekehrtenGruppen(basis: Project): Project { return { ...basis, phases: basis.phases.map((p) => ({ ...p, signalGroupIds: [...p.signalGroupIds].reverse(), })), }; } const beschriftung = (project: Project): string[] => buildSignalPlan(project).transitions.map( (t) => `${t.duration}:${t.critical === null ? '-' : `${t.critical.fromGroupName}->${t.critical.toGroupName}`}`, ); it('nennt dieselbe Beziehung, wenn die Signalgruppen der Phase umgekehrt zugeordnet sind', () => { /* * Nachgestellt aus der Bedienung: In der Phasenansicht eine Signalgruppe * abwaehlen und wieder anwaehlen haengt sie hinten an. Bis 5.23.0 stand * danach in der Uebergangstabelle der Planunterlage ein anderes Paar, bei * Byte fuer Byte demselben Plan. */ const basis = beispiel(); expect(beschriftung(mitUmgekehrtenGruppen(basis))).toEqual(beschriftung(basis)); }); /** * Zwei Beziehungen mit derselben gedruckten Zwischenzeit und verschiedenem * ungerundeten Wert - eigens dafuer gebaut. * * WARUM ES DIESEN FALL BRAUCHT. Der Fall darueber laeuft an der * Beispieldatei, und dort war er ZAHNLOS: Eine Mutation, die den Rang auf * die aufgerundete Zwischenzeit zusammenfallen laesst, blieb gruen. Am * Beispielknotenpunkt kehrt das Umdrehen der Gruppenzuordnung die * Reihenfolge der beiden gleichauf liegenden Paare gar nicht um - der Fall * prueft dort also die Regel nicht, sondern eine Zufaelligkeit. Hier stehen * genau zwei Paare, und ihre Reihenfolge kehrt sich wirklich um. * * tue = 3 s, vr = 10 m/s, Pkw-Zuschlag 6 m, kein Einfahrweg: * 6 m ergeben roh 4,2 s, 12 m ergeben roh 4,8 s - beide 5 s. */ function zweiGleichauf(raeumwege: readonly [number, number]): Project { const basis = createEmptyProject('Gleichauf', DATUM, 'knotenpunkt'); const gruppe = (name: string, index: number) => createSignalGroup({ name, mode: 'kfz' as const, vZul: 50, index }); const a = gruppe('A', 0); const b = gruppe('B', 1); const c = gruppe('C', 2); const d = gruppe('D', 3); const p1 = createPhase('Erste', [a.id, b.id]); const p2 = createPhase('Zweite', [c.id, d.id]); const beziehung = (von: string, nach: string, weg: number) => ({ ...createConflict(von, nach), clearingDistance: weg, enteringDistance: 0, }); return { ...basis, signalGroups: [a, b, c, d], phases: [p1, p2], conflicts: [ beziehung(a.id, c.id, raeumwege[0]), beziehung(b.id, d.id, raeumwege[1]), beziehung(c.id, a.id, raeumwege[0]), beziehung(d.id, b.id, raeumwege[1]), ], program: { ...basis.program, phaseOrder: [p1.id, p2.id] }, }; } it('waehlt bei gleicher gedruckter Zwischenzeit die Beziehung mit dem groesseren Rohwert', () => { const plan = buildSignalPlan(zweiGleichauf([6, 12])); const erster = plan.transitions[0]; expect(erster, 'erster Uebergang').toBeDefined(); expect(erster!.duration, 'beide runden auf 5 s').toBe(5); expect( [...plan.intergreens.values()].map((i) => i.value).every((v) => v === 5), 'wirklich Gleichstand in der gedruckten Zahl', ).toBe(true); // B -> D fordert roh 4,8 s, A -> C nur 4,2 s. expect(erster!.critical?.fromGroupName).toBe('B'); expect(erster!.critical?.toGroupName).toBe('D'); }); it('waehlt sie auch, wenn die Gruppen umgekehrt zugeordnet sind', () => { // Ohne den Rang entschiede hier die Reihenfolge, und die Antwort kippte. const basis = zweiGleichauf([6, 12]); expect(beschriftung(mitUmgekehrtenGruppen(basis))).toEqual(beschriftung(basis)); expect(beschriftung(basis)[0]).toContain('B->D'); }); it('kehrt die Antwort mit dem Rohwert um, wenn die Wege getauscht sind', () => { // Die Gegenprobe zum Fall darueber: Nicht der Name "B" gewinnt, sondern // der groessere Rohwert - sonst pruefte der Fall nur eine Konstante. const plan = buildSignalPlan(zweiGleichauf([12, 6])); expect(plan.transitions[0]?.critical?.fromGroupName).toBe('A'); expect(plan.transitions[0]?.critical?.toGroupName).toBe('C'); }); it('bleibt die Dauer der Uebergaenge davon ohnehin unberuehrt', () => { // Die Gegenprobe: Der Fall oben waere auch dann gruen, wenn die Uebergaenge // gar keine Beschriftung mehr traegen. Hier steht, dass es sie gibt. const plan = buildSignalPlan(beispiel()); expect(plan.transitions.length).toBeGreaterThan(0); expect(plan.transitions.filter((t) => t.critical !== null).length).toBe( plan.transitions.length, ); expect(plan.cycleTime).toBe(100); }); it('meldet keine massgebende Beziehung, wo alle Zwischenzeiten 0 s sind', () => { /* * `critical === null` heisst "keine Beziehung mit einer Zwischenzeit ueber * 0 s"; daran haengen die Uebergangszeile der Ansicht und der Fehler * zwischenzeiten.uebergang-ungesichert. Beim Umbau auf den Rang ist diese * Bedingung einmal herausgefallen - drei Wachen haben es gemeldet, und * dieser Fall haelt sie an Ort und Stelle fest. */ const basis = createStandardIntersectionProject('Null', DATUM); const ohneWege: Project = { ...basis, conflicts: basis.conflicts.map((c) => ({ ...c, clearingDistance: 0, enteringDistance: 0, manualIntergreen: 0, })), }; const plan = buildSignalPlan(ohneWege); expect([...plan.intergreens.values()].every((i) => i.value === 0)).toBe(true); expect(plan.transitions.every((t) => t.critical === null)).toBe(true); }); }); describe('C: Die Warnung zur Hoechstfreigabezeit nennt jede betroffene Signalgruppe', () => { /** Standardvorlage mit derselben Vorgabe an mehreren Gruppen der ersten Phase. */ function mitVorgabe(sekunden: number, gruppen: readonly string[]): Project { const basis = createStandardIntersectionProject('Gleichstand', DATUM); return { ...basis, signalGroups: basis.signalGroups.map((g) => gruppen.includes(g.name) ? { ...g, maxGreenOverride: sekunden } : g, ), }; } const meldung = (project: Project): string => { const notiz = buildSignalPlan(project).notes.find( (n) => n.code === 'hoechstfreigabezeit-unter-mindestfreigabezeit', ); expect(notiz, 'Warnung zur Hoechstfreigabezeit').toBeDefined(); return notiz!.message; }; it('nennt beide Gruppen, wo beide dieselbe wirkungslose Vorgabe tragen', () => { const text = meldung(mitVorgabe(3, ['K1', 'K2'])); expect(text, 'die erste Gruppe').toContain('"K1"'); expect(text, 'die zweite ebenso').toContain('"K2"'); expect(text, 'im Plural').toContain('Für die Signalgruppen'); }); it('bleibt bei einer Gruppe im Singular', () => { // Gegenprobe: Der Wortlaut des Regelfalls darf sich nicht geaendert haben. const text = meldung(mitVorgabe(3, ['K1'])); expect(text).toContain('Für die Signalgruppe "K1" ist'); expect(text).not.toContain('Signalgruppen'); }); it('sagt dasselbe, wenn die Gruppen der Phase umgekehrt zugeordnet sind', () => { const basis = mitVorgabe(3, ['K1', 'K2']); const umgekehrt: Project = { ...basis, phases: basis.phases.map((p) => ({ ...p, signalGroupIds: [...p.signalGroupIds].reverse(), })), }; // Bis 5.23.0 wechselte hier der genannte Name, bei unveraendertem Plan. expect(meldung(umgekehrt)).toBe(meldung(basis)); }); }); describe('D: Die zusaetzliche Sekunde bei gleichgestellten Phasen steht in der Unterlage', () => { /** Zwei in allem gleiche Richtungen, je 7 s Uebergang. */ const zweiGleiche = (umlauf: number, gewicht = 0) => distributeGreenTimes( umlauf, [ { id: 'a', weight: gewicht, minGreen: 5 }, { id: 'b', weight: gewicht, minGreen: 5 }, ], [7, 7], ); const restsekunde = (umlauf: number, gewicht = 0) => zweiGleiche(umlauf, gewicht).notes.find((n) => n.code === 'freigabezeit-restsekunde'); it('meldet sich, wo die verfuegbare Zeit nicht aufgeht', () => { const ergebnis = zweiGleiche(99); expect( ergebnis.phases.map((p) => p.green), 'eine Sekunde Unterschied', ).toEqual([43, 42]); const notiz = restsekunde(99); expect(notiz, 'die Meldung zur Restsekunde').toBeDefined(); expect(notiz!.severity).toBe('hinweis'); expect(notiz!.message).toContain('85 s lässt sich nicht ganzzahlig auf 2 Phasen aufteilen'); expect(notiz!.message, 'wer sie bekommt').toContain( 'Je eine zusätzliche Sekunde geht an Phase 1', ); expect(notiz!.message, 'und dass es dafuer keinen Grund gibt').toContain( 'Ein sachlicher Grund für diese Auswahl besteht nicht', ); }); it('meldet sich auch bei erfassten, gleich grossen Verkehrsstaerken', () => { // Der Fall, der bis 5.23.0 GAR KEINE Meldung trug: Zwei Phasen mit // demselben Saettigungsgrad, 43 s und 42 s im Plan, kein Wort dazu. expect(zweiGleiche(99, 0.4).phases.map((p) => p.green)).toEqual([43, 42]); expect(restsekunde(99, 0.4), 'auch hier').toBeDefined(); }); it('schweigt, wo die Zeit aufgeht', () => { expect(zweiGleiche(100).phases.map((p) => p.green)).toEqual([43, 43]); expect(restsekunde(100), 'ohne Rest kein Hinweis').toBeUndefined(); }); it('schweigt, wo der Unterschied begruendet ist', () => { /* * Die wichtigste Gegenprobe: Verschiedene Verkehrsstaerken ergeben * verschiedene Freigabezeiten, und daran ist nichts zu erklaeren. Eine * Meldung hier waere Laerm. */ const ergebnis = distributeGreenTimes( 99, [ { id: 'a', weight: 0.6, minGreen: 5 }, { id: 'b', weight: 0.2, minGreen: 5 }, ], [7, 7], ); expect(ergebnis.notes.map((n) => n.code)).not.toContain('freigabezeit-restsekunde'); }); }); /* * E: Das Freigabefenster der koordinierten Hauptrichtung (plan/koordinierung.ts). * * DIE FUENFTE STELLE, behoben mit Fassung 5.43.0. Der Kopf * dieser Datei nannte vier; eine hatte die Suche vom 10.09.2026 nicht gesehen, * weil die Koordinierungskette erst mit 5.35.0 dazugekommen ist. * * Eine Signalgruppe kann in getrennten Phasen freigegeben sein. Fuer die Gruene * Welle traegt das laengste Fenster - durch das kuerzere passt der Pulk nicht -, * und `fenster` suchte es mit `g.duration > laengstes.duration`. Sind zwei * Fenster gleich lang, gewinnt damit das, das in `greens` zuerst steht, also die * Phasenreihenfolge. Das ist genau die Gestalt der vier Faelle darueber. * * ES GIBT HIER EINE FEINERE FACHLICHE AUSKUNFT, und der Grundsatz verlangt, zuerst * nach ihr zu suchen: WELCHES DER BEIDEN FENSTER EIN BAND ERGIBT. Am * Knotenpunkt dieses Abschnitts ergibt das eine 9 s Bandbreite, das andere 0 s * und den Fehler "keine durchgehende Bandbreite" - fuer denselben Plan. Die * Wahl wird deshalb am Band getroffen; die stabile Ordnung greift erst bei * gleicher Bandbreite, und dass SIE nichts bedeutet, steht im Quelltext daneben. * * BERICHTIGT (Fassung 5.43.0): Hier stand "ES GIBT HIER KEINE FEINERE FACHLICHE * AUSKUNFT ... die Bandbreite bleibt dieselbe", und die beiden Groessen waren * vertauscht. Die Bandbreite haengt an der Wahl, die Versatzzeitempfehlungen * haengen nicht daran: Sie entstehen in `coordination.ts` allein aus Abstand * und Progressionsgeschwindigkeit, die Bandbreite dagegen als Schnittmenge * aller Fenster - und die der Nachbarn sind Eingabedaten, die sich nicht * mitverschieben. Der Fall darunter, der das belegt, bewachte deshalb genau die * Groesse, die von der Wahl unberuehrt ist. */ describe('E: Das Freigabefenster der Hauptrichtung haengt nicht an der Fensterreihenfolge', () => { /** * Vier Phasen, die Hauptrichtung in der ersten und der dritten - zwei * getrennte Fenster gleicher Laenge. * * `nachbar` legt fest, ab wann der Nachbar in 200 m freigibt, und damit, * WELCHES der beiden Fenster ein Band traegt: bei 20 s das im Umlauf fruehere, * bei 50 s das spaetere. Beides wird geprueft - sonst waere "das mit dem Band" * von "immer das fruehere" nicht zu unterscheiden. * * `gegenrichtung` haengt der Kette die Rueckrichtung an (Signalgruppe B, frei * in der zweiten und vierten Phase, ebenfalls zwei gleich lange Fenster). */ function zweiFenster( nachbar = 20, gegenrichtung: { gegenBeginn: number; gegenDauer: number } | null = null, ): Project { const basis = createEmptyProject('Zwei Fenster', DATUM, 'knotenpunkt'); const a = createSignalGroup({ name: 'A', mode: 'kfz' as const, vZul: 50, index: 0 }); const b = createSignalGroup({ name: 'B', mode: 'kfz' as const, vZul: 50, index: 1 }); const phasen = [ createPhase('P1', [a.id]), createPhase('P2', [b.id]), createPhase('P3', [a.id]), createPhase('P4', [b.id]), ]; const beziehung = (von: string, nach: string) => ({ ...createConflict(von, nach), clearingDistance: 20, enteringDistance: 0, }); let projekt: Project = { ...basis, signalGroups: [a, b], phases: phasen, conflicts: [beziehung(a.id, b.id), beziehung(b.id, a.id)], program: { ...basis.program, method: 'manuell', manualCycleTime: 60, phaseOrder: phasen.map((p) => p.id), }, }; projekt = actions.setzeKoordinierung(projekt, { hauptrichtung: a.id, ...(gegenrichtung === null ? {} : { gegenrichtung: b.id }), progressionsgeschwindigkeit: 36, platz: 0, }); projekt = actions.koordinierungsknotenAnlegen(projekt, 'West'); const west = projekt.koordinierung!.nachbarn[0]!; return actions.koordinierungsknotenAendern(projekt, west.id, { abstand: 200, freigabeBeginn: nachbar, freigabeDauer: 12, ...(gegenrichtung ?? {}), }); } /** Nur ein Fenster einer Signalgruppe im Plan lassen - fuer die Messung. */ function nurFenster(plan: SignalPlan, groupId: string, index: number): SignalPlan { return { ...plan, groups: plan.groups.map((g) => g.groupId === groupId ? { ...g, greens: g.greens.slice(index, index + 1) } : g, ), }; } /** Das gewaehlte Freigabefenster dieses Knotenpunkts. */ function eigenes(projekt: Project, plan: SignalPlan) { const lage = koordinierungslage(projekt, plan); const glied = lage.kette.find((k) => k.eigener); return { freigabe: glied?.freigabe, gegenrichtung: glied?.gegenrichtung, bandbreite: lage.ergebnis?.bandwidth, rueckbreite: lage.ergebnis?.rueckrichtung?.breite, meldungen: lage.ergebnis?.notes.map((n) => n.code) ?? [], versatz: lage.ergebnis?.intersections, }; } /** Derselbe Plan mit umgekehrter Reihenfolge der Freigabefenster. */ function mitUmgekehrtenFenstern(plan: SignalPlan, groupId: string): SignalPlan { return { ...plan, groups: plan.groups.map((g) => g.groupId === groupId ? { ...g, greens: [...g.greens].reverse() } : g, ), }; } it('baut wirklich zwei gleich lange Fenster', () => { // Vorbedingung des Falls darunter, und zugleich der Beleg, dass die Lage // aus gewoehnlicher Eingabe entsteht und nicht von Hand gebaut ist. const projekt = zweiFenster(); const gruppe = buildSignalPlan(projekt).groups.find( (g) => g.groupId === projekt.koordinierung!.hauptrichtung, ); expect(gruppe?.greens.map((g) => g.duration)).toEqual([9, 9]); expect(gruppe?.greens.map((g) => g.start)).toEqual([0, 30]); }); it('die beiden Fenster sind fuer das Band nicht gleichwertig', () => { /* * DER BELEG, DASS ES EINE FEINERE AUSKUNFT GIBT, und die Messung, auf die * sich der Kommentar in koordinierung.ts beruft. Ohne diesen Fall waere die * Begruendung der Wahl wieder eine Behauptung - genau der Befund, aus dem * diese Berichtigung entstanden ist. */ const projekt = zweiFenster(); const haupt = projekt.koordinierung!.hauptrichtung!; const plan = buildSignalPlan(projekt); const mitErstem = eigenes(projekt, nurFenster(plan, haupt, 0)); const mitZweitem = eigenes(projekt, nurFenster(plan, haupt, 1)); expect(mitErstem.bandbreite, 'das Fenster ab 0 s traegt ein Band').toBe(9); expect(mitZweitem.bandbreite, 'das Fenster ab 30 s traegt keines').toBe(0); expect(mitZweitem.meldungen, 'und meldet dafuer einen Fehler').toContain('keine-bandbreite'); /* * Die andere Haelfte der berichtigten Begruendung: Die Versatzzeiten * beruehrt die Wahl NICHT. Sie entstehen aus Abstand und * Progressionsgeschwindigkeit. Dieser Fall haelt fest, warum die Wahl am * Band zu treffen ist und nicht an ihnen. */ expect(mitZweitem.versatz, 'die Versatzzeiten haengen nicht am Fenster').toEqual( mitErstem.versatz, ); }); it('waehlt das Fenster mit dem Band, gleich wie die beiden im Plan stehen', () => { const projekt = zweiFenster(); const haupt = projekt.koordinierung!.hauptrichtung!; const plan = buildSignalPlan(projekt); for (const [wie, p] of [ ['wie gebaut', plan], ['umgekehrt', mitUmgekehrtenFenstern(plan, haupt)], ] as const) { const gewaehlt = eigenes(projekt, p); expect(gewaehlt.freigabe, wie).toEqual({ start: 0, duration: 9 }); expect(gewaehlt.bandbreite, wie).toBe(9); expect(gewaehlt.meldungen, wie).not.toContain('keine-bandbreite'); } }); it('nimmt dafuer auch das spaetere Fenster, wenn dieses das Band traegt', () => { /* * DIE GEGENPROBE ZUR STABILEN ORDNUNG: Gibt der Nachbar erst ab 50 s frei, * traegt das Fenster ab 30 s das Band und das ab 0 s keines. Ohne diesen * Fall waere "das Fenster mit dem Band" von "immer das im Umlauf fruehere" * nicht zu unterscheiden - und genau diese Verwechslung war der Stand vor * der Berichtigung. */ const projekt = zweiFenster(50); const haupt = projekt.koordinierung!.hauptrichtung!; const plan = buildSignalPlan(projekt); expect(eigenes(projekt, nurFenster(plan, haupt, 0)).bandbreite, 'Vorbedingung').toBe(0); for (const p of [plan, mitUmgekehrtenFenstern(plan, haupt)]) { const gewaehlt = eigenes(projekt, p); expect(gewaehlt.freigabe).toEqual({ start: 30, duration: 9 }); expect(gewaehlt.bandbreite).toBe(9); } }); it('waehlt das Fenster der Gegenrichtung an deren eigenem Band', () => { /* * DIESELBE STELLE, ZWEITE RICHTUNG: `fenster` hat die Gegenrichtung mit * demselben Vergleich gesucht, also mit demselben Gleichstand. Hier traegt * das im Umlauf SPAETERE der beiden Fenster das Rueckband - waehlte die * Reihenfolge, entstuende die Warnung "kein durchgehendes Band in der * Gegenrichtung" fuer einen Plan, der eines hat. */ const projekt = zweiFenster(20, { gegenBeginn: 26, gegenDauer: 12 }); const gegen = projekt.koordinierung!.gegenrichtung!; const plan = buildSignalPlan(projekt); expect( plan.groups.find((g) => g.groupId === gegen)?.greens, 'Vorbedingung: zwei gleich lange Fenster der Gegenrichtung', ).toEqual([ { start: 15, duration: 9 }, { start: 45, duration: 9 }, ]); const gewaehlt = eigenes(projekt, plan); expect(gewaehlt.gegenrichtung).toEqual({ start: 45, duration: 9 }); expect(gewaehlt.rueckbreite).toBe(8); expect(gewaehlt.meldungen).not.toContain('keine-bandbreite-gegenrichtung'); // Und die Hinrichtung bleibt davon unberuehrt. expect(gewaehlt.freigabe).toEqual({ start: 0, duration: 9 }); }); it('nimmt weiterhin das laengere Fenster, wo es eines gibt', () => { /* * Die Gegenprobe: Nicht das fruehere gewinnt, sondern das laengere. Ohne * diesen Fall waere der vorige auch mit "immer das erste" gruen. */ const projekt = zweiFenster(); const haupt = projekt.koordinierung!.hauptrichtung!; const plan = buildSignalPlan(projekt); const verlaengert: SignalPlan = { ...plan, groups: plan.groups.map((g) => g.groupId === haupt ? { ...g, greens: [g.greens[0]!, { ...g.greens[1]!, duration: 12 }] } : g, ), }; expect(koordinierungslage(projekt, verlaengert).kette.find((k) => k.eigener)?.freigabe).toEqual( { start: 30, duration: 12, }, ); // Auch wenn es vorne steht. expect( koordinierungslage(projekt, mitUmgekehrtenFenstern(verlaengert, haupt)).kette.find( (k) => k.eigener, )?.freigabe, ).toEqual({ start: 30, duration: 12 }); }); it('sagt der Ansicht, dass es mehrere Fenster sind', () => { /* * Eine Auswahl, die niemand getroffen hat, gehoert ausgesprochen. Gemeldet * wird, DASS es mehrere sind - nicht, welches gilt; der Hinweistext dazu * steht in ui/views/koordinierungView.ts und nennt das gewaehlte Fenster * bisher nicht (offener Punkt seit Fassung 5.43.0). */ const projekt = zweiFenster(); expect(koordinierungslage(projekt, buildSignalPlan(projekt)).mehrereFreigabefenster).toBe(true); }); });