import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { parseProject } from '@/domain/model/schema'; import { createStandardIntersectionProject } from '@/domain/model/factory'; /* * Fassung 5.10.0 - die Fahrstreifenzahl darf gebrochen sein. * * `num()` prueft `lanes` nur gegen 1 bis 8. Eine Projektdatei mit * "lanes": 2.5 wurde ohne ein Wort uebernommen, ging mit 2,5 in * `saturationFlow` ein und stand so in Tabelle und Ausdruck. Befund 55 hat den * Wert zuletzt nur noch in deutscher Schreibweise gedruckt - eine unzulaessige * Angabe in gepflegter Form. * * Ein Fahrstreifen ist eine ANZAHL. Ein halber Fahrstreifen ist keine Planung, * sondern ein verdorbener Eintrag, und die Grenze zieht dieselbe Stelle, die * schon 1 bis 8 zieht: das Einlesen. * * SO WEIT REICHT ES, NICHT WEITER. Fuer jede Projektdatei ist der Fall damit * geschlossen. Ueber die Oberflaeche entsteht ein gebrochener Wert weiterhin - * `numberCell` in src/ui/inputs.ts begrenzt nur `min` und `max` (dort bewusst * so: `step` ist die Sprungweite der Pfeiltasten, kein Raster), eine getippte * 2,5 geht also unbeanstandet in den Speicher, in `saturationFlow`, in die * Tabelle und in den Ausdruck. Zu schliessen ist das in `checkSignalGroups` in * src/domain/validation/engine.ts, die nicht auf der Liste dieser Gruppe steht. * * DIE RICHTUNG IST ABWAERTS. Die Fahrstreifenzahl geht ueber * `saturationFlow` in die Kapazitaet ein; aufrunden gaebe Kapazitaet, die * nicht da ist, und liesse den Plan besser aussehen, als er ist. Abrunden * nimmt Kapazitaet weg - die sichere Richtung -, und gemeldet wird es * ohnehin. * * GEGEN DEN ALTSTAND (5.9.0) schlagen fuenf Faelle fehl: die vier zum Einlesen * und der zum Vorbehalt im Kommentar an der Zufahrt. Die uebrigen vier - drei * Gegenproben und der Blick in engine.ts - bestehen dort ebenso. */ const DATUM = new Date('2026-01-01T00:00:00Z'); /** Das Projekt als schlichte Daten - so, wie es aus einer Datei kaeme. */ function alsDatei(): Record { return JSON.parse( JSON.stringify(createStandardIntersectionProject('Fahrstreifen', DATUM)), ) as Record; } /** Dieselbe Datei, aber die erste Signalgruppe traegt die uebergebene Zahl. */ function mitGruppenwert(wert: unknown): ReturnType { const roh = alsDatei(); const gruppen = roh['signalGroups'] as Record[]; return parseProject({ ...roh, signalGroups: gruppen.map((g, index) => (index === 0 ? { ...g, lanes: wert } : g)), }); } /** Dieselbe Datei, aber die erste Zufahrt traegt die uebergebene Zahl. */ function mitZufahrtswert(wert: unknown): ReturnType { const roh = alsDatei(); const knoten = roh['intersection'] as Record; const arme = knoten['arms'] as Record[]; return parseProject({ ...roh, intersection: { ...knoten, arms: arme.map((a, index) => (index === 0 ? { ...a, lanes: wert } : a)), }, }); } describe('Fahrstreifenzahl der Signalgruppe', () => { it('bringt 2,5 auf eine ganze Zahl und meldet die Ersetzung', () => { const { project, issues, repaired } = mitGruppenwert(2.5); expect(project.signalGroups[0]?.lanes).toBe(2); const meldung = issues.find((i) => i.path === 'signalGroups[0].lanes'); expect(meldung, 'Meldung zur ersetzten Fahrstreifenzahl').toBeDefined(); expect(meldung?.severity).toBe('warnung'); expect(meldung?.message).toContain('Fahrstreifenzahl'); expect(meldung?.message).toContain('ganze Zahl'); // Die Datei ist verändert worden; der Anwender muss das beim Speichern // wissen. expect(repaired).toBe(true); }); it('rundet ab und nicht auf - 7,9 wird 7', () => { // Aufrunden gaebe einen Fahrstreifen mehr Kapazitaet, als die Zufahrt hat. // Das ist die unsichere Richtung, auch wenn 7,9 nach 8 aussieht. const { project } = mitGruppenwert(7.9); expect(project.signalGroups[0]?.lanes).toBe(7); }); it('nimmt die Zahl auch aus einer Zeichenkette und rundet sie ab', () => { // parseNumber liest "2,5" mit deutschem Komma; die Ganzzahlpruefung darf // hinter dieser Umwandlung stehen, nicht davor. const { project, issues } = mitGruppenwert('2,5'); expect(project.signalGroups[0]?.lanes).toBe(2); expect(issues.some((i) => i.path === 'signalGroups[0].lanes')).toBe(true); }); }); describe('Fahrstreifenzahl der Zufahrt', () => { it('bringt 3,5 auf eine ganze Zahl und meldet die Ersetzung', () => { const { project, issues } = mitZufahrtswert(3.5); expect(project.intersection.arms[0]?.lanes).toBe(3); const meldung = issues.find((i) => i.path === 'intersection.arms[0].lanes'); expect(meldung, 'Meldung zur ersetzten Fahrstreifenzahl').toBeDefined(); expect(meldung?.severity).toBe('warnung'); expect(meldung?.message).toContain('ganze Zahl'); }); }); describe('So weit reicht die Behebung', () => { /* * Der Kommentar an der Zufahrt las sich als abgeschlossener Fall. Er ist es * nur fuer den Einlesepfad: `numberCell` in src/ui/inputs.ts begrenzt eine * getippte Zahl allein gegen `min` und `max`, eine 2,5 aus der Oberflaeche * steht danach unbeanstandet im Speicher. Solange das so ist, sagt der * Kommentar es auch. */ const SCHEMA = readFileSync('src/domain/model/schema.ts', 'utf8'); const ENGINE = readFileSync('src/domain/validation/engine.ts', 'utf8'); it('nennt an der Zufahrt alle drei geschlossenen Wege', () => { /* * Frueher stand hier, der Kommentar muesse den OFFENEN Weg ueber die * Oberflaeche nennen: Das Fahrstreifenfeld der Zufahrt * (src/ui/views/projectView.ts) trug die Ganzzahlgrenze als einziges der * drei Felder nicht. Diese Luecke ist geschlossen; die Zusicherung haette * sonst einen Vorbehalt festgehalten, den es nicht mehr gibt. * * Was der Kommentar weiterhin sagen muss: dass die Grenze an jeder der * drei Stellen greift und dass der Pruefbericht sie nach wie vor nicht * prueft - der Fall darunter ist die Gegenprobe dazu. */ const zufahrt = SCHEMA.slice( SCHEMA.indexOf('Eine Anzahl, keine Messgroesse'), SCHEMA.indexOf('`intersection.arms[${index}].lanes`'), ); expect(zufahrt, 'Fundstelle des Zufahrtskommentars').not.toBe(''); expect(zufahrt).toContain('alle drei Wege'); expect(zufahrt).toContain('engine.ts'); }); it('haelt fest, dass der Pruefbericht die Fahrstreifenzahl noch nicht prueft', () => { // Die Gegenprobe zum Vorbehalt: Sobald eine Regel dazukommt, faellt dieser // Fall - und mit ihm der Satz, der den Weg als offen beschreibt. expect(ENGINE).not.toContain('lanes'); }); }); describe('Gegenproben - was sich nicht aendern darf', () => { it('laesst eine ganze Fahrstreifenzahl unberuehrt und schweigt dazu', () => { const { project, issues } = mitGruppenwert(3); expect(project.signalGroups[0]?.lanes).toBe(3); expect(issues.filter((i) => i.path === 'signalGroups[0].lanes')).toHaveLength(0); }); it('meldet einen Wert unter dem Mindestwert weiterhin nur einmal', () => { // 0,5 liegt unter 1. Die Begrenzung hebt auf 1 an - und 1 ist bereits // ganz. Zwei Meldungen zu einem Eintrag waeren eine zu viel. const { project, issues } = mitGruppenwert(0.5); expect(project.signalGroups[0]?.lanes).toBe(1); const meldungen = issues.filter((i) => i.path === 'signalGroups[0].lanes'); expect(meldungen).toHaveLength(1); expect(meldungen[0]?.message).toContain('Mindestwert'); }); it('meldet einen Wert ueber dem Hoechstwert weiterhin nur einmal', () => { const { project, issues } = mitGruppenwert(8.5); expect(project.signalGroups[0]?.lanes).toBe(8); const meldungen = issues.filter((i) => i.path === 'signalGroups[0].lanes'); expect(meldungen).toHaveLength(1); expect(meldungen[0]?.message).toContain('Höchstwert'); }); });