IMPORT-POLISH: 4 Importer-Fixes nach Re-Import #2
FIX-1 decision-matching: apply_conflict_decisions/apply_dob_remaps nutzen jetzt canon_pair(name)[0] als Match-Key (Dedup-Identitaet: call-name ohne Zucht, v.d.<->von den gefaltet). Workaround-Spelling v.d. in Victoria Welbys Decision bleibt erhalten; beide Formen matchen jetzt. Kommentar im decision-Eintrag aktualisiert. FIX-2 specific-wins: _alleles_compatible aendert '? vs x = False' -> '? vs x = True' (spezifischer Wert gewinnt). C- vs CC, G- vs Gg, P? vs PP sind kein Konflikt mehr. Echte Wert-Widersprueche (DD vs Dd, Ee vs ee, PP vs Pp) bleiben Konflikte. Loest Enya, Ella, Zac automatisch (Konflikte 8->5 erwartet). 2 bestehende Tests angepasst, 7 neue Tests. FIX-3 parent-FK backfill: nach dem Wurfchronik-Rueckverknuepfungs- Block iteriert ImportService.RunAsync ueber bereits importierte Wuerfe mit null Father/MotherId und setzt fehlende FKs wenn das Elterntier jetzt ladbar ist. Trockenlauf zaehlt, Execute schreibt. LitterSummary.ParentFksBackfilled + 2 neue C#-Tests (SQLite). FIX-4 Skarlett-Artefakt: parse_detail() strippt trailing / +YEAR aus dem Genotyp-Tail (re.sub). Sterbejahr bleibt als death-Date erhalten -> Skarlett erscheint als reiner Sterbedatum-Konflikt. 2 neue Python-Tests. Gate: 124/124 C#-Tests, Python test_extract/test_genotype ALL PASS, has-pending-model-changes = No. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -321,6 +321,116 @@ namespace GerbilManager.Tests
|
||||
Assert.Equal("aa Ccchm ?? eef ?? ?? ?? ??", ImportService.ComposeGenotype(g));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParentFkBackfill_fills_null_litter_parent_on_reimport()
|
||||
{
|
||||
// Run 1: litter "Wurf A" has sire "Vater" (conflict=true — not loaded) and dam "Mutter"
|
||||
// (conflict=false — loaded). After run 1: litter.FatherId = null.
|
||||
// Run 2: sire "Vater" is no longer in conflict. Backfill must set litter.FatherId.
|
||||
var dir = Path.Combine(Path.GetTempPath(), "backfill-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
using var conn = new SqliteConnection("DataSource=:memory:");
|
||||
conn.Open();
|
||||
try
|
||||
{
|
||||
var littersJson = """
|
||||
[{"id":"L-A","litterId":"A","date":"01.05.2023","damName":"Mutter [ZdkC]","sireName":"Vater [ZdkC]","totalBorn":3,"zuchtnummer":"","note":""}]
|
||||
""";
|
||||
// Run 1: Vater is in conflict -> not loaded
|
||||
var animals1 = """
|
||||
[
|
||||
{"id":"mutter","name":"Mutter [ZdkC]","dob":"01.01.2021","death":"","farbschlag":"","gender":"female","zuchtCanon":"kleinechaote",
|
||||
"genotype":{"mapped8locus":{},"rawGenotype":"","unmappedTokens":[]},"conflict":false},
|
||||
{"id":"vater","name":"Vater [ZdkC]","dob":"02.02.2021","death":"","farbschlag":"","gender":"male","zuchtCanon":"kleinechaote",
|
||||
"genotype":{"mapped8locus":{},"rawGenotype":"","unmappedTokens":[]},"conflict":true},
|
||||
{"id":"kind","name":"Kind [ZdkC]","dob":"01.05.2023","death":"","farbschlag":"","gender":null,"zuchtCanon":"kleinechaote",
|
||||
"genotype":{"mapped8locus":{},"rawGenotype":"","unmappedTokens":[]},"conflict":false,
|
||||
"litterRef":{"litterId":"L-A","method":"geburtsdatum+eltern","confidence":"hoch"}}
|
||||
]
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(dir, "litters.json"), littersJson);
|
||||
File.WriteAllText(Path.Combine(dir, "animals.json"), animals1);
|
||||
|
||||
var opts = new DbContextOptionsBuilder<ApplicationContext>().UseSqlite(conn).Options;
|
||||
using var db = new ApplicationContext(opts);
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
|
||||
var report1 = await new ImportService(db, dir, dir).RunAsync(execute: true);
|
||||
Assert.Equal(0, report1.Litters.ParentFksBackfilled);
|
||||
var litter1 = await db.Litters.SingleAsync(l => l.Name == "Wurf A");
|
||||
Assert.Null(litter1.FatherId); // Vater was quarantined -> null FK
|
||||
Assert.NotNull(litter1.MotherId); // Mutter was loaded -> set
|
||||
|
||||
// Run 2: Vater is now conflict=false
|
||||
var animals2 = """
|
||||
[
|
||||
{"id":"mutter","name":"Mutter [ZdkC]","dob":"01.01.2021","death":"","farbschlag":"","gender":"female","zuchtCanon":"kleinechaote",
|
||||
"genotype":{"mapped8locus":{},"rawGenotype":"","unmappedTokens":[]},"conflict":false},
|
||||
{"id":"vater","name":"Vater [ZdkC]","dob":"02.02.2021","death":"","farbschlag":"","gender":"male","zuchtCanon":"kleinechaote",
|
||||
"genotype":{"mapped8locus":{},"rawGenotype":"","unmappedTokens":[]},"conflict":false},
|
||||
{"id":"kind","name":"Kind [ZdkC]","dob":"01.05.2023","death":"","farbschlag":"","gender":null,"zuchtCanon":"kleinechaote",
|
||||
"genotype":{"mapped8locus":{},"rawGenotype":"","unmappedTokens":[]},"conflict":false,
|
||||
"litterRef":{"litterId":"L-A","method":"geburtsdatum+eltern","confidence":"hoch"}}
|
||||
]
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(dir, "animals.json"), animals2);
|
||||
|
||||
var report2 = await new ImportService(db, dir, dir).RunAsync(execute: true);
|
||||
Assert.Equal(1, report2.Litters.ParentFksBackfilled); // backfill happened
|
||||
var vater = await db.Gerbils.SingleAsync(g => g.ExternalRef == "vater");
|
||||
var litter2 = await db.Litters.SingleAsync(l => l.Name == "Wurf A");
|
||||
Assert.Equal(vater.Id, litter2.FatherId); // FK now set
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { Directory.Delete(dir, recursive: true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ParentFkBackfill_dry_run_counts_without_writing()
|
||||
{
|
||||
// Dry-run on a DB with an existing null-parent litter should predict the backfill count.
|
||||
var dir = Path.Combine(Path.GetTempPath(), "backfill-dr-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
using var conn = new SqliteConnection("DataSource=:memory:");
|
||||
conn.Open();
|
||||
try
|
||||
{
|
||||
var littersJson = """
|
||||
[{"id":"L-B","litterId":"B","date":"15.06.2023","damName":"Mami [ZdkC]","sireName":"Papi [ZdkC]","totalBorn":2,"zuchtnummer":"","note":""}]
|
||||
""";
|
||||
var animals1 = """
|
||||
[
|
||||
{"id":"mami","name":"Mami [ZdkC]","dob":"03.03.2021","death":"","farbschlag":"","gender":"female","zuchtCanon":"kleinechaote",
|
||||
"genotype":{"mapped8locus":{},"rawGenotype":"","unmappedTokens":[]},"conflict":false},
|
||||
{"id":"papi","name":"Papi [ZdkC]","dob":"04.04.2021","death":"","farbschlag":"","gender":"male","zuchtCanon":"kleinechaote",
|
||||
"genotype":{"mapped8locus":{},"rawGenotype":"","unmappedTokens":[]},"conflict":true}
|
||||
]
|
||||
""";
|
||||
File.WriteAllText(Path.Combine(dir, "litters.json"), littersJson);
|
||||
File.WriteAllText(Path.Combine(dir, "animals.json"), animals1);
|
||||
|
||||
var opts = new DbContextOptionsBuilder<ApplicationContext>().UseSqlite(conn).Options;
|
||||
using var db = new ApplicationContext(opts);
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
await new ImportService(db, dir, dir).RunAsync(execute: true); // run 1
|
||||
|
||||
// Run 2 dry-run with papi un-quarantined
|
||||
var animals2 = animals1.Replace("\"conflict\":true", "\"conflict\":false");
|
||||
File.WriteAllText(Path.Combine(dir, "animals.json"), animals2);
|
||||
var dry = await new ImportService(db, dir, dir).RunAsync(execute: false);
|
||||
|
||||
Assert.Equal(1, dry.Litters.ParentFksBackfilled); // predicted but not written
|
||||
var litter = await db.Litters.SingleAsync(l => l.Name == "Wurf B");
|
||||
Assert.Null(litter.FatherId); // not written in dry-run
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { Directory.Delete(dir, recursive: true); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("01.02.2020", 2020, 2, 1)]
|
||||
[InlineData("5.3.21", 2021, 3, 5)]
|
||||
|
||||
Reference in New Issue
Block a user