import { describe, expect, it } from 'vitest'; import { setConflict } from '@/app/actions'; import { validateProject } from '@/domain/validation/engine'; import { buildSignalPlan } from '@/domain/plan/signalPlan'; import { createStandardIntersectionProject } from '@/domain/model/factory'; import { parseProject } from '@/domain/model/schema'; import type { Conflict, Project } from '@/domain/model/project'; /* * Befund 2 - die verbliebenen Schreib- und Nachschlagstellen (Fassung 5.9.0). * * Der Befund war: Doppelte Konfliktkennungen wurden beim Einlesen nicht * bemerkt, und das Aendern einer Beziehung aenderte stumm eine zweite mit. * Die erste Behebung hat die Neuvergabe beim Einlesen eingebaut - damit sind Daten aus * `parseProject` entschaerft. `setConflict` sucht seine Beziehung aber ueber * das Paar (fromId, toId) und schreibt danach ueber die KENNUNG: zwei * Schluessel fuer dieselbe Entscheidung. Trifft die Kennung ein zweites Mal zu, * schreibt die Funktion an eine Beziehung, die sie nie nachgeschlagen hat. */ const DATUM = new Date('2026-01-01T00:00:00Z'); /** * Zwei Beziehungen mit derselben Kennung, aber verschiedenen Paaren. * * Von Hand zusammengesetzt: Genau so entsteht der Zustand auch im Betrieb - * aus einer Projektdatei fremder Herkunft oder einer Umwandlung, die an * `parseProject` vorbeifuehrt. */ function mitDoppelterKennung(): { projekt: Project; erste: Conflict; zweite: Conflict } { const roh = createStandardIntersectionProject('Doppelte Kennung', DATUM); const erste = roh.conflicts[0]!; const zweite = { ...roh.conflicts[1]!, id: erste.id }; return { projekt: { ...roh, conflicts: [erste, zweite] }, erste, zweite, }; } describe('setConflict schreibt ueber das Paar, nicht ueber die Kennung', () => { it('aendert nur die nachgeschlagene Beziehung', () => { const { projekt, erste, zweite } = mitDoppelterKennung(); const nachher = setConflict(projekt, zweite.fromId, zweite.toId, { clearingDistance: 44 }); const geaendert = nachher.conflicts.find( (c) => c.fromId === zweite.fromId && c.toId === zweite.toId, ); const unberuehrt = nachher.conflicts.find( (c) => c.fromId === erste.fromId && c.toId === erste.toId, ); expect(geaendert?.clearingDistance).toBe(44); // Bis hierher trug auch diese Beziehung 44 m - der Anwender hat einen Weg // eingetragen und zwei Zwischenzeiten veraendert. expect(unberuehrt?.clearingDistance).toBe(erste.clearingDistance); }); it('zieht den Herkunftsnachweis nur an der nachgeschlagenen Beziehung nach', () => { // Der Nachweis sagt aus, dass die Zeile im Ausdruck aus dem Lageplan // stammt. An der mitgeaenderten Beziehung war er ebenfalls entwertet - // ohne dass dort jemand etwas eingetragen haette. const { projekt, erste, zweite } = mitDoppelterKennung(); const nachher = setConflict(projekt, zweite.fromId, zweite.toId, { clearingDistance: 44 }); const unberuehrt = nachher.conflicts.find( (c) => c.fromId === erste.fromId && c.toId === erste.toId, ); expect(unberuehrt?.wegnachweis).toBe(erste.wegnachweis); }); it('legt eine noch nicht erfasste Beziehung weiterhin an', () => { const roh = createStandardIntersectionProject('Neu', DATUM); const k1 = roh.signalGroups.find((g) => g.name === 'K1')!; const k2 = roh.signalGroups.find((g) => g.name === 'K2')!; const nachher = setConflict(roh, k1.id, k2.id, { clearingDistance: 17 }); expect(nachher.conflicts).toHaveLength(roh.conflicts.length + 1); const neu = nachher.conflicts.find((c) => c.fromId === k1.id && c.toId === k2.id); expect(neu?.clearingDistance).toBe(17); }); }); /* * Die zweite Stelle des Befundes: Der Sprungknopf des Pruefberichts loest die * Notiz ueber `c.id === note.konfliktId` auf. Das traegt - aber nur, solange * die Kennungen eindeutig sind. Genau das ist hier festgehalten: Faellt die * Neuvergabe beim Einlesen weg, ist die Aufloesung ueber die Kennung nicht mehr * eindeutig, und der Bericht schickt den Anwender an die falsche Beziehung. */ describe('Voraussetzung der Aufloesung ueber die Kennung', () => { it('vergibt eine doppelte Konfliktkennung beim Einlesen neu', () => { const roh = createStandardIntersectionProject('Einlesen', DATUM); const rohDatei = JSON.parse(JSON.stringify(roh)) as Record; const konflikte = rohDatei['conflicts'] as Record[]; konflikte[1]!['id'] = konflikte[0]!['id']; const ergebnis = parseProject(rohDatei); expect(ergebnis.project).not.toBeNull(); const kennungen = (ergebnis.project?.conflicts ?? []).map((c) => c.id); expect(kennungen.length).toBeGreaterThan(1); expect(new Set(kennungen).size).toBe(kennungen.length); }); it('schickt den Sprungknopf an die Beziehung, aus der die Meldung stammt', () => { // Eine von Hand auf 1 s gekuerzte Zwischenzeit K1 nach K3: Die Meldung // traegt die Kennung dieser Beziehung, und das Sprungziel muss sie nennen. const roh = createStandardIntersectionProject('Sprungziel', DATUM); const k1 = roh.signalGroups.find((g) => g.name === 'K1')!; const k3 = roh.signalGroups.find((g) => g.name === 'K3')!; const projekt: Project = { ...roh, conflicts: roh.conflicts.map((c) => c.fromId === k1.id && c.toId === k3.id ? { ...c, manualIntergreen: 1 } : c, ), }; const gemeint = projekt.conflicts.find((c) => c.fromId === k1.id && c.toId === k3.id)!; const bericht = validateProject(projekt, buildSignalPlan(projekt), DATUM); const befund = bericht.findings.find( (f) => f.rule === 'signalplan.zwischenzeit-vorgabe-unter-rechenwert', ); expect(befund?.target?.kind).toBe('konflikt'); expect(befund?.target?.id).toBe(gemeint.id); expect(befund?.target?.label).toBe('K1 nach K3'); }); });