import { readFileSync, readdirSync } from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import type { Project } from '@/domain/model/project'; /* * Befund 49 (Fassung 5.10.0). * * Die Unterlagen stellten der 4.x-Empfehlung ("addierte Objekte auf eine Zahl * -> immer NaN") fuer Version 5 eine "begruendete Empfehlung" gegenueber und * sagten zu, dass die Aenderungen "andere Zahlen" liefern - also eine Ausgabe, * nicht bloss eine vorhandene Formel. * * `compareCycleTimeMethods` rechnete sie und war geprueft, hatte aber im ganzen * `src/`-Baum keinen Aufrufer. Der Signalzeitenplan ging ueber * `resolveCycleTime` und rechnete genau das eine gewaehlte Verfahren; die * Auswahlliste der Phasenansicht kannte keine Empfehlung, und weder Pruefbericht * noch Ausdruck noch Tabellenausgabe fuehrten einen Empfehlungstext. * * DER FALL IST GEFALLEN, UND ZWAR PLANMAESSIG (Fassung 5.29.0). Der Vergleich * hat seit dieser Fassung einen Aufrufer: Der Planaufbau rechnet ihn und legt * ihn als `cycleComparison` in den Plan, die Phasenansicht stellt die vier * Ansaetze gegenueber, und die Planunterlage druckt sie in einem eigenen, * abwaehlbaren Abschnitt. Die Faelle messen die Aufruferlage am Baum, nicht an * einer Behauptung, und verlangen jetzt den Aufrufer. */ /** Alle Quelldateien unter src/. */ function quelldateien(verzeichnis: string): string[] { const gefunden: string[] = []; for (const eintrag of readdirSync(verzeichnis, { withFileTypes: true })) { const pfad = path.join(verzeichnis, eintrag.name); if (eintrag.isDirectory()) gefunden.push(...quelldateien(pfad)); else if (eintrag.name.endsWith('.ts')) gefunden.push(pfad); } return gefunden; } /** * Dateien unter src/, die den Namen nennen - ohne die Stelle der Definition * und ohne Kommentare. * * KOMMENTARE ZAEHLEN NICHT (Fassung 5.28.0): Die Frage lautet "gibt es einen * Aufrufer", und ein Satz ueber die Funktion ist keiner. Seit der Behebung * erklaert der Kopfkommentar von `compareCycleTimeMethods`, warum die * Empfehlung ein Wert und kein Verfahren ist, und nennt dabei den eigenen * Namen; die Pruefung schlug daraufhin an, ohne dass ein Aufruf hinzugekommen * waere. Ein Waechter, der sich an der eigenen Begruendung stoert, treibt dazu, * die Begruendung wegzulassen. */ function nennungenAusserDerDefinition(name: string): string[] { const gefunden: string[] = []; for (const datei of quelldateien('src')) { const inhalt = readFileSync(datei, 'utf8') .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/\/\/[^\n]*/g, ''); const ohneDefinition = inhalt.replace(`export function ${name}(`, ''); if (ohneDefinition.includes(name)) gefunden.push(datei); } return gefunden; } const DATUM = new Date('2026-01-01T00:00:00Z'); /** * Ein Knotenpunkt, an dem NUR das HBS-Verfahren scheitert. * * Gemessen: 900 Kfz/h je Signalgruppe ergeben Y = 0,90 bei einer Verlustzeit * von 24 s. Die Auslastungssumme liegt damit ueber dem Ziel-Auslastungsgrad * des HBS-Verfahrens (0,85) und unter der Grenze 1 der wartezeitminimalen * Ansaetze: Der Vergleich meldet fuer HBS 'uebersaettigt', Webster dagegen * 'maximum' - rechenbar, aber ueber dem Hoechstwert. * * Genau diese Lage zeigt, ob fremde Meldungen in den Pruefbericht laufen: Der * Kode 'uebersaettigt' entsteht ausschliesslich im HBS- und im HCM-Zweig, und * gewaehlt ist keiner von beiden. */ function uebersaettigtesHbs(): Project { const roh = createStandardIntersectionProject('Nur HBS uebersaettigt', DATUM); return { ...roh, program: { ...roh.program, method: 'webster' }, demands: roh.signalGroups .filter((g) => g.mode === 'kfz') .map((g) => ({ signalGroupId: g.id, volume: 900, heavyVehicleShare: 0 })), }; } /** * Eine einstreifige Verkehrsfuehrung mit einer Umlaufzeit ueber 120 s. * * Der lange Raeumweg treibt die Zwischenzeiten und damit die Verlustzeit; die * Anlagenart laesst bis 300 s zu. Gemessen mit 60 m Raeumweg je Beziehung und * 300 Kfz/h: 230 s nach Webster, ohne Grenzfall. Am Knotenpunkt waere derselbe * Wert bei 120 s gekappt - daran zeigt sich, mit welchen Schranken der * Vergleich rechnet. */ function langeEngstelle(): Project { const roh = createStandardIntersectionProject('Lange Engstelle', DATUM); return { ...roh, anlagenart: 'einstreifig', conflicts: roh.conflicts.map((c) => ({ ...c, clearingDistance: 60 })), demands: roh.signalGroups .filter((g) => g.mode === 'kfz') .map((g) => ({ signalGroupId: g.id, volume: 300, heavyVehicleShare: 0 })), }; } describe('Umlaufzeitempfehlung - der Sachstand', () => { it('hat compareCycleTimeMethods jetzt einen Aufrufer im src-Baum', () => { // Umgedreht mit Fassung 5.29.0: Der Planaufbau ruft den Vergleich, legt ihn als // `cycleComparison` in den Plan, und Ansicht wie Ausdruck lesen ihn von // dort. Faellt der Aufrufer wieder weg, gehoeren die Vermerke unten // zurueck in die Unterlagen - dieser Fall sagt es dann. expect(nennungenAusserDerDefinition('compareCycleTimeMethods')).toContain( path.join('src', 'domain', 'plan', 'signalPlan.ts'), ); }); it('nimmt der Plan den Vergleich mit, ohne fremde Meldungen einzusammeln', () => { /* * Die entscheidende Nebenbedingung des Ausbaus: Die drei nicht gewaehlten * Verfahren rechnen an einer Umlaufzeit, die niemand schaltet. Ihre * Beanstandungen duerfen den Pruefbericht dieser Planung nicht fuellen. * * Der Fall waehlt Webster bei einer Auslastungssumme, bei der das * HBS-Verfahren uebersaettigt ist - die gemessenen Zahlen stehen am * Projektaufbau `uebersaettigtesHbs`. */ const plan = buildSignalPlan(uebersaettigtesHbs()); expect(plan.cycleComparison, 'der Plan traegt den Vergleich').not.toBeNull(); expect(plan.cycleComparison?.results.hbs.bounded).toBe('uebersaettigt'); expect( plan.notes.some((n) => n.code === 'uebersaettigt'), 'die Uebersaettigung des HBS-Verfahrens steht nicht im Pruefbericht', ).toBe(false); expect(plan.cycleResult?.method, 'gerechnet wird das gewaehlte Verfahren').toBe('webster'); }); it('nimmt das gewaehlte Ergebnis aus dem Vergleich, statt es zweimal zu rechnen', () => { // Sonst koennten Tafel und Signalzeitenplan verschiedene Zahlen zeigen. const plan = buildSignalPlan(uebersaettigtesHbs()); expect(plan.cycleResult).not.toBeNull(); expect(plan.cycleComparison?.results.webster.cycleTime).toBe(plan.cycleResult?.cycleTime); }); it('rechnet den Vergleich mit den Schranken der Anlagenart', () => { /* * DIE STELLE, AN DER ES STILL SCHIEFGEHT. `resolveCycleTime` baut sich aus * den Vorgaben und der Anlagenart einen eigenen Kennwertsatz * (`cycleDefaults`); der Vergleich muss denselben bekommen. Wer ihn * stattdessen mit RILSA_DEFAULTS rechnet, bekommt die Grenzen des * Knotenpunkts - und an einer einstreifigen Verkehrsfuehrung, wo bis 300 s * zulaessig sind, staende in der Tafel ein auf 120 s gekapptes Ergebnis * neben einer Umlaufzeit von 150 s im Plan. * * Gemessen: Die Grenzen der einstreifigen Fuehrung sind 30 bis 300 s, die * des Knotenpunkts 30 bis 120 s. */ const plan = buildSignalPlan(langeEngstelle()); expect(plan.anlagenart).toBe('einstreifig'); const webster = plan.cycleComparison?.results.webster; expect(webster, 'der Vergleich ist da').toBeDefined(); expect( webster?.cycleTime, 'mit den Knotenpunktgrenzen waere hier bei 120 s gekappt worden', ).toBeGreaterThan(120); expect(webster?.bounded, 'unter 300 s ist das kein Grenzfall').toBe('keine'); expect(webster?.cycleTime, 'dieselbe Zahl wie im Plan').toBe(plan.cycleResult?.cycleTime); }); });