fix(tickets): 16 Tickets der Züchterin — Genetik, Importer, Daten, Akte
All checks were successful
CI / Frontend Tests (Node/Vite) (push) Successful in 9m38s
CI / Backend Tests (.NET) (push) Successful in 1m14s
CI / Docker Build & Push (push) Successful in 1m33s
CI / Deploy auf TrueNAS (Custom App) (push) Successful in 8s

Genetik-Engine (bde4ec70, f89e95ad, 2322c2a8):
- Saphir/Platin/Platin-Hell unterscheiden sich NUR in der C-Zygotie und waren im
  Katalog identisch (locusToken reduziert auf das dominante Allel) → Saphir war
  unerreichbar. Token darf jetzt ein exaktes, ungeordnetes Allelpaar "x/y"
  verlangen: Platin C/C, Saphir C/cchm, Platin-Hell C/ch. TS- und Python-Mirror
  identisch, Seeds regeneriert, Migration ReseedColorVarietiesGen6Saphir.
- c[hm] wird als Alias auf c[chm] normalisiert (Tippfehler in zwei Charts) —
  vorher war Jays Gencode unparsebar und ergab "Zobel-Hell".
- 6 Tiere wechseln den errechneten Farbschlag (5x Platin→Saphir, Jay→Zobel).

Importer-Logik:
- renameTo-Notiz-Sweep: der alte Name blieb in Wurf-Notizen stehen ("Blacky +
  Kruke") — jetzt wortgenau ersetzt, mit vier Guards gegen Kollateralschaden.
- Neuer Resolution-Schlüssel goHomeDate (autoritativ, zieht GivenAway nach) und
  spottingType; Gencode-Overrides gewinnen jetzt bei präzisem Match (vorher
  wurden Entscheidungen stumm verschluckt, z. B. Eliza und Chris).
- parse_date verwirft implausible Jahre (JackJack hatte 1310-05-13).
- extract.py erkennt Stammbaum-Blöcke auch ohne Stern vor dem Geburtsdatum
  (Bijou bekam dadurch den Nachbar-Ast als Mutter).
- Backend-Ingest überträgt SpottingType (nur bei Payload-Wert).

Daten (conflict-decisions.json, re-ingest-stabil): Kuke-Merge + Zuchtname,
Jamie- und Sakura-Dubletten, Phantom-Tier "Unbekannt", Eltern von Kathlin,
Fast Boy/Ziwa und dem Q4-/TS-Wurf, Eliza-Gencode, Merle-Abgabe, JackJack.

Frontend (24522f5f): Abgabedatum steht in der Tier-Akte jetzt direkt unter dem
Abnehmer und hängt nicht mehr am Status.

Tests: +25 Checks test_merge_resolve, +7 test_genotype, +8 genetics.test.ts,
neuer Extract- und Ingest-Test, e2e GOHOME-ROW. Alles grün.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-19 23:31:48 +02:00
parent bf074696b4
commit 5a6d1c2541
22 changed files with 2832 additions and 182 deletions

View File

@@ -3,6 +3,7 @@ using GerbilManagerWebAPI.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System.Text.Json;
using System.Text.Json.Nodes;
namespace GerbilManager.Tests
{
@@ -286,5 +287,56 @@ namespace GerbilManager.Tests
Assert.NotNull(father.DateOfDeath); // Todesdatum aus Override angewandt
Assert.Equal(GerbilStatus.Deceased, father.Status); // ... und Status daraus abgeleitet
}
/// <summary>
/// Setzt bzw. entfernt die Scheckungsart im gestageten Payload (Testdaten werden hier
/// selbst aufgebaut — der Test hängt bewusst an keiner echten conflict-decisions.json).
/// </summary>
private void SetPayloadSpottingType(string? value, bool remove = false)
{
var root = JsonNode.Parse(File.ReadAllText(_resolvedJsonPath))!;
var papa = root["Gerbils"]!.AsArray()
.First(g => (string?)g!["Name"] == "Papa")!.AsObject();
if (remove) papa.Remove("SpottingType");
else papa["SpottingType"] = value;
File.WriteAllText(_resolvedJsonPath, root.ToJsonString());
}
[Fact]
public async Task IngestResolved_writes_spotting_type_and_keeps_it_when_payload_has_none()
{
// Ticket a547be62 (Eliza): Die Scheckungsart kennt der Importer nur über
// conflict-decisions.json — sie MUSS also durch den Ingest getragen werden.
// Umgekehrt darf ein leerer Payload-Wert eine von Hand in der App erfasste
// Scheckungsart NICHT nullen (der Importer weiß von ihr nichts).
using var db = NewDb();
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?> { { "Import:SourcePath", _dir } })
.Build();
// (1) Neues Tier: Wert aus dem Payload landet in der DB.
SetPayloadSpottingType("Ansatzschecke");
await new IngestResolvedService(db, config, null!).RunAsync();
var father = await db.Gerbils.SingleAsync(g => g.Id == FatherId);
Assert.Equal("Ansatzschecke", father.SpottingType);
// (2) Re-Ingest, Feld fehlt im Payload → bestehender Wert bleibt stehen.
SetPayloadSpottingType(null, remove: true);
await new IngestResolvedService(db, config, null!).RunAsync();
father = await db.Gerbils.SingleAsync(g => g.Id == FatherId);
Assert.Equal("Ansatzschecke", father.SpottingType);
// (3) Re-Ingest mit explizitem null → ebenfalls kein Nullen (Guard im Update-Zweig).
SetPayloadSpottingType(null);
await new IngestResolvedService(db, config, null!).RunAsync();
father = await db.Gerbils.SingleAsync(g => g.Id == FatherId);
Assert.Equal("Ansatzschecke", father.SpottingType);
// (4) Ein NEUER Wert aus dem Payload gewinnt weiterhin (Entscheidung korrigiert).
SetPayloadSpottingType("Mantelschecke");
await new IngestResolvedService(db, config, null!).RunAsync();
father = await db.Gerbils.SingleAsync(g => g.Id == FatherId);
Assert.Equal("Mantelschecke", father.SpottingType);
}
}
}