import { describe, expect, it } from 'vitest'; import { compareCycleTimeMethods } from '@/domain/rilsa/cycle'; import { RILSA_DEFAULTS } from '@/domain/rilsa/constants'; /** * Fassung 5.28.0, Punkte A1 bis A3. * * `compareCycleTimeMethods` hat bis heute keinen Aufrufer - und genau deshalb * standen drei Fehler unbemerkt darin. Ein Test, der die Funktion unmittelbar * ruft, ist die einzige Wache, die sie derzeit haben kann; dass sie unbenutzt * ist, bewacht `umlaufzeitempfehlung.test.ts` gesondert und * planmaessig. * * Alle Rechenwerte hier sind aus den Formeln in `cycle.ts` und den Vorgaben in * `constants.ts` von Hand nachgerechnet und stehen als Rechenweg im Kommentar: * * webster tU = (1,5 L + 5) / (1 - Y) * akcelik tU = (1,4 L + 6) / (1 - Y) * hbs tU = L x / (x - Y) mit x = 0,85 (Ziel-Auslastungsgrad) * hcm tU = L Xc / (Xc - Y) mit Xc = 0,90 (hoechster Auslastungsgrad) * * gerastert auf 5 s, begrenzt auf 30 bis 120 s. */ describe('Umlaufzeitvergleich - ein Wert, kein Gewinner (A1)', () => { it('nennt bei Gleichstand alle Verfahren, die den Wert erreichen', () => { /* * L = 10 ist die Verlustzeit, bei der Webster und Akcelik zusammenfallen: * 1,5 L + 5 = 1,4 L + 6 <=> L = 10. Beide rechnen dann 20 / (1 - Y). * * Bei Y = 0,60: * webster 20 / 0,40 = 50,0 -> 50 s * akcelik 20 / 0,40 = 50,0 -> 50 s * hbs 8,5 / 0,25 = 34,0 -> 35 s * hcm 9,0 / 0,30 = 30,0 -> 30 s * Kapazitaetsbedarf 35 s, Wartezeitoptimum 50 s, empfohlen 50 s. * * BIS ZUR BEHEBUNG stand hier ein einzelnes Ergebnis, herausgesucht mit * `Object.values(results).find(...)`. Webster gewann, weil es im * Objektliteral zuerst steht - nicht, weil es etwas besser koennte. */ const vergleich = compareCycleTimeMethods({ lostTime: 10, criticalFlowRatios: [0.6] }); expect(vergleich.results.webster.cycleTime).toBe(50); expect(vergleich.results.akcelik.cycleTime).toBe(50); expect(vergleich.recommendedCycleTime).toBe(50); expect(vergleich.recommendedMethods).toEqual(['webster', 'akcelik']); }); it('sagt dazu, dass die Reihenfolge der Aufzaehlung nichts bedeutet', () => { // Die Hausregel verlangt beides: eine stabile Ordnung UND den Satz, dass // sie nichts aussagt. Ohne ihn liest der erste Eintrag sich als Gewinner. const vergleich = compareCycleTimeMethods({ lostTime: 10, criticalFlowRatios: [0.6] }); expect(vergleich.reason).toContain('keinen Vorrang'); }); it('gibt kein einzelnes Verfahrensergebnis mehr als Empfehlung aus', () => { // Der empfohlene Wert entsteht aus dem Maximum zweier Zielgroessen und // muss mit keinem Einzelergebnis zusammenfallen. Ein Feld, das so tut, // als gaebe es ein massgebendes Verfahren, ist die Unwahrheit. const vergleich = compareCycleTimeMethods({ lostTime: 20, criticalFlowRatios: [0.6] }); expect('recommended' in vergleich).toBe(false); }); it('laesst die Empfehlung im fehlerfreien Fall unveraendert', () => { /* * L = 20, Y = 0,60 - der Fall aus cycle.test.ts: * webster 35 / 0,40 = 87,5 -> 90 s * akcelik 34 / 0,40 = 85,0 -> 85 s * hbs 17 / 0,25 = 68,0 -> 70 s * hcm 18 / 0,30 = 60,0 -> 60 s * Kapazitaetsbedarf 70 s, Wartezeitoptimum 85 s, empfohlen 85 s - und das * erreicht allein Akcelik. */ const vergleich = compareCycleTimeMethods({ lostTime: 20, criticalFlowRatios: [0.6] }); expect(vergleich.recommendedCycleTime).toBe(85); expect(vergleich.recommendedMethods).toEqual(['akcelik']); expect(vergleich.reason).toContain('Empfohlen wird 85,0 s'); }); }); describe('Umlaufzeitvergleich - jeder Fehler zaehlt (A2)', () => { it('empfiehlt nichts, wo der erforderliche Umlauf ueber dem Hoechstwert liegt', () => { /* * DER GEFAEHRLICHSTE FALL DIESER FUNKTION. L = 40, Y = 0,80: * webster 65 / 0,20 = 325 s * akcelik 62 / 0,20 = 310 s * hbs 34 / 0,05 = 680 s * hcm 36 / 0,10 = 360 s * Alle vier ueberschreiten den Hoechstwert von 120 s und melden * 'umlaufzeit-ueber-hoechstwert' als Fehler. Uebersaettigt ist keines: * Y = 0,80 liegt unter allen vier Grenzen (1, 1, 0,85 und 0,90). * * BIS ZUR BEHEBUNG fragte die Funktion nur nach dem Kode 'uebersaettigt'. * Sie schrieb deshalb "Empfohlen wird 120,0 s" - neben vier Meldungen, die * sagten, die Anlage sei mit dieser Phaseneinteilung nicht * leistungsfaehig. */ const vergleich = compareCycleTimeMethods({ lostTime: 40, criticalFlowRatios: [0.8] }); for (const verfahren of ['webster', 'akcelik', 'hbs', 'hcm'] as const) { expect( vergleich.results[verfahren].notes.some((n) => n.code === 'umlaufzeit-ueber-hoechstwert'), verfahren, ).toBe(true); } expect(vergleich.recommendedCycleTime).toBeNull(); expect(vergleich.recommendedMethods).toEqual([]); expect(vergleich.reason).not.toContain('Empfohlen wird'); expect(vergleich.reason).toContain('Höchstwert'); expect(vergleich.reason).toContain('alle vier Verfahren'); }); it('empfiehlt auch dann nichts, wenn nur ein Verfahren beanstandet ist', () => { /* * L = 5, Y = 0,86 - nur das HBS-Verfahren scheitert: * hbs Y = 0,86 >= x = 0,85 -> uebersaettigt * hcm 4,5 / 0,04 = 112,5 s -> 115 s -> rechenbar * webster 12,5 / 0,14 = 89,3 s -> 90 s -> rechenbar * akcelik 13,0 / 0,14 = 92,9 s -> 95 s -> rechenbar * * Warum das genuegt: Der Kapazitaetsbedarf ist das Maximum aus HBS und * HCM, und das uebersaettigte HBS-Ergebnis traegt den Hoechstwert als * ERSATZWERT. Er ginge in das Maximum ein, und die gebildete Zahl saehe * gueltig aus, ohne den erforderlichen Umlauf zu decken. */ const vergleich = compareCycleTimeMethods({ lostTime: 5, criticalFlowRatios: [0.86] }); expect(vergleich.results.hbs.bounded).toBe('uebersaettigt'); expect(vergleich.results.hcm.notes.some((n) => n.severity === 'fehler')).toBe(false); expect(vergleich.results.webster.notes.some((n) => n.severity === 'fehler')).toBe(false); expect(vergleich.recommendedCycleTime).toBeNull(); expect(vergleich.reason).toContain('übersättigt'); expect(vergleich.reason).toContain('Beanstandet ist HBS'); expect(vergleich.reason).not.toContain('alle vier'); }); it('nennt jeden aufgetretenen Grund genau einmal', () => { /* * L = 20, Y = 0,90 - zwei verschiedene Gruende zugleich: * hbs, hcm Y erreicht die Grenze -> uebersaettigt * webster 35 / 0,10 = 350 s -> ueber dem Hoechstwert * akcelik 34 / 0,10 = 340 s -> ueber dem Hoechstwert * Vier Verfahren, zwei Gruende, zwei Saetze - nicht vier. */ const vergleich = compareCycleTimeMethods({ lostTime: 20, criticalFlowRatios: [0.5, 0.4] }); expect(vergleich.recommendedCycleTime).toBeNull(); expect(vergleich.reason).toContain('übersättigt'); expect(vergleich.reason).toContain('Höchstwert'); expect(vergleich.reason.split('übersättigt').length - 1, 'Grund nur einmal').toBe(1); expect(vergleich.reason.split('Höchstwert').length - 1, 'Grund nur einmal').toBe(1); }); it('empfiehlt nichts ohne Saettigungsgrade', () => { const vergleich = compareCycleTimeMethods({ lostTime: 20, criticalFlowRatios: [] }); expect(vergleich.recommendedCycleTime).toBeNull(); expect(vergleich.reason).toContain('Sättigungsgrade'); }); }); describe('Umlaufzeitvergleich - kein Scheinergebnis im Fehlerfall (A3)', () => { it('fuehrt im Fehlerfall keinen Wert und kein Verfahren', () => { /* * BIS ZUR BEHEBUNG gab die Funktion hier `results.webster` zurueck - ein * vollstaendiges Ergebnis mit der Zahl 120 s, in einem Feld, von dem der * Satz unmittelbar daneben sagte, es gebe keines. Wer das Feld las und den * Satz nicht, bekam einen Ersatzwert als Empfehlung. */ const vergleich = compareCycleTimeMethods({ lostTime: 20, criticalFlowRatios: [0.5, 0.4] }); expect(vergleich.recommendedCycleTime).toBeNull(); expect(vergleich.recommendedMethods).toEqual([]); // Die Einzelergebnisse bleiben erhalten - sie sind die Auskunft, aus der // hervorgeht, WORAN es liegt. expect(vergleich.results.hbs.cycleTime).toBe(RILSA_DEFAULTS.cycleTime.max); expect(vergleich.results.hbs.bounded).toBe('uebersaettigt'); }); });