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 { REGELBREITE } from '@/domain/geometrie/vermessung'; import type { Lageplan, Planlinie } from '@/domain/geometrie/lageplan'; import type { Project } from '@/domain/model/project'; /** * Ehrlichkeit der Herkunftsangaben im Ausdruck. * * Das Modell fuehrt nicht mit, woher Raeum- und Einfahrweg einer * Konfliktbeziehung stammen: `Conflict.note` ist ein freies Bemerkungsfeld. * uebernimmWege hinterlaesst dort zwar "Aus dem Lageplan vermessen", doch der * Bearbeiter kann den Text ueberschreiben, und vor allem bleibt er stehen, wenn * er anschliessend die Wege von Hand aendert - die Massenuebernahme im * Konfliktdialog tut genau das. * * Ein Ausdruck, der diesen Text als Herkunftsnachweis ausgibt, macht in einer * Unterlage fuer ein verkehrsbehoerdliches Verfahren eine falsche Angabe zur * Herkunft eines sicherheitsrelevanten Masses. Diese Tests halten fest, dass * der Ausdruck nur behauptet, was das Modell hergibt. */ const FIXED_DATE = 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 stueck of inhalt.matchAll(/\((?:\\.|[^\\()])*\)\s*Tj/g)) { teile.push(stueck[0].slice(1, stueck[0].lastIndexOf(')'))); } } // Lange Absaetze sind auf Zeilen umbrochen; fuer die Suche wieder // zusammenziehen, sonst findet contains() den halben Satz nicht. return teile.join(' ').replace(/\s+/g, ' '); } function ausdruck(project: Project): string { const plan = buildSignalPlan(project); const report = validateProject(project, plan, FIXED_DATE); return pdfText( buildProjectPdf( project, plan, report, { ...DEFAULT_PDF_OPTIONS, includeCalculationDetails: true }, FIXED_DATE, ), ); } /** Fahrlinie mit zwei Punkten - genug, damit der Lageplan auswertbar ist. */ function fahrlinie(id: string): Planlinie { return { id, name: id, mode: 'kfz', movement: 'geradeaus', breiteMeter: REGELBREITE.kfz, punkte: [ { x: 10, y: 10 }, { x: 10, y: 90 }, ], haltlinieId: null, startT: 0.5, signalGroupId: null, }; } const AUSWERTBARER_LAGEPLAN: Lageplan = { arbeitsbereiche: [], signalgeber: [], bild: { datenUrl: 'data:image/png;base64,AAAA', breite: 800, hoehe: 600, herkunft: 'Luftbild NRW · Land NRW · Kante 400 m', geladenAm: '2026-03-04T09:15:00.000Z', }, kalibrierung: { von: { x: 100, y: 200 }, bis: { x: 200, y: 200 }, laengeMeter: 50, herkunft: 'gemessen', }, haltlinien: [], linien: [fahrlinie('l1'), fahrlinie('l2')], }; describe('Herkunftsangaben im Ausdruck', () => { it('weist die Bemerkung einer Beziehung als Bemerkung aus, nicht als Herkunft', () => { const text = ausdruck(createStandardIntersectionProject('Testknoten', FIXED_DATE)); expect(text).toContain( 'Die Spalte "Bemerkung" gibt daneben den freien Vermerk des Bearbeiters', ); expect(text).toContain('Sie ist kein Nachweis der Herkunft'); // Der Vorgaenger erhob den freien Text zum Nachweis der Vermessung. expect(text).not.toContain('setzt die Übernahme aus dem Lageplan'); // Der Nachweis steht jetzt in einer EIGENEN Spalte, die das Modell fuehrt. expect(text).toContain('Herkunft der Wege'); expect(text).toContain('Das Programm führt die Angabe selbst und lässt sie nicht beschreiben'); }); it('erfindet fuer eine leere Bemerkung keine Herkunft', () => { // Die Beziehungen des Regelknotenpunkts tragen keine Bemerkung. Ihre Wege // stammen aus der Vorlage, nicht aus der Hand des Bearbeiters - "von Hand // erfasst" waere schon hier falsch. const text = ausdruck(createStandardIntersectionProject('Testknoten', FIXED_DATE)); expect(text).not.toContain('von Hand erfasst'); }); it('behauptet keine Vermessung, wenn der Weg nach der Uebernahme geaendert wurde', () => { const basis = createStandardIntersectionProject('Testknoten', FIXED_DATE); const erster = basis.conflicts[0]; expect(erster).toBeDefined(); if (erster === undefined) return; // Genau der gefaehrliche Ablauf: aus dem Lageplan uebernommen, danach den // Raeumweg von Hand verlaengert. Die Bemerkung bleibt stehen. const project: Project = { ...basis, conflicts: basis.conflicts.map((c) => c.id === erster.id ? { ...c, note: 'Aus dem Lageplan vermessen.', clearingDistance: 41 } : c, ), }; const text = ausdruck(project); // Die Bemerkung darf erscheinen - aber nur als Bemerkung, mit dem Hinweis, // dass sie aelter sein kann als der Wert daneben. expect(text).toContain('Aus dem Lageplan vermessen.'); expect(text).toContain('kann aus der Zeit vor einer späteren Änderung der Wege stammen'); // Und die Spalte, die den Nachweis traegt, darf sich davon nicht // anstecken lassen: Die Beziehungen des Regelknotenpunkts sind nicht aus // einem Lageplan uebernommen worden, egal was in der Bemerkung steht. expect(text).toContain('Herkunft nicht nachgewiesen'); }); it('behauptet in der Planungsgrundlage nicht, die Masse stammten aus der Unterlage', () => { const basis = createStandardIntersectionProject('Testknoten', FIXED_DATE); const text = ausdruck({ ...basis, lageplan: AUSWERTBARER_LAGEPLAN }); expect(text).toContain('Der Lageplan ist maßstäblich auswertbar'); // Seit das Modell die Herkunft je Beziehung fuehrt, waere der alte Satz // ("Das Projekt fuehrt die Herkunft der einzelnen Masse nicht mit") falsch. expect(text).not.toContain('führt die Herkunft der einzelnen Maße nicht mit'); expect(text).toContain('führt das Projekt je Beziehung mit'); // Der Vorgaenger verwies fuer die Uebernahme auf die Bemerkungsspalte. expect(text).not.toContain('Welche Beziehungen daraus übernommen wurden'); }); it('behauptet ohne Lageplan nicht, die Wege seien von Hand erfasst', () => { const text = ausdruck(createStandardIntersectionProject('Testknoten', FIXED_DATE)); expect(text).toContain('Diesen Unterlagen liegt kein Lageplan und kein Luftbild bei'); expect(text).toContain('stammen daher nicht aus diesen Unterlagen'); }); });