import { inflateSync } from 'node:zlib'; import { describe, expect, it } from 'vitest'; import { buildProjectPdf, DEFAULT_PDF_OPTIONS } from '@/services/export/pdf'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { validateProject } from '@/domain/validation'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { ANLAGENART_GRENZEN } from '@/domain/rilsa/constants'; import { PRUEFSTAND_LABELS } from '@/domain/rilsa/quellen'; import { toWinAnsi } from '@/render/pdfSurface'; import type { AnlagenartGrenzen } from '@/domain/rilsa/constants'; import type { Project } from '@/domain/model/project'; import * as fmt from '@/ui/format'; /* * BEFUND 7 (Fassung 5.11.0): Der Abschnitt "Schranken der Anlagenart" las seine * Grenzen aus der Konstanten ANLAGENART_GRENZEN statt aus `plan.grenzen`. * * Was der Anwender sonst erlebt: Er traegt in der Vorgabenverwaltung eine * engere Umlaufzeit ein (45 bis 100 s). Der Pruefbericht misst daran * (engine.ts: `plan.grenzen.cycleTime`), die Aufstellung "Abweichungen von den * Regelwerten" fuehrt die Vorgabe als Abweichung - und zwei Seiten weiter * druckt die Spalte "Angesetzt" der Schrankentabelle "30 s bis 120 s" mit dem * Pruefstand "gesichert". Ein Pruefer bekommt zwei Zahlenwerke in EINER * Anordnungsunterlage und kann sie nicht zur Deckung bringen; zugleich verweist * quellen.ts im selben Dokument ausdruecklich auf diesen Abschnitt als die * Stelle, die die WIRKSAME Schranke nennt. * * Der Satz unter der Tabelle bleibt beim Regelwerkswert: Die 120 s der RiLSA * sind als Sicherheitsgrenze begruendet, eine selbst gesetzte Obergrenze traegt * diese Begruendung nicht (Grundsatz: selbstgesetzte Schwellen tragen nie * "gesichert"). */ const STICHTAG = new Date('2026-01-01T12:00:00Z'); /** Sichtbaren Text aus dem PDF lesen; die Inhaltsstroeme sind komprimiert. */ function pdfText(bytes: Uint8Array): string { const roh = new TextDecoder('latin1').decode(bytes); const teile: string[] = []; const suche = /stream\r?\n/g; let treffer: RegExpExecArray | null; while ((treffer = suche.exec(roh)) !== null) { const start = treffer.index + treffer[0].length; const ende = roh.indexOf('endstream', start); if (ende < 0) continue; let inhalt: string; try { inhalt = inflateSync(Buffer.from(roh.slice(start, ende), 'latin1')).toString('latin1'); } catch { continue; } for (const t of inhalt.matchAll(/\(((?:[^()\\]|\\.)*)\)\s*Tj/g)) { teile.push(t[1]!.replace(/\\([()\\])/g, '$1')); } } return teile.join(' ').replace(/\s+/g, ' '); } function projekt(vorgaben: Project['settings']['rilsa']): Project { const basis = createStandardIntersectionProject('Hauptstrasse / Bahnhofstrasse', STICHTAG); return { ...basis, settings: { ...basis.settings, rilsa: { ...basis.settings.rilsa, ...vorgaben } }, }; } function ausdruck(vorgaben: Project['settings']['rilsa']): { text: string; grenzen: ReturnType['grenzen']; } { const project = projekt(vorgaben); const plan = buildSignalPlan(project); const bericht = validateProject(project, plan, STICHTAG); return { text: pdfText(buildProjectPdf(project, plan, bericht, DEFAULT_PDF_OPTIONS, STICHTAG)), grenzen: plan.grenzen, }; } const gedruckt = (text: string): string => toWinAnsi(text).replace(/\s+/g, ' '); const REGELWERK = ANLAGENART_GRENZEN.knotenpunkt; const spanne = (g: AnlagenartGrenzen, welche: 'zulaessig' | 'regelbereich'): string => welche === 'zulaessig' ? `${fmt.seconds(g.cycleTime.min)} bis ${fmt.seconds(g.cycleTime.max)}` : `${fmt.seconds(g.cycleTime.recommendedMin)} bis ${fmt.seconds(g.cycleTime.recommendedMax)}`; /* * NACHTRAG ZU BEFUND 7: Der angesetzte Wert allein genuegt nicht. Grundlage * und Pruefstand derselben Zeile kommen aus `grenzen.herkunft`, und das ist * unveraendert die Herkunft der Anlagenart - neben einer eingeengten Umlaufzeit * stand damit "RiLSA 2015 ... (30 bis 120 s)" und der Pruefstand "gesichert". * Geprueft wird deshalb die GANZE Zeile: Wo der angesetzte Wert vom Regelwert * abweicht, traegt der Zeilenkopf den Vermerk, und erst danach folgen Wert, * Grundlage und Pruefstand. */ function zeile( kopf: string, grenzen: AnlagenartGrenzen, welche: 'zulaessig' | 'regelbereich', ): string { const angesetzt = spanne(grenzen, welche); const regelwert = spanne(REGELWERK, welche); const vermerk = angesetzt === regelwert ? '' : ` (eigene Vorgabe, abweichend vom Regelwert ${regelwert})`; const h = welche === 'zulaessig' ? REGELWERK.herkunft.umlaufzeit : REGELWERK.herkunft.regelbereich; return gedruckt(`${kopf}${vermerk} ${angesetzt} ${h.text} ${PRUEFSTAND_LABELS[h.pruefstand]}`); } describe('Befund 7 - Spalte "Angesetzt" der Schrankentabelle', () => { it('druckt die Umlaufzeitschranken dieses Plans, nicht die der Anlagenart', () => { const { text, grenzen } = ausdruck({ cycleTimeMin: 45, cycleTimeMax: 100 }); expect(grenzen.cycleTime.min, 'Vorbedingung: die Vorgabe wirkt').toBe(45); expect(grenzen.cycleTime.max, 'Vorbedingung: die Vorgabe wirkt').toBe(100); const angesetzt = spanne(grenzen, 'zulaessig'); const regelwerk = spanne(REGELWERK, 'zulaessig'); expect(text, `die Spalte "Angesetzt" nennt ${angesetzt}`).toContain(gedruckt(angesetzt)); expect(text, `nicht die Werte der Anlagenart (${regelwerk})`).not.toContain( gedruckt(`Umlaufzeit, zulässig ${regelwerk}`), ); // Die eingeengte Zeile steht nicht ohne Kennzeichnung neben "gesichert". expect(text, 'Kennwert samt Vermerk, dann Wert, Grundlage und Pruefstand').toContain( zeile('Umlaufzeit, zulässig', grenzen, 'zulaessig'), ); }); it('druckt den empfohlenen Bereich dieses Plans, auch wenn nur die Obergrenze vorgegeben ist', () => { // Schon eine eingetragene Obergrenze allein verschiebt den Regelbereich // ueber clamp() (signalPlan.ts): gemessen 60 bis 60 s bei gedruckten // 60 bis 90 s. const { text, grenzen } = ausdruck({ cycleTimeMax: 60 }); expect(grenzen.cycleTime.recommendedMax, 'Vorbedingung: der Regelbereich wird eingeengt').toBe( 60, ); expect(text).toContain(gedruckt('Umlaufzeit, empfohlener Bereich (eigene Vorgabe')); // Beide Zeilen sind hier verschoben: die zulaessige Obergrenze durch die // Vorgabe selbst, der Regelbereich durch clamp() - und beide tragen den // Vermerk vor Wert, Grundlage und Pruefstand. expect(text).toContain(zeile('Umlaufzeit, empfohlener Bereich', grenzen, 'regelbereich')); expect(text).toContain(zeile('Umlaufzeit, zulässig', grenzen, 'zulaessig')); }); it('vermerkt keine Abweichung, wo keine ist, und sagt es im Einleitungsabsatz', () => { const { text, grenzen } = ausdruck({}); expect(grenzen.cycleTime, 'Vorbedingung: ohne Vorgabe gelten die Werte der Anlagenart').toEqual( REGELWERK.cycleTime, ); expect(text, 'ohne eigene Vorgabe steht kein Vermerk in der Tabelle').not.toContain( gedruckt('(eigene Vorgabe, abweichend vom Regelwert'), ); expect(text).toContain(zeile('Umlaufzeit, zulässig', grenzen, 'zulaessig')); expect( text, 'der Einleitungsabsatz sagt, wofuer Grundlage und Pruefstand bei eigener Vorgabe stehen', ).toContain( gedruckt( 'Steht unter "Angesetzt" eine eigene Vorgabe aus der Vorgabenverwaltung, ist das an der ' + 'Zeile vermerkt: Grundlage und Prüfstand nennen dann weiterhin den Regelwert, von dem ' + 'die Vorgabe abweicht, und nicht die Herkunft des angesetzten Wertes.', ), ); }); it('nennt die Sicherheitsgrenze der RiLSA weiter mit ihrem Regelwert und die eigene Vorgabe daneben', () => { const { text } = ausdruck({ cycleTimeMax: 100 }); const regelwert = fmt.seconds(ANLAGENART_GRENZEN.knotenpunkt.cycleTime.max); expect( text, 'die Begruendung "Sicherheitsgrenze" gehoert zum Regelwerkswert, nicht zur eigenen Vorgabe', ).toContain( gedruckt(`Die Obergrenze der Umlaufzeit von ${regelwert} ist eine Sicherheitsgrenze`), ); expect(text, 'die engere eigene Vorgabe wird als solche benannt').toContain( gedruckt(`Für diesen Plan ist eine engere Obergrenze von ${fmt.seconds(100)} vorgegeben`), ); }); it('laesst den Satz ohne eigene Vorgabe unveraendert', () => { const { text } = ausdruck({}); expect(text).toContain( gedruckt( `Die Obergrenze der Umlaufzeit von ${fmt.seconds(ANLAGENART_GRENZEN.knotenpunkt.cycleTime.max)} ` + 'ist eine Sicherheitsgrenze', ), ); expect(text).not.toContain(gedruckt('ist eine engere Obergrenze von')); }); });