import { describe, expect, it } from 'vitest'; import { createEmptyProject, createStandardIntersectionProject } from '@/domain/model/factory'; import { loadProject } from '@/domain/model/migrate'; import { parseProject } from '@/domain/model/schema'; import { CURRENT_SCHEMA_VERSION } from '@/domain/model/project'; import { ANLAGENART_GRENZEN } from '@/domain/rilsa/constants'; /** * Die Anlagenart auf ihrem Weg durch Vorlage, Datei und Altprojekt. * * WOGEGEN DIESE TESTS STEHEN: Bis Schema 8 galten die Schranken des * Knotenpunkts fuer jede Anlage - vor allem der Raeumweg von hoechstens 500 m. * Bei einer einstreifigen Verkehrsfuehrung IST der Raeumweg die ganze * Engstelle; eine 800 m lange Fuehrung waere beim Einlesen stillschweigend auf * 500 m gekuerzt worden, und die daraus gerechnete Zwischenzeit haette danach * rund 30 s zu kurz dagestanden. * * Die Gegenrichtung ist genauso wichtig: Eine Datei ohne Angabe darf nicht * plotzlich die weiten Schranken der Arbeitsstelle bekommen. Fehlende Angabe * heisst "knotenpunkt", und zwar ohne Meldung - das Feld gab es damals nicht. */ const FIXED_DATE = new Date('2026-01-01T00:00:00Z'); /** Zwei Signalgruppen und eine Beziehung mit dem angegebenen Raeumweg. */ function dateiMit(anlagenart: unknown, raeumweg: number, schemaVersion = CURRENT_SCHEMA_VERSION) { return { schemaVersion, anlagenart, signalGroups: [ { id: 'a', name: 'K1', mode: 'kfz' }, { id: 'b', name: 'K2', mode: 'kfz' }, ], conflicts: [{ fromId: 'a', toId: 'b', clearingDistance: raeumweg, enteringDistance: 5 }], }; } describe('Modell und Vorlage', () => { it('fuehrt die Schemaversion 12', () => { // ANPASSUNG (Befund A2): Aufstieg auf 10 fuer die Fahrzeugart // 'strassenbahn' und das Konfliktfeld haltVorKnoten. ANPASSUNG (Befund C1): // Aufstieg auf 11 fuer das Signalgruppenfeld blindenzusatz (Freigabe fuer // die ganze Furt). ANPASSUNG (Fassung 5.5.0, Fragen E1/E2): Aufstieg auf 12 // fuer das Konfliktfeld engerRadius und den Ueberfahrzeit-Ansatz. // ANPASSUNG: Aufstieg auf 13 fuer den Lastzuganteil am Schwerverkehr // (TrafficDemand.lastzugAnteil, HBS 2015 Gl. 2-5). expect(CURRENT_SCHEMA_VERSION).toBe(19); }); it('legt ein leeres Projekt ohne Angabe als Knotenpunkt an', () => { expect(createEmptyProject('Leer', FIXED_DATE).anlagenart).toBe('knotenpunkt'); }); it('nimmt die Anlagenart als Vorgabe entgegen', () => { // Der Assistent legt Fussgaengerschutzanlagen und einstreifige Fuehrungen // an. Die Art gehoert in denselben Schritt wie das Projekt, sonst // durchlaeuft es einen Zustand mit den falschen Schranken. expect(createEmptyProject('Baustelle', FIXED_DATE, 'einstreifig').anlagenart).toBe( 'einstreifig', ); expect(createEmptyProject('Furt', FIXED_DATE, 'fussgaengerschutzanlage').anlagenart).toBe( 'fussgaengerschutzanlage', ); }); it('weist den Beispielknotenpunkt als Knotenpunkt aus', () => { expect(createStandardIntersectionProject('Vorlage', FIXED_DATE).anlagenart).toBe('knotenpunkt'); }); }); describe('Einlesen', () => { it('liest ein selbst erzeugtes Projekt mit seiner Art wieder ein', () => { const original = createEmptyProject('Baustelle B7', FIXED_DATE, 'einstreifig'); expect(parseProject(original, FIXED_DATE).project.anlagenart).toBe('einstreifig'); }); it('gibt einer Datei ohne Angabe "knotenpunkt", und zwar ohne Meldung', () => { // Eine Datei aus Schema 8 ist nicht fehlerhaft: Sie kannte das Feld nicht, // und etwas anderes als einen Knotenpunkt konnte sie gar nicht beschreiben. const { project, issues } = parseProject( { schemaVersion: 8, signalGroups: [{ id: 'a', name: 'K1', mode: 'kfz' }] }, FIXED_DATE, ); expect(project.anlagenart).toBe('knotenpunkt'); expect(issues.filter((i) => i.path === 'anlagenart')).toHaveLength(0); }); it('faellt bei unbekannter Art auf "knotenpunkt" zurueck und meldet das', () => { // Hier steht etwas, das jemand gemeint hat. Stillschweigend die engsten // Schranken zu setzen waere sicher, aber der Bearbeiter wuesste nicht, // warum seine Engstelle nicht rechnet. const { project, issues } = parseProject(dateiMit('kreisverkehr', 20), FIXED_DATE); expect(project.anlagenart).toBe('knotenpunkt'); const meldung = issues.find((i) => i.path === 'anlagenart'); expect(meldung).toBeDefined(); expect(meldung!.severity).toBe('warnung'); expect(meldung!.message).toContain('kreisverkehr'); }); }); describe('Raeumweg an der Anlagenart', () => { it('nimmt bei einstreifiger Fuehrung eine Engstelle ueber 500 m unveraendert an', () => { // DER FEHLER, UM DEN ES GEHT: Bis Schema 8 stand hier fest 500 m, und die // 800 m waeren beim Oeffnen der Datei verschwunden. const { project, issues } = parseProject(dateiMit('einstreifig', 800), FIXED_DATE); expect(project.conflicts[0]!.clearingDistance).toBe(800); expect(issues.filter((i) => i.path.includes('clearingDistance'))).toHaveLength(0); }); it('begrenzt den Raeumweg auf die Grenze der jeweiligen Art und sagt es', () => { const grenze = ANLAGENART_GRENZEN.einstreifig.maxClearingDistance; const { project, issues } = parseProject(dateiMit('einstreifig', grenze + 500), FIXED_DATE); expect(project.conflicts[0]!.clearingDistance).toBe(grenze); const meldung = issues.find((i) => i.path.includes('clearingDistance')); expect(meldung).toBeDefined(); // Ohne die Art in der Meldung liest sich die Zahl wie eine Programmgrenze. expect(meldung!.message).toContain('Einstreifige'); }); it('haelt am Knotenpunkt die bisherigen 500 m', () => { const { project, issues } = parseProject(dateiMit('knotenpunkt', 800), FIXED_DATE); expect(project.conflicts[0]!.clearingDistance).toBe( ANLAGENART_GRENZEN.knotenpunkt.maxClearingDistance, ); expect(issues.some((i) => i.path.includes('clearingDistance'))).toBe(true); }); it('gibt einer Datei ohne Angabe nicht die weiten Schranken der Arbeitsstelle', () => { // Die Gegenprobe: Der Rueckfall darf nicht die grosszuegigste Art sein. const { project } = parseProject(dateiMit(undefined, 800, 8), FIXED_DATE); expect(project.conflicts[0]!.clearingDistance).toBe(500); }); }); describe('Uebernahme von Altprojekten', () => { const legacyProject = { metadata: { version: '4.3.3' }, projektdaten: { projektBezeichnung: 'Alt', // Das Altformat fuehrte Baustellenfelder. Daraus wird NICHT auf eine // einstreifige Fuehrung geschlossen: Auch ein Knotenpunkt in einer // Baustelle wird angemietet, und die Fehlannahme haette seine // Umlaufzeitschranke still von 120 s auf 300 s angehoben. baustellenVon: true, mietkosten: { tagessatz: 40 }, }, signalgruppen: [ { id: 'SG-1', nummer: 'K1', typ: 'kfz' }, { id: 'SG-2', nummer: 'K2', typ: 'kfz' }, ], sperrmatrix: { matrix: [ [false, true], [true, false], ], }, zwischenzeiten: { parameter: { raeumweg: 24, einfahrweg: 9 } }, }; it('uebernimmt ein Altprojekt als Knotenpunkt', () => { const result = loadProject(legacyProject, FIXED_DATE); expect(result.migrated).toBe(true); expect(result.project.anlagenart).toBe('knotenpunkt'); }); it('sagt beim Oeffnen einer Datei aus Schema 8 nichts zur Anlagenart', () => { // Anders als beim Wegnachweis wird hier nichts behauptet, was unbekannt // waere - eine Meldung wuerde nur das Gewicht der uebrigen verwaessern. const result = loadProject(dateiMit(undefined, 24, 8), FIXED_DATE); expect(result.project.anlagenart).toBe('knotenpunkt'); expect(result.issues.some((i) => i.path === 'anlagenart')).toBe(false); }); it('warnt bei einer Datei aus einer neueren Schemaversion', () => { // Der Grund fuer den Aufstieg auf 9: Ein aelterer Programmstand soll eine // Arbeitsstelle mit 250 s Umlauf nicht stillschweigend auf 120 s stutzen. const result = loadProject( { schemaVersion: CURRENT_SCHEMA_VERSION + 1, signalGroups: [] }, FIXED_DATE, ); expect(result.issues.some((i) => i.path === 'schemaVersion')).toBe(true); }); });