blob: bd17d79a2071c405ab7b1633ad056956393b4195 [file] [log] [blame]
package eu.mulk.aendggner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import eu.mulk.aendggner.aenderung.Aenderungsbefehl.UnbekannterBefehl;
import eu.mulk.aendggner.aenderung.parse.AenderungsgesetzParser;
import eu.mulk.aendggner.aenderung.parse.PatchTextExtraktor;
import eu.mulk.aendggner.aenderung.parse.TextBereiniger;
import eu.mulk.aendggner.anwendung.BefehlAnwender;
import eu.mulk.aendggner.gesetz.gii.GiiXmlLoader;
import eu.mulk.aendggner.synopse.HtmlRenderer;
import eu.mulk.aendggner.synopse.SynopseBuilder;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
/**
* Smoke-Test der Gesamtpipeline auf den IfSG-Beispieldaten (nicht eingecheckt; der Test wird
* übersprungen, wenn sie fehlen).
*
* <p>Achtung: Das Beispiel-XML ist bereits konsolidiert (Stand 18.11.2020, enthält die
* Art.-1-Änderungen des Beispiel-Änderungsgesetzes schon). Die Assertions prüfen deshalb
* Parse-Zahlen und sauberes Reporting, nicht die Diff-Korrektheit.
*/
class EndToEndTest {
private static final Path SAMPLEDATA = Path.of("src/main/resources/sampledata");
private static final Path STAMMGESETZ = SAMPLEDATA.resolve("IfSG/BJNR104510000.xml");
private static final Path AENDERUNGSGESETZ = SAMPLEDATA.resolve("IfSG/bgbl120s2397_78991.pdf");
@Test
void gesamtePipelineAufIfSgBeispiel() throws Exception {
assumeTrue(Files.exists(STAMMGESETZ), "IfSG-Beispieldaten fehlen");
assumeTrue(Files.exists(AENDERUNGSGESETZ), "BGBl-Beispiel-PDF fehlt");
// 1. Stammgesetz laden.
var gesetz = new GiiXmlLoader().load(STAMMGESETZ);
assertThat(gesetz.jurabk()).isEqualTo("IfSG");
assertThat(gesetz.normen()).hasSize(86);
assertThat(gesetz.norm("§ 5").orElseThrow().absaetze()).hasSize(8);
// 2. Änderungsgesetz-Text extrahieren.
var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(AENDERUNGSGESETZ));
assertThat(text).contains("wird wie folgt geändert");
assertThat(text).contains("Nach § 28 wird folgender § 28a eingefügt");
// 3. Befehle parsen: Artikel 1 und 2 betreffen das IfSG.
var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
assertThat(parseErgebnis.artikel()).containsExactly("1", "2");
assertThat(parseErgebnis.befehle().size()).isGreaterThanOrEqualTo(50);
var typisiert =
parseErgebnis.befehle().stream().filter(b -> !(b instanceof UnbekannterBefehl)).count();
assertThat(typisiert).isGreaterThanOrEqualTo(40);
// Das Original-BGBl enthält ein überzähliges Anführungszeichen (Artikel 3) — muss als
// Warnung gemeldet werden, nicht als Abbruch.
assertThat(parseErgebnis.warnungen()).isNotEmpty();
// 4. Anwenden: wirft nicht; jeder Befehl erhält einen Protokolleintrag.
var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
assertThat(anwendung.protokoll()).hasSameSizeAs(parseErgebnis.befehle());
assertThat(anwendung.anzahlAngewandt()).isGreaterThan(0);
// 5. Synopse rendern.
var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false);
var html = HtmlRenderer.rendere(synopse, "E2E-Test");
assertThat(html).contains("Alte Fassung").contains("Neue Fassung");
assertThat(html).contains("Manuell prüfen");
assertThat(html).containsAnyOf("<del>", "<ins>");
}
/** Neues digitales BGBl-Format (recht.bund.de, ab 2023): 3. UWGÄndG 2026. */
@Test
void uwgNeuesBgblFormat() throws Exception {
var xml = SAMPLEDATA.resolve("UWG/BJNR141400004.xml");
var pdf = SAMPLEDATA.resolve("UWG/bgbl126s0043_regelungstext.pdf");
assumeTrue(Files.exists(xml) && Files.exists(pdf), "UWG-Beispieldaten fehlen");
var gesetz = new GiiXmlLoader().load(xml);
var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
// Der Fontgrößen-Filter muss die Fußnotenblöcke entfernt haben, die im neuen Format sonst
// mitten im Gesetzestext landen.
assertThat(text).doesNotContain("Dieses Gesetz dient der Umsetzung der Richtlinie (EU)");
assertThat(text).contains("wird wie folgt geändert");
var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
assertThat(parseErgebnis.artikel()).containsExactly("1");
assertThat(parseErgebnis.befehle().size()).isGreaterThanOrEqualTo(10);
var typisiert =
parseErgebnis.befehle().stream().filter(b -> !(b instanceof UnbekannterBefehl)).count();
assertThat(typisiert).isGreaterThan(parseErgebnis.befehle().size() / 2);
var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false);
assertThat(HtmlRenderer.rendere(synopse, "E2E-Test")).contains("Neue Fassung");
}
/** Stresstest: GEG-Novelle 2023 („Heizungsgesetz“, 26 Seiten, >100 Befehle). */
@Test
void gegGrossesAenderungsgesetz() throws Exception {
var xml = SAMPLEDATA.resolve("GEG/BJNR172810020.xml");
var pdf = SAMPLEDATA.resolve("GEG/bgbl123s0280_regelungstext.pdf");
assumeTrue(Files.exists(xml) && Files.exists(pdf), "GEG-Beispieldaten fehlen");
var gesetz = new GiiXmlLoader().load(xml);
var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
assertThat(parseErgebnis.artikel()).contains("1");
assertThat(parseErgebnis.befehle().size()).isGreaterThanOrEqualTo(100);
// Das XML ist bereits konsolidiert; entscheidend ist, dass die Anwendung sauber terminiert
// und jeden Befehl protokolliert.
var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
assertThat(anwendung.protokoll()).hasSameSizeAs(parseErgebnis.befehle());
var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false);
assertThat(HtmlRenderer.rendere(synopse, "E2E-Test")).contains("Manuell prüfen");
}
/**
* Gesetzentwurf (Bundestags-Drucksache mit markerloser Silbentrennung). Das AGG-XML ist NICHT
* konsolidiert (Stand 2023, Entwurf von 2026) — hier entstehen echte Diffs.
*/
@Test
void aggRegierungsentwurfMitEchtenDiffs() throws Exception {
var xml = SAMPLEDATA.resolve("AGG/BJNR189710006.xml");
var pdf = SAMPLEDATA.resolve("AGG/BT-Drs-21-6178_Regierungsentwurf.pdf");
assumeTrue(Files.exists(xml) && Files.exists(pdf), "AGG-Beispieldaten fehlen");
var gesetz = new GiiXmlLoader().load(xml);
var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
// Markerlose Silbentrennung („Schwel|lenwertes“) muss zusammengezogen sein.
assertThat(text).contains("Schwellenwertes");
var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
assertThat(parseErgebnis.artikel()).containsExactly("1");
// Der Begründungsteil darf keine Befehle erzeugen.
assertThat(parseErgebnis.befehle().size()).isBetween(15, 40);
var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
// Inkl. der Mehrfachziel-„jeweils“-Befehle (§ 3, § 20, § 30) werden hier ~22 Befehle
// angewandt; nur die zwei Sonderfälle (positionaler Lokator, Verbundbefehl) bleiben manuell.
assertThat(anwendung.anzahlAngewandt()).isGreaterThanOrEqualTo(20);
assertThat(anwendung.anzahlManuell()).isLessThanOrEqualTo(3);
var synopse = SynopseBuilder.baue(gesetz, anwendung, parseErgebnis.warnungen(), false);
var html = HtmlRenderer.rendere(synopse, "E2E-Test");
assertThat(html).contains("<del>").contains("<ins>");
// Stichprobe aus Artikel 1 Nummer 1: „Alters“ → „Lebensalters“ in § 1.
assertThat(html).contains("<ins>Lebensalters</ins>");
// Mehrfachziel-Einfügung (§ 30 Absatz 2 Satz 1 und Absatz 3): „Bildung,“ vor „Familie“.
assertThat(html).contains("<ins>Bildung,");
// Mehrfachziel-Streichung (§ 3 Absatz 1 Satz 2 und Absatz 4): gezielt entfernt — der
// untargetierte Absatz 5 behält die Angabe.
var norm3 = anwendung.neu().norm("§ 3").orElseThrow();
var absatz1 =
norm3.absaetze().stream().filter(a -> "1".equals(a.nummer())).findFirst().orElseThrow();
assertThat(absatz1.text()).doesNotContain("in Bezug auf § 2 Abs. 1 Nr. 1 bis 4");
var absatz5 =
norm3.absaetze().stream().filter(a -> "5".equals(a.nummer())).findFirst().orElseThrow();
assertThat(absatz5.text()).contains("in Bezug auf § 2 Abs. 1 Nr. 1 bis 4");
}
/**
* Regierungsentwurf, dessen Artikel 1 ein Ablösegesetz ist; nur Artikel 2 ändert das ProdHaftG.
*/
@Test
void prodHaftGRegierungsentwurf() throws Exception {
var xml = SAMPLEDATA.resolve("ProdHaftG/BJNR021980989.xml");
var pdf = SAMPLEDATA.resolve("ProdHaftG/RegE_Produkthaftungsrecht_2025-12-17.pdf");
assumeTrue(Files.exists(xml) && Files.exists(pdf), "ProdHaftG-Beispieldaten fehlen");
var gesetz = new GiiXmlLoader().load(xml);
var text = TextBereiniger.bereinige(new PatchTextExtraktor().extrahiere(pdf));
var parseErgebnis = new AenderungsgesetzParser().parse(text, gesetz, null);
// Artikel 1 (neues Produkthaftungsgesetz) betrifft das Stammgesetz nicht; Artikel 2 schon.
assertThat(parseErgebnis.artikel()).containsExactly("2");
assertThat(parseErgebnis.befehle()).hasSize(1);
var anwendung = BefehlAnwender.anwenden(gesetz, parseErgebnis.befehle());
assertThat(anwendung.anzahlAngewandt()).isEqualTo(1);
var neueNorm = anwendung.neu().norm("§ 19").orElseThrow();
assertThat(neueNorm.titel()).isEqualTo("Außerkrafttreten");
assertThat(neueNorm.gesamtText()).contains("9. Dezember 2026 außer Kraft");
}
}