import { describe, expect, it } from 'vitest'; import { createConflict, createEmptyProject, createSignalGroup } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; import { parseProject } from '@/domain/model/schema'; import { buildSignalPlan, intergreenKey } from '@/domain/plan/signalPlan'; /* * Befund 61, behoben in Fassung 5.10.0 - zusammengesetzte Schluessel aus * Signalgruppenkennungen waren mehrdeutig. * * `intergreenKey` verband zwei Kennungen mit "|", `parseConflicts` mit "->". * Beim Einlesen wird eine Kennung nur auf leer und auf Doppelvergabe geprueft, * nicht auf ein Zeichenrepertoire - sie kommt unveraendert aus der Datei. Fuer * die Kennungen "a|b", "c", "a" und "b|c" lieferte intergreenKey("a|b","c") * denselben Schluessel wie intergreenKey("a","b|c"): Die Karte behielt nur den * zuletzt geschriebenen Eintrag, und die andere Beziehung erhielt die fremde * Zwischenzeit. Ist sie kuerzer, wird der Phasenuebergang zu kurz bemessen - * die gefaehrliche Richtung, und die Gegenprobe faellt aus, weil * `checkIntergreenGaps` denselben Schluessel nachschlaegt. * * Mit "->" in einer Kennung traf es schon das Einlesen: Die zweite, sachlich * andere Beziehung wurde mit "doppelt erfasst" verworfen. * * Aus der Anwendung heraus entsteht das nicht - `createId` vergibt nur * Kleinbuchstaben, Ziffern und "-" -, ueber eine von Hand geschriebene * Projektdatei sehr wohl; die ist im Projekt ausdruecklich als Einstiegspunkt * anerkannt. * * GEGEN DEN ALTSTAND schlagen die ersten drei Faelle fehl. Die letzten beiden * halten die Grenzen fest und bestehen auch dort. * * DIE QUELLE IST SEIT DER FASSUNG 5.12.0 GESCHLOSSEN: Das Einlesen prueft * die Kennungen jetzt gegen ein Zeichenrepertoire (`ZULAESSIGE_KENNUNG` in * src/domain/model/schema.ts) und ersetzt, was nicht hineinpasst - ein "|" * kann also nicht mehr aus einer Datei kommen. Der erste Fall baut sein * Projekt deshalb unmittelbar auf statt ueber `parseProject`, wie es die * Nachbarwache `schluesselAusKennungen.test.ts` aus demselben Grund tut: * Er bewacht die SCHLUESSELBILDUNG - die zweite Reihe, und die einzige, die * noch traegt, wenn das Repertoire eines Tages weiter gefasst wird. Die * Faelle darunter gehen weiter ueber das Einlesen; sie messen dessen eigene * Zusagen und bestehen mit den ersetzten Kennungen unveraendert, weil die * Verweise mitziehen. */ /** Vier Kennungen, deren zusammengesetzte Schluessel bisher kollidierten. */ const GRUPPEN_MIT_STRICH = [ { id: 'a|b', name: 'K1', mode: 'kfz' }, { id: 'c', name: 'K2', mode: 'kfz' }, { id: 'a', name: 'K3', mode: 'kfz' }, { id: 'b|c', name: 'K4', mode: 'kfz' }, ]; /** Dieselben vier Gruppen, aber am Einlesen vorbei zusammengesetzt. */ function projektMitStrich(): Project { const leer = createEmptyProject('Kennungen mit Strich', new Date('2026-01-01T00:00:00Z')); return { ...leer, signalGroups: GRUPPEN_MIT_STRICH.map((g, i) => ({ ...createSignalGroup({ name: g.name, mode: 'kfz', index: i }), id: g.id, })), conflicts: [ { ...createConflict('a|b', 'c'), id: 'cf-1', clearingDistance: 40 }, { ...createConflict('a', 'b|c'), id: 'cf-2', clearingDistance: 5 }, ], }; } describe('Befund 61 - Kennungen mit Trennzeichen', () => { it('haelt zwei Beziehungen mit kollidierendem Schluessel auseinander', () => { const project = projektMitStrich(); expect(project.conflicts).toHaveLength(2); const plan = buildSignalPlan(project); const lang = plan.intergreens.get(intergreenKey('a|b', 'c')); const kurz = plan.intergreens.get(intergreenKey('a', 'b|c')); expect(lang?.conflictId).toBe('cf-1'); expect(kurz?.conflictId).toBe('cf-2'); // Der lange Raeumweg ergibt die laengere Zwischenzeit; erbte eine // Beziehung die fremde, waeren beide Werte gleich. expect((lang?.value ?? 0) > (kurz?.value ?? 0)).toBe(true); }); it('vergibt fuer jede Beziehung einen eigenen Schluessel', () => { expect(intergreenKey('a|b', 'c')).not.toBe(intergreenKey('a', 'b|c')); }); it('behaelt beim Einlesen eine Beziehung mit "->" in der Kennung', () => { const { project, issues } = parseProject({ signalGroups: [ { id: 'x->y', name: 'K1', mode: 'kfz' }, { id: 'z', name: 'K2', mode: 'kfz' }, { id: 'x', name: 'K3', mode: 'kfz' }, { id: 'y->z', name: 'K4', mode: 'kfz' }, ], conflicts: [ { id: 'cf-1', fromId: 'x->y', toId: 'z', clearingDistance: 20, enteringDistance: 0 }, { id: 'cf-2', fromId: 'x', toId: 'y->z', clearingDistance: 20, enteringDistance: 0 }, ], }); expect(project.conflicts.map((c) => c.id)).toEqual(['cf-1', 'cf-2']); expect(issues.filter((i) => i.path.startsWith('conflicts'))).toEqual([]); }); it('meldet eine wirklich doppelt erfasste Beziehung weiterhin', () => { const { project, issues } = parseProject({ signalGroups: GRUPPEN_MIT_STRICH, conflicts: [ { id: 'cf-1', fromId: 'a|b', toId: 'c', clearingDistance: 20, enteringDistance: 0 }, { id: 'cf-2', fromId: 'a|b', toId: 'c', clearingDistance: 40, enteringDistance: 0 }, ], }); expect(project.conflicts).toHaveLength(1); expect( issues.some((i) => i.path === 'conflicts[1]' && i.message.includes('doppelt erfasst')), ).toBe(true); }); it('laesst die Schreibweise gewoehnlicher Kennungen unveraendert', () => { // Kennungen aus createId enthalten weder "|" noch das Fluchtzeichen; der // Schluessel liest sich weiterhin als "fromId|toId". expect(intergreenKey('sg-1-abc', 'sg-2-def')).toBe('sg-1-abc|sg-2-def'); }); });