lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
/ tests domain programmbibliothek.test.ts
| 1 | import { describe, expect, it } from 'vitest'; |
| 2 | import * as actions from '@/app/actions'; |
| 3 | import { createStandardIntersectionProject } from '@/domain/model/factory'; |
| 4 | import { |
| 5 | ERSTES_PROGRAMM, |
| 6 | PROGRAMM_HOECHSTZAHL, |
| 7 | aktiverProgrammsatz, |
| 8 | legeProgrammAn, |
| 9 | loescheProgramm, |
| 10 | programmanzahl, |
| 11 | programmfolge, |
| 12 | programmplatz, |
| 13 | wechsleProgramm, |
| 14 | } from '@/domain/model/programme'; |
| 15 | import { CURRENT_SCHEMA_VERSION, type Project } from '@/domain/model/project'; |
| 16 | import { loadProject } from '@/domain/model/migrate'; |
| 17 | import { buildSignalPlan } from '@/domain/plan/signalPlan'; |
| 18 | import { programmPruefungen, validateProject } from '@/domain/validation'; |
| 19 | |
| 20 | /** |
| 21 | * Fassung 5.33.0 - mehrere Tagesprogramme in einem Projekt. |
| 22 | * |
| 23 | * DIE EINE SICHERHEITSRELEVANTE BEDINGUNG DES BEFUNDS: Pruefung und |
| 24 | * Ausgabesperre laufen ueber ALLE Programme und nicht ueber das sichtbare. |
| 25 | * Sonst geht ein Nachtprogramm mit unterschrittener Mindestfreigabezeit |
| 26 | * ungeprueft in die Anordnungsunterlage. Sie steht in diesem Fall ganz unten |
| 27 | * und ist der Grund, aus dem alles Uebrige gebaut wurde. |
| 28 | */ |
| 29 | |
| 30 | const STICHTAG = new Date('2026-01-01T12:00:00Z'); |
| 31 | |
| 32 | function basis(): Project { |
| 33 | return createStandardIntersectionProject('Hauptstraße / Bahnhofstraße', STICHTAG); |
| 34 | } |
| 35 | |
| 36 | /** Ein Programm, dessen Umlaufzeit die Schranke des Knotenpunkts reisst. */ |
| 37 | function mitFehler(project: Project): Project { |
| 38 | return { |
| 39 | ...project, |
| 40 | program: { ...project.program, method: 'manuell', manualCycleTime: 300 }, |
| 41 | }; |
| 42 | } |
| 43 | |
| 44 | describe('Programmbibliothek - das Umschalten traegt die Daten', () => { |
| 45 | it('hält die Verkehrsstärken jedes Programms auseinander', () => { |
| 46 | /* |
| 47 | * DIE KERNZUSAGE. Fruehspitze und Nacht beschreiben denselben Knotenpunkt |
| 48 | * mit verschiedenen Zaehlungen. Liefe eine Zahl in das andere Programm |
| 49 | * ueber, waere die Bemessung der Nacht die der Spitze - und die |
| 50 | * Freigabezeiten fielen fuer die falsche Stunde aus. |
| 51 | */ |
| 52 | const tag = basis(); |
| 53 | const k1 = tag.signalGroups[0]!.id; |
| 54 | const mitTag = actions.setDemand(tag, k1, { volume: 900 }); |
| 55 | |
| 56 | const nacht = legeProgrammAn(mitTag, 'Nacht'); |
| 57 | expect(nacht.demands, 'ein neues Programm beginnt ohne Zählung').toEqual([]); |
| 58 | const mitNacht = actions.setDemand(nacht, k1, { volume: 120 }); |
| 59 | |
| 60 | const zurueck = wechsleProgramm(mitNacht, ERSTES_PROGRAMM); |
| 61 | expect(zurueck.demands.find((d) => d.signalGroupId === k1)?.volume).toBe(900); |
| 62 | |
| 63 | const wiederNacht = wechsleProgramm(zurueck, mitNacht.program.id); |
| 64 | expect(wiederNacht.demands.find((d) => d.signalGroupId === k1)?.volume).toBe(120); |
| 65 | }); |
| 66 | |
| 67 | it('hält die festen Freigabezeiten jedes Programms auseinander', () => { |
| 68 | const tag = basis(); |
| 69 | const phase = tag.phases[0]!.id; |
| 70 | const mitTag = actions.updatePhase(tag, phase, { manualGreen: 42 }); |
| 71 | |
| 72 | const nacht = legeProgrammAn(mitTag, 'Nacht'); |
| 73 | expect(nacht.phases.find((p) => p.id === phase)?.manualGreen).toBeNull(); |
| 74 | const mitNacht = actions.updatePhase(nacht, phase, { manualGreen: 12 }); |
| 75 | |
| 76 | const zurueck = wechsleProgramm(mitNacht, ERSTES_PROGRAMM); |
| 77 | expect(zurueck.phases.find((p) => p.id === phase)?.manualGreen).toBe(42); |
| 78 | expect( |
| 79 | wechsleProgramm(zurueck, mitNacht.program.id).phases.find((p) => p.id === phase)?.manualGreen, |
| 80 | ).toBe(12); |
| 81 | }); |
| 82 | |
| 83 | it('führt keine Angabe zweimal', () => { |
| 84 | /* |
| 85 | * Die Form dieses Modells - aktives Programm oben, uebrige in der |
| 86 | * Bibliothek - traegt nur, solange sich die beiden Seiten nicht |
| 87 | * ueberschneiden. Stuende das aktive Programm auch in der Bibliothek, gaebe |
| 88 | * es zwei Staende derselben Sache, und einer davon wuerde beim naechsten |
| 89 | * Umschalten der andere. |
| 90 | */ |
| 91 | let projekt = basis(); |
| 92 | projekt = legeProgrammAn(projekt, 'Nacht'); |
| 93 | projekt = legeProgrammAn(projekt, 'Frühspitze'); |
| 94 | for (const eintrag of programmfolge(projekt)) { |
| 95 | const gewechselt = wechsleProgramm(projekt, eintrag.satz.programm.id); |
| 96 | expect( |
| 97 | gewechselt.programmbibliothek.map((s) => s.programm.id), |
| 98 | `nach dem Wechsel auf ${eintrag.satz.programm.name}`, |
| 99 | ).not.toContain(gewechselt.program.id); |
| 100 | expect(programmanzahl(gewechselt)).toBe(3); |
| 101 | } |
| 102 | }); |
| 103 | |
| 104 | it('lässt die Reihenfolge stehen', () => { |
| 105 | // Ohne festgehaltenen Platz wanderte das bearbeitete Programm bei jedem |
| 106 | // Umschalten an den Anfang, und die Auswahlliste sortierte sich unter der |
| 107 | // Hand um - fuer den Bearbeiter sieht das aus, als habe er etwas |
| 108 | // verschoben. |
| 109 | let projekt = basis(); |
| 110 | projekt = legeProgrammAn(projekt, 'Nacht'); |
| 111 | projekt = legeProgrammAn(projekt, 'Frühspitze'); |
| 112 | const namen = programmfolge(projekt).map((e) => e.satz.programm.name); |
| 113 | expect(namen).toEqual(['Programm 1', 'Nacht', 'Frühspitze']); |
| 114 | |
| 115 | for (const eintrag of programmfolge(projekt)) { |
| 116 | const gewechselt = wechsleProgramm(projekt, eintrag.satz.programm.id); |
| 117 | expect( |
| 118 | programmfolge(gewechselt).map((e) => e.satz.programm.name), |
| 119 | `nach dem Wechsel auf ${eintrag.satz.programm.name}`, |
| 120 | ).toEqual(namen); |
| 121 | } |
| 122 | }); |
| 123 | |
| 124 | it('übernimmt Einstellungen, aber keine Zählungen', () => { |
| 125 | const roh = basis(); |
| 126 | const tag: Project = { |
| 127 | ...roh, |
| 128 | program: { ...roh.program, method: 'manuell', manualCycleTime: 80, offset: 17 }, |
| 129 | }; |
| 130 | const nacht = legeProgrammAn(tag, 'Nacht'); |
| 131 | expect(nacht.program.method).toBe('manuell'); |
| 132 | expect(nacht.program.manualCycleTime).toBe(80); |
| 133 | expect(nacht.program.offset).toBe(17); |
| 134 | expect(nacht.program.phaseOrder).toEqual(tag.program.phaseOrder); |
| 135 | expect(nacht.demands).toEqual([]); |
| 136 | expect(aktiverProgrammsatz(nacht).festeFreigabezeiten).toEqual([]); |
| 137 | }); |
| 138 | |
| 139 | it('bleibt bei unbekannter Kennung und beim letzten Programm stehen', () => { |
| 140 | const projekt = basis(); |
| 141 | expect(wechsleProgramm(projekt, 'gibt-es-nicht')).toBe(projekt); |
| 142 | expect(wechsleProgramm(projekt, projekt.program.id)).toBe(projekt); |
| 143 | // Ein Projekt ohne Programm hat keinen Signalzeitenplan. |
| 144 | expect(loescheProgramm(projekt, projekt.program.id)).toBe(projekt); |
| 145 | }); |
| 146 | |
| 147 | it('lässt beim Löschen den Nachbarn eintreten', () => { |
| 148 | let projekt = basis(); |
| 149 | projekt = legeProgrammAn(projekt, 'Nacht'); |
| 150 | projekt = legeProgrammAn(projekt, 'Frühspitze'); |
| 151 | const nachtId = programmfolge(projekt)[1]!.satz.programm.id; |
| 152 | |
| 153 | // Das aktive Programm loeschen: der Nachbar tritt an seine Stelle. |
| 154 | const ohneAktives = loescheProgramm(projekt, projekt.program.id); |
| 155 | expect(programmanzahl(ohneAktives)).toBe(2); |
| 156 | expect(ohneAktives.program.id).toBe(nachtId); |
| 157 | |
| 158 | // Ein eingelagertes loeschen: das aktive bleibt aktiv. |
| 159 | const ohneNacht = loescheProgramm(projekt, nachtId); |
| 160 | expect(ohneNacht.program.id).toBe(projekt.program.id); |
| 161 | expect(programmfolge(ohneNacht).map((e) => e.satz.programm.name)).toEqual([ |
| 162 | 'Programm 1', |
| 163 | 'Frühspitze', |
| 164 | ]); |
| 165 | }); |
| 166 | |
| 167 | it('nimmt nicht mehr als die Höchstzahl auf', () => { |
| 168 | let projekt = basis(); |
| 169 | for (let i = 2; i <= PROGRAMM_HOECHSTZAHL; i += 1) { |
| 170 | projekt = legeProgrammAn(projekt, `Programm ${String(i)}`); |
| 171 | } |
| 172 | expect(programmanzahl(projekt)).toBe(PROGRAMM_HOECHSTZAHL); |
| 173 | expect(legeProgrammAn(projekt, 'eins zu viel')).toBe(projekt); |
| 174 | }); |
| 175 | |
| 176 | it('klemmt einen unmöglichen Platz, statt ein Loch zu lassen', () => { |
| 177 | const projekt = legeProgrammAn(basis(), 'Nacht'); |
| 178 | for (const platz of [-3, 99, Number.NaN, 1.7]) { |
| 179 | const verbogen: Project = { ...projekt, programmplatz: platz }; |
| 180 | expect(programmplatz(verbogen), String(platz)).toBeGreaterThanOrEqual(0); |
| 181 | expect(programmplatz(verbogen), String(platz)).toBeLessThanOrEqual(1); |
| 182 | expect(programmfolge(verbogen), String(platz)).toHaveLength(2); |
| 183 | } |
| 184 | }); |
| 185 | }); |
| 186 | |
| 187 | describe('Programmbibliothek - die Aktionen der Oberfläche', () => { |
| 188 | it('reichen an den Fachkern durch, ohne etwas hinzuzutun', () => { |
| 189 | /* |
| 190 | * Die drei Aktionen sind Durchreichen und sollen es bleiben: Rechnen |
| 191 | * geschieht in `domain/`, und eine Aktion, die unterwegs etwas anderes |
| 192 | * tut als die Fachfunktion, waere eine zweite Fassung derselben Regel. |
| 193 | * Verglichen wird deshalb Ergebnis gegen Ergebnis. |
| 194 | */ |
| 195 | const roh = basis(); |
| 196 | // Beim Anlegen kann nur der Inhalt verglichen werden: Die Kennung des neuen |
| 197 | // Programms wird erzeugt und faellt bei jedem Aufruf anders aus. |
| 198 | const ueberAktion = actions.programmAnlegen(roh, 'Nacht'); |
| 199 | const ueberFachkern = legeProgrammAn(roh, 'Nacht'); |
| 200 | expect({ ...ueberAktion, program: { ...ueberAktion.program, id: '' } }).toEqual({ |
| 201 | ...ueberFachkern, |
| 202 | program: { ...ueberFachkern.program, id: '' }, |
| 203 | }); |
| 204 | |
| 205 | const zwei = legeProgrammAn(roh, 'Nacht'); |
| 206 | expect(actions.programmWechseln(zwei, ERSTES_PROGRAMM)).toEqual( |
| 207 | wechsleProgramm(zwei, ERSTES_PROGRAMM), |
| 208 | ); |
| 209 | expect(actions.programmEntfernen(zwei, zwei.program.id)).toEqual( |
| 210 | loescheProgramm(zwei, zwei.program.id), |
| 211 | ); |
| 212 | expect(programmanzahl(actions.programmEntfernen(zwei, zwei.program.id))).toBe(1); |
| 213 | }); |
| 214 | }); |
| 215 | |
| 216 | describe('Programmbibliothek - was allen Programmen gehört', () => { |
| 217 | it('hängt eine neue Phase an jede Programmreihenfolge', () => { |
| 218 | // Eine Phase, die in einem Programm nicht vorkommt, bekommt dort nie |
| 219 | // Freigabe. Das darf nicht davon abhaengen, welches Programm beim Anlegen |
| 220 | // gerade sichtbar war. |
| 221 | let projekt = legeProgrammAn(basis(), 'Nacht'); |
| 222 | projekt = actions.addPhase(projekt); |
| 223 | const neue = projekt.phases[projekt.phases.length - 1]!.id; |
| 224 | for (const eintrag of programmfolge(projekt)) { |
| 225 | expect(eintrag.satz.programm.phaseOrder, eintrag.satz.programm.name).toContain(neue); |
| 226 | } |
| 227 | }); |
| 228 | |
| 229 | it('entfernt eine gelöschte Phase aus jedem Programm', () => { |
| 230 | let projekt = basis(); |
| 231 | const phase = projekt.phases[0]!.id; |
| 232 | projekt = actions.updatePhase(projekt, phase, { manualGreen: 33 }); |
| 233 | projekt = legeProgrammAn(projekt, 'Nacht'); |
| 234 | projekt = actions.updatePhase(projekt, phase, { manualGreen: 11 }); |
| 235 | projekt = actions.removePhase(projekt, phase); |
| 236 | |
| 237 | for (const eintrag of programmfolge(projekt)) { |
| 238 | expect(eintrag.satz.programm.phaseOrder, eintrag.satz.programm.name).not.toContain(phase); |
| 239 | expect( |
| 240 | eintrag.satz.festeFreigabezeiten.map((f) => f.phaseId), |
| 241 | eintrag.satz.programm.name, |
| 242 | ).not.toContain(phase); |
| 243 | } |
| 244 | expect(projekt.phases.some((p) => p.id === phase)).toBe(false); |
| 245 | }); |
| 246 | |
| 247 | it('räumt eine gelöschte Signalgruppe aus jedem Programm', () => { |
| 248 | /* |
| 249 | * Sonst fuehrt das Nachtprogramm eine Verkehrsstaerke fuer einen Strom, den |
| 250 | * es nicht mehr gibt. Beim naechsten Umschalten stuende sie wieder im |
| 251 | * Projekt - genau der Fehler, an dem der Altbestand mit seiner ueber |
| 252 | * Feldindizes gefuehrten Konfliktmatrix scheiterte. |
| 253 | */ |
| 254 | let projekt = basis(); |
| 255 | const k1 = projekt.signalGroups[0]!.id; |
| 256 | projekt = legeProgrammAn(projekt, 'Nacht'); |
| 257 | projekt = actions.setDemand(projekt, k1, { volume: 150 }); |
| 258 | projekt = wechsleProgramm(projekt, ERSTES_PROGRAMM); |
| 259 | projekt = actions.removeSignalGroup(projekt, k1); |
| 260 | |
| 261 | for (const eintrag of programmfolge(projekt)) { |
| 262 | expect( |
| 263 | eintrag.satz.demands.map((d) => d.signalGroupId), |
| 264 | eintrag.satz.programm.name, |
| 265 | ).not.toContain(k1); |
| 266 | } |
| 267 | }); |
| 268 | }); |
| 269 | |
| 270 | describe('Programmbibliothek - die Prüfung läuft über alle Programme', () => { |
| 271 | it('sperrt die Ausgabe wegen eines Fehlers in einem nicht angezeigten Programm', () => { |
| 272 | /* |
| 273 | * DER FALL, UM DEN ES GEHT. Das sichtbare Programm ist in Ordnung, das |
| 274 | * eingelagerte nicht. Wer nur das sichtbare prueft, gibt eine |
| 275 | * Anordnungsunterlage aus, in der ein Programm steckt, das niemand geprueft |
| 276 | * hat. Eine Anordnung nach § 45 StVO gilt der ANLAGE, nicht einer |
| 277 | * Tageszeit. |
| 278 | */ |
| 279 | const sauber = basis(); |
| 280 | const sauberBericht = validateProject(sauber, buildSignalPlan(sauber), STICHTAG); |
| 281 | expect(sauberBericht.errorCount, 'Vorbedingung: das Grundprojekt ist fehlerfrei').toBe(0); |
| 282 | expect(sauberBericht.exportBlocked).toBe(false); |
| 283 | |
| 284 | // Das Nachtprogramm bekommt den Fehler, danach zurueck auf das saubere. |
| 285 | const mitNacht = wechsleProgramm(mitFehler(legeProgrammAn(sauber, 'Nacht')), sauber.program.id); |
| 286 | expect(mitNacht.program.id, 'sichtbar ist das fehlerfreie Programm').toBe(sauber.program.id); |
| 287 | |
| 288 | const bericht = validateProject(mitNacht, buildSignalPlan(mitNacht), STICHTAG); |
| 289 | expect(bericht.exportBlocked, 'die Ausgabe ist gesperrt').toBe(true); |
| 290 | const befund = bericht.findings.find((f) => f.rule === 'programm.fehler-in-anderem-programm'); |
| 291 | expect(befund?.severity).toBe('fehler'); |
| 292 | expect(befund?.message).toContain('Nacht'); |
| 293 | }); |
| 294 | |
| 295 | it('meldet Warnungen eines anderen Programms als Warnung, nicht als Fehler', () => { |
| 296 | const sauber = basis(); |
| 297 | // Ein Programm ohne Verkehrsstaerken: der Plan steht, aber die Bemessung |
| 298 | // fehlt - das ist eine Warnung und keine Sperre. |
| 299 | const mitLeerem = wechsleProgramm(legeProgrammAn(sauber, 'Nacht'), sauber.program.id); |
| 300 | const bericht = validateProject(mitLeerem, buildSignalPlan(mitLeerem), STICHTAG); |
| 301 | expect(bericht.findings.some((f) => f.rule === 'programm.fehler-in-anderem-programm')).toBe( |
| 302 | false, |
| 303 | ); |
| 304 | const warnung = bericht.findings.find((f) => f.rule === 'programm.warnung-in-anderem-programm'); |
| 305 | expect(warnung?.severity).toBe('warnung'); |
| 306 | expect(bericht.exportBlocked).toBe(false); |
| 307 | }); |
| 308 | |
| 309 | it('zählt keinen Fehler doppelt und prüft nicht im Kreis', () => { |
| 310 | /* |
| 311 | * Ohne den Schalter `ohneProgrammpruefung` pruefte die Pruefung jedes |
| 312 | * Programms alle uebrigen mit: bei vier Programmen sechzehn Plaene, und |
| 313 | * jeder Fehler stuende viermal im Bericht. Die Spaltensumme der Uebersicht |
| 314 | * waere dann groesser als die Zahl der Fehler. |
| 315 | */ |
| 316 | const mitNacht = wechsleProgramm(mitFehler(legeProgrammAn(basis(), 'Nacht')), ERSTES_PROGRAMM); |
| 317 | const pruefungen = programmPruefungen(mitNacht, STICHTAG); |
| 318 | expect(pruefungen).toHaveLength(2); |
| 319 | expect(pruefungen[0]?.aktiv).toBe(true); |
| 320 | expect(pruefungen[0]?.errorCount, 'das sichtbare Programm ist fehlerfrei').toBe(0); |
| 321 | expect(pruefungen[1]?.name).toBe('Nacht'); |
| 322 | expect(pruefungen[1]?.errorCount).toBeGreaterThan(0); |
| 323 | expect(pruefungen[1]?.aktiv).toBe(false); |
| 324 | }); |
| 325 | |
| 326 | it('rechnet jedem Programm seine eigene Umlaufzeit', () => { |
| 327 | const roh = basis(); |
| 328 | let projekt: Project = { |
| 329 | ...roh, |
| 330 | program: { ...roh.program, method: 'manuell', manualCycleTime: 60 }, |
| 331 | }; |
| 332 | projekt = legeProgrammAn(projekt, 'Nacht'); |
| 333 | projekt = { |
| 334 | ...projekt, |
| 335 | program: { ...projekt.program, method: 'manuell', manualCycleTime: 90 }, |
| 336 | }; |
| 337 | const pruefungen = programmPruefungen(projekt, STICHTAG); |
| 338 | expect(pruefungen.map((p) => p.umlaufzeit)).toEqual([60, 90]); |
| 339 | expect(pruefungen.map((p) => p.verfahren)).toEqual(['manuell', 'manuell']); |
| 340 | }); |
| 341 | }); |
| 342 | |
| 343 | describe('Programmbibliothek - in der Datei', () => { |
| 344 | it('trägt die Programme über Speichern und Laden', () => { |
| 345 | let projekt = basis(); |
| 346 | const k1 = projekt.signalGroups[0]!.id; |
| 347 | const phase = projekt.phases[0]!.id; |
| 348 | projekt = legeProgrammAn(projekt, 'Nacht'); |
| 349 | projekt = actions.setDemand(projekt, k1, { volume: 130 }); |
| 350 | projekt = actions.updatePhase(projekt, phase, { manualGreen: 9 }); |
| 351 | projekt = wechsleProgramm(projekt, ERSTES_PROGRAMM); |
| 352 | |
| 353 | const geladen = loadProject(JSON.parse(JSON.stringify(projekt)) as unknown, STICHTAG); |
| 354 | expect(geladen.project.schemaVersion).toBe(CURRENT_SCHEMA_VERSION); |
| 355 | expect(programmanzahl(geladen.project)).toBe(2); |
| 356 | const nacht = geladen.project.programmbibliothek[0]; |
| 357 | expect(nacht?.programm.name).toBe('Nacht'); |
| 358 | expect(nacht?.demands.find((d) => d.signalGroupId === k1)?.volume).toBe(130); |
| 359 | expect(nacht?.festeFreigabezeiten).toEqual([{ phaseId: phase, sekunden: 9 }]); |
| 360 | expect(geladen.issues.filter((i) => i.path.startsWith('programm'))).toEqual([]); |
| 361 | }); |
| 362 | |
| 363 | it('macht aus einer Datei ohne Bibliothek genau ein Programm', () => { |
| 364 | // Eine Datei bis Schema 15 kennt weder Kennung noch Bibliothek. Sie muss |
| 365 | // ohne Beanstandung als Einprogramm-Projekt ankommen - alles andere waere |
| 366 | // ein Vorwurf an eine Datei, die nichts falsch macht. |
| 367 | const roh = JSON.parse(JSON.stringify(basis())) as Record<string, unknown>; |
| 368 | roh['schemaVersion'] = 15; |
| 369 | delete roh['programmbibliothek']; |
| 370 | delete roh['programmplatz']; |
| 371 | delete (roh['program'] as Record<string, unknown>)['id']; |
| 372 | |
| 373 | const geladen = loadProject(roh, STICHTAG); |
| 374 | expect(geladen.project.program.id).toBe(ERSTES_PROGRAMM); |
| 375 | expect(geladen.project.programmbibliothek).toEqual([]); |
| 376 | expect(geladen.project.programmplatz).toBe(0); |
| 377 | expect(geladen.issues.filter((i) => i.severity !== 'hinweis')).toEqual([]); |
| 378 | }); |
| 379 | |
| 380 | it('ersetzt eine doppelt vergebene Programmkennung, statt das Programm zu verwerfen', () => { |
| 381 | /* |
| 382 | * Zwei gleiche Kennungen liessen die Reihenfolge entscheiden, welches |
| 383 | * Programm gemeint ist - der Bearbeiter saehe die Verkehrsstaerken eines |
| 384 | * anderen. Ein ganzes Tagesprogramm stillschweigend fallen zu lassen waere |
| 385 | * aber der groessere Verlust; deshalb wird umbenannt und gemeldet. |
| 386 | */ |
| 387 | const roh = JSON.parse(JSON.stringify(legeProgrammAn(basis(), 'Nacht'))) as Record< |
| 388 | string, |
| 389 | unknown |
| 390 | >; |
| 391 | const bibliothek = roh['programmbibliothek'] as Record<string, unknown>[]; |
| 392 | const aktiveKennung = (roh['program'] as Record<string, unknown>)['id']; |
| 393 | (bibliothek[0]!['programm'] as Record<string, unknown>)['id'] = aktiveKennung; |
| 394 | |
| 395 | const geladen = loadProject(roh, STICHTAG); |
| 396 | expect(programmanzahl(geladen.project)).toBe(2); |
| 397 | const kennungen = programmfolge(geladen.project).map((e) => e.satz.programm.id); |
| 398 | expect(new Set(kennungen).size, 'jede Kennung genau einmal').toBe(2); |
| 399 | expect(geladen.issues.some((i) => i.message.includes('mehrfach vergeben'))).toBe(true); |
| 400 | }); |
| 401 | }); |