feat(import): RPRO3-Dubletten-Entscheidungen der Züchterin als Override

Manuelle "gleich/verschieden?"-Entscheidungen aus den rpro3-import-Tickets
fließen jetzt in den RennmausPro-III-Import ein:

- Rpro3Decisions (Modell + JSON-Loader): same (Force-Merge), different
  (Force-Split), fields (Farbe/DOB/Herkunft/resident/Notiz). Schlüssel = rid.
- Rpro3Dedup.Run(animals, decisions): honoriert die Overrides nach dem
  automatischen Dedup (eine rid zieht ihren Cluster mit).
- Rpro3ImportService: lädt Import/Rpro3/rpro3-decisions.json, wendet
  Feld-Overrides im Plan-Builder an.
- rpro3-decisions.json: 14 bestätigte Entscheidungen (Eiji, Momo, Samuel,
  Female, Kennedy, Fegur, Tuli, Bura, Mister X, Zoey, Max, Akiro, Merlin,
  Snickers).
- tools/import/rpro3_lookup.py: Triage-Helfer (Farbe/Gencode/Eltern/
  Nachzucht+Partner aus _rpro3.db) für die Rückfragen an die Züchterin.
- 4 neue Tests (Force-Merge/Force-Split/Loader); Suite 268 grün.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 11:26:50 +02:00
parent bbd305861f
commit a333bce1b3
7 changed files with 699 additions and 4 deletions

View File

@@ -89,6 +89,73 @@ namespace GerbilManager.Tests
Assert.Equal(2, r.Placeholders.Count);
}
[Fact]
public void Dedup_force_merges_variants_declared_same_by_breeder()
{
// Zwei Varianten, die der Auto-Dedup wegen Farbkonflikt getrennt lässt …
var animals = new List<Rpro3Animal>
{
Ext("u1", "Akiro", new DateOnly(2008, 2, 5), "Polarfuchs", "Privatzucht"), // Variante B
Ext("u2", "Akiro", new DateOnly(2008, 2, 6), "Polarfuchs, hell", "unbekannt"), // Variante C
};
// ohne Entscheidung: getrennt (Farbe + DOB unterschiedlich → Konflikt)
Assert.Empty(Rpro3Dedup.Run(animals).MergeClusters);
// … werden per „Same"-Entscheidung der Züchterin zusammengelegt.
var decisions = new Rpro3Decisions
{
Decisions = { new Rpro3Decision { Name = "Akiro", Same = { new() { "u1", "u2" } } } }
};
var r = Rpro3Dedup.Run(animals, decisions);
Assert.Single(r.MergeClusters);
Assert.Equal(2, r.MergeClusters.Values.First().Count);
Assert.Equal(r.RidToRoot["u1"], r.RidToRoot["u2"]);
}
[Fact]
public void Dedup_force_splits_variants_declared_different_by_breeder()
{
// Zwei Datensätze, die der Auto-Dedup zusammenlegen würde (kompatibel) …
var animals = new List<Rpro3Animal>
{
Ext("u1", "Max", new DateOnly(2013, 2, 1), "Marder", "Clan A"),
Ext("u2", "Max", new DateOnly(2013, 2, 1), "Marder", "Clan A"),
};
Assert.Single(Rpro3Dedup.Run(animals).MergeClusters);
// … bleiben durch „Different" getrennt.
var decisions = new Rpro3Decisions
{
Decisions = { new Rpro3Decision { Name = "Max", Different = { new() { "u1" }, new() { "u2" } } } }
};
var r = Rpro3Dedup.Run(animals, decisions);
Assert.Empty(r.MergeClusters);
Assert.NotEqual(r.RidToRoot["u1"], r.RidToRoot["u2"]);
}
[Fact]
public void Rpro3Decisions_load_returns_empty_when_file_missing()
{
var d = Rpro3Decisions.Load(Path.Combine(Path.GetTempPath(), "does-not-exist-" + Guid.NewGuid().ToString("N") + ".json"));
Assert.Empty(d.Decisions);
}
[Fact]
public void Rpro3Decisions_roundtrips_through_json()
{
var path = Path.Combine(Path.GetTempPath(), "rpro3-dec-" + Guid.NewGuid().ToString("N") + ".json");
File.WriteAllText(path,
"{\"decisions\":[{\"name\":\"Bura\",\"same\":[[\"228\",\"u2128\"]],\"fields\":{\"228\":{\"origin\":\"Sarah Wörz\"}}}]}");
try
{
var d = Rpro3Decisions.Load(path);
Assert.Single(d.Decisions);
Assert.Equal("Sarah Wörz", d.BuildFieldIndex()["228"].Origin);
Assert.Equal(new[] { "228", "u2128" }, d.SameGroups().First());
}
finally { File.Delete(path); }
}
// ── Integration: Reader + Execute gegen eine Mini-RPRO3-DB ────
[Fact]