import { describe, expect, it } from 'vitest'; import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; import { computeCycleTime } from '@/domain/rilsa/cycle'; import { QUELLEN } from '@/domain/rilsa/quellen'; import { settingFields } from '@/domain/rilsa/settings'; /** * Befund 19 (Fassung 5.11.0): Ziel- und Hoechst-Auslastungsgrad standen * unter der Fundstelle 'praxis'. * * Deren Hinweis lautet woertlich "Frei wählbar; wirkt sich nicht auf die * Einhaltung des Regelwerks aus" - und dieser Satz wird GEDRUCKT: Das * Fundstellenverzeichnis der Planunterlage setzt ihn als "Zu beachten" unter * die Zeile (pdf.ts, quellenverzeichnis). Fuer diese beiden Werte ist er * falsch. Der Ziel-Auslastungsgrad steht im Nenner der HBS-Umlaufzeitformel, * der Hoechstwert in dem der HCM-Formel; beide entscheiden ausserdem, ab * welchem Y die Rechnung "uebersaettigt" meldet, und beide sind je die * Schwelle einer Meldung des Pruefberichts. Die Umlaufzeit ist an * Regelwerksgrenzen gebunden - die Einhaltung des Regelwerks haengt also sehr * wohl daran. * * Derselbe Fehlertyp wie bei der Anfahrverlustzeit (Befund 27, Fassung 5.10.0), * die aus demselben Grund eine eigene Fundstelle bekam. */ const SATZ_DER_WIRKUNGSLOSIGKEIT = 'wirkt sich nicht auf die Einhaltung des Regelwerks aus'; const AUSLASTUNGSFELDER = ['targetDegreeOfSaturation', 'maxDegreeOfSaturation'] as const; function feld(key: (typeof AUSLASTUNGSFELDER)[number]) { const gefunden = settingFields().find((f) => f.key === key); expect(gefunden, `Vorgabenfeld "${key}"`).toBeDefined(); return gefunden!; } describe('Auslastungsgrade: die Fundstelle darf keine Wirkungslosigkeit behaupten (Befund 19)', () => { it('haengt beide Felder nicht mehr an der Sammelquelle "praxis"', () => { for (const key of AUSLASTUNGSFELDER) { expect(feld(key).quellenSchluessel, key).not.toBe('praxis'); } }); it('druckt zu keinem der beiden Felder den Satz von der Wirkungslosigkeit', () => { for (const key of AUSLASTUNGSFELDER) { expect(feld(key).quelle.hinweis ?? '', key).not.toContain(SATZ_DER_WIRKUNGSLOSIGKEIT); expect(feld(key).quelle.verwendung, key).not.toContain(SATZ_DER_WIRKUNGSLOSIGKEIT); } }); it('nennt in der Fundstelle statt dessen, worauf die beiden Werte wirken', () => { for (const key of AUSLASTUNGSFELDER) { const quelle = feld(key).quelle; expect(quelle.verwendung, `${key}: Umlaufzeit`).toContain('Umlaufzeit'); expect(quelle.verwendung, `${key}: Pruefbericht`).toContain('Prüfbericht'); expect(quelle.sachgebiet, `${key}: Sachgebiet`).toContain('Auslastungsgrad'); } }); it('fuehrt beide als selbstgesetzte Schwelle, nie als gesichert', () => { for (const key of AUSLASTUNGSFELDER) { // Grundsatz: selbstgesetzte Schwellen tragen nie "gesichert". Die Wahl // INNERHALB des Bandes trifft dieses Programm; belegt ist nur das Band. expect(feld(key).quelle.pruefstand, key).not.toBe('gesichert'); expect(feld(key).quelle.regelwerk, key).not.toContain('HBS'); } }); it('nennt seit 5.27.0 das belegte Band und haelt beide Werte darin', () => { /* * UMGESTELLT MIT 5.27.0. Hier stand die Erwartung "Kurzform Planungspraxis" * und "regelwerk enthaelt nicht RiLSA" - richtig, solange die RiLSA nicht * vorlag: Das HBS 2015 kennt als Ueberlastungskriterium allein q > C, und * ein Ziel- oder Hoechstwert kommt darin nicht vor. Die RiLSA nennt in * Abschnitt 2.6 sehr wohl ein Band (0,80 bis 0,90). Belegt ist damit das * Band, nicht die Wahl darin - deshalb bleibt der Pruefstand * "abzugleichen" und die Fundstelle nennt beides. */ for (const key of AUSLASTUNGSFELDER) { const quelle = feld(key).quelle; expect(quelle.kurz, key).toBe('RiLSA 2015'); expect(quelle.fundstelle, key).toContain('2.6'); expect(quelle.hinweis ?? '', key).toContain('0,80 und 0,90'); } expect(RILSA_DEFAULTS.capacity.targetDegreeOfSaturation).toBe(0.85); expect(RILSA_DEFAULTS.capacity.maxDegreeOfSaturation).toBe(0.9); }); /* * Der Fall sagt genau zwei Dinge zu, und beide misst er: Der Satz steht * weiter an der Sammelquelle, und keines der beiden Auslastungsfelder haengt * noch an ihr. OB der Satz fuer die verbliebenen Felder zutrifft, prueft er * NICHT - eine solche Pauschalaussage war die Ursache von Befund 19, und sie * gehoert nicht als Zusage in einen Testnamen. Nachgemessen ist sie je * Feld einzeln. * * NACHGEZOGEN (Fassung 5.12.0): Die Zusicherung "es haengen ueberhaupt * noch Felder an der Sammelquelle" ist entfallen, denn inzwischen sind die * letzten drei Felder aus ihr herausgeloest * (`kennwertquellen.test.ts`). Die Schleife lief damit ueber eine * leere Liste und pruefte im Regelfall nichts mehr - sie fing nur noch die * Rueckkehr eines Auslastungsfeldes. An ihre Stelle tritt die staerkere Form * derselben Aussage: An der Sammelquelle haengt KEIN Vorgabenfeld mehr, * also erst recht keines der beiden. Der Satz bleibt an der Quelle stehen, * weil sie den Massstab benennt; gedruckt wird er nirgends mehr. * * ENTFERNT: Unter der Null stand die Schleife des alten * Falles - `for (const f of anDerSammelquelle)` mit der Zusicherung, keines * der Felder an der Quelle sei ein Auslastungsfeld. Hinter einer Liste der * Laenge 0 kann sie keinen Durchlauf haben; GEMESSEN mit `expect.fail()` * als erster Zeile ihres Rumpfes: alle acht Faelle der Datei blieben gruen. * Sie faellt weg und wird nicht umgekehrt zur tragenden Zusicherung * gemacht - die Null darueber ist die staerkere Aussage, eine leere Liste * enthaelt auch kein Auslastungsfeld, und sie fuer die Schleife * herzugeben waere ein Abschwaechen. Dass keines der beiden Felder an der * Sammelquelle haengt, misst ohnehin der erste Fall dieser Datei je Feld * einzeln. */ it('laesst den Satz an der Sammelquelle stehen, und keines der beiden Auslastungsfelder haengt noch an ihr', () => { expect(QUELLEN.praxis.hinweis ?? '').toContain(SATZ_DER_WIRKUNGSLOSIGKEIT); const anDerSammelquelle = settingFields().filter((f) => f.quellenSchluessel === 'praxis'); expect( anDerSammelquelle.map((f) => f.key), 'Vorgabenfelder an "praxis"', ).toHaveLength(0); }); }); describe('Auslastungsgrade: die Wirkung, die der alte Satz bestritt', () => { it('verschiebt der Ziel-Auslastungsgrad die HBS-Umlaufzeit', () => { const eingang = { lostTime: 20, criticalFlowRatios: [0.35, 0.35] }; const mitVorgabe = computeCycleTime('hbs', eingang); const hoeher = computeCycleTime('hbs', eingang, { ...RILSA_DEFAULTS, capacity: { ...RILSA_DEFAULTS.capacity, targetDegreeOfSaturation: 0.95 }, }); expect(RILSA_DEFAULTS.capacity.targetDegreeOfSaturation).toBe(0.85); // L * x / (x - Y): 20 * 0,85 / 0,15 = 113,3 s gegen 20 * 0,95 / 0,25 = 76 s. expect(mitVorgabe.raw).toBeCloseTo(113.33, 2); expect(hoeher.raw).toBeCloseTo(76, 2); expect(hoeher.cycleTime).toBeLessThan(mitVorgabe.cycleTime); }); it('entscheidet der Hoechst-Auslastungsgrad, ab wann die HCM-Rechnung uebersaettigt meldet', () => { // Y = 0,88 - unter dem Hoechstwert 0,90 und ueber einem strengeren 0,85. // Die Zahlen sind mit 5.27.0 nachgezogen: Zuvor stand der Hoechstwert bei // 0,95, und Y = 0,92 lag darunter. const eingang = { lostTime: 20, criticalFlowRatios: [0.44, 0.44] }; // Seit 5.27.0 das obere Ende des Bandes, das die RiLSA 2015 in // Abschnitt 2.6 fuer den Auslastungsgrad x nennt (0,80 bis 0,90). expect(RILSA_DEFAULTS.capacity.maxDegreeOfSaturation).toBe(0.9); expect(computeCycleTime('hcm', eingang).bounded).not.toBe('uebersaettigt'); const strenger = computeCycleTime('hcm', eingang, { ...RILSA_DEFAULTS, capacity: { ...RILSA_DEFAULTS.capacity, maxDegreeOfSaturation: 0.85 }, }); expect(strenger.bounded).toBe('uebersaettigt'); expect(strenger.notes.some((n) => n.code === 'uebersaettigt')).toBe(true); }); });