Compare commits
5 Commits
feature/ui
...
319aca7608
| Author | SHA1 | Date | |
|---|---|---|---|
| 319aca7608 | |||
| 4d9e901139 | |||
| 01781f23d6 | |||
| e1e088f041 | |||
| 358e30db1f |
@@ -82,8 +82,12 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
ResidencySummary? Residency = null);
|
ResidencySummary? Residency = null);
|
||||||
|
|
||||||
/// <summary>Bestand (resident) vs external pedigree ancestors; FlippedByParentRule = foreign-
|
/// <summary>Bestand (resident) vs external pedigree ancestors; FlippedByParentRule = foreign-
|
||||||
/// Zuchtname animals made resident because they parented a Clan offspring (rule b).</summary>
|
/// Zuchtname animals made resident because they parented a Clan offspring (rule b).
|
||||||
public sealed record ResidencySummary(int Resident, int External, int FlippedByParentRule);
|
/// HerkunftBackfilled = resident animals whose null OriginBreeder was filled (safe, no overwrite).
|
||||||
|
/// FarbschlagWouldRebackfill = already-imported animals with a stale ColorVariety that the
|
||||||
|
/// current extract would update (no-op counter until the safety mechanism is ratified).</summary>
|
||||||
|
public sealed record ResidencySummary(int Resident, int External, int FlippedByParentRule,
|
||||||
|
int HerkunftBackfilled = 0, int FarbschlagWouldRebackfill = 0);
|
||||||
|
|
||||||
public sealed record LitterSummary(int InSource, int Created, int AlreadyImported,
|
public sealed record LitterSummary(int InSource, int Created, int AlreadyImported,
|
||||||
int DerivedFromChart = 0, int DerivedSkipped = 0, int ParentFksDropped = 0,
|
int DerivedFromChart = 0, int DerivedSkipped = 0, int ParentFksDropped = 0,
|
||||||
|
|||||||
@@ -137,12 +137,14 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
// name+DOB -> gid index, across EXISTING rows AND this run's planned animals, so that
|
// name+DOB -> gid index, across EXISTING rows AND this run's planned animals, so that
|
||||||
// chart-position parentRefs (PEDIGREE-LINK) can resolve a parent to a real gerbil id.
|
// chart-position parentRefs (PEDIGREE-LINK) can resolve a parent to a real gerbil id.
|
||||||
var existingRows = await _db.Gerbils
|
var existingRows = await _db.Gerbils
|
||||||
.Select(g => new { g.Id, g.Name, g.DateOfBirth, g.ExternalRef, g.LitterId }).ToListAsync();
|
.Select(g => new { g.Id, g.Name, g.DateOfBirth, g.ExternalRef, g.LitterId, g.ColorVarietyId }).ToListAsync();
|
||||||
var gidByNameDob = new Dictionary<string, Guid>();
|
var gidByNameDob = new Dictionary<string, Guid>();
|
||||||
foreach (var g in existingRows)
|
foreach (var g in existingRows)
|
||||||
gidByNameDob[NameDobKey(g.Name, g.DateOfBirth)] = g.Id;
|
gidByNameDob[NameDobKey(g.Name, g.DateOfBirth)] = g.Id;
|
||||||
var existingLitterByExtRef = existingRows.Where(g => g.ExternalRef != null)
|
var existingLitterByExtRef = existingRows.Where(g => g.ExternalRef != null)
|
||||||
.ToDictionary(g => g.ExternalRef!, g => g.LitterId);
|
.ToDictionary(g => g.ExternalRef!, g => g.LitterId);
|
||||||
|
var existingColorVarietyByExtRef = existingRows.Where(g => g.ExternalRef != null)
|
||||||
|
.ToDictionary(g => g.ExternalRef!, g => g.ColorVarietyId);
|
||||||
|
|
||||||
// PASS 1: assign ids + resolve fb/gender/Wurfchronik link (no writes yet).
|
// PASS 1: assign ids + resolve fb/gender/Wurfchronik link (no writes yet).
|
||||||
var plan = new List<AnimalPlan>();
|
var plan = new List<AnimalPlan>();
|
||||||
@@ -169,7 +171,8 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
if (!exists) gidByNameDob.TryAdd(NameDobKey(a.Name, ParseDate(a.Dob)), gid);
|
if (!exists) gidByNameDob.TryAdd(NameDobKey(a.Name, ParseDate(a.Dob)), gid);
|
||||||
|
|
||||||
var currentLitter = exists && existingLitterByExtRef.TryGetValue(a.Id, out var el) ? el : null;
|
var currentLitter = exists && existingLitterByExtRef.TryGetValue(a.Id, out var el) ? el : null;
|
||||||
plan.Add(new AnimalPlan(a, gid, exists, wurfLitterId, currentLitter, colorVarietyId, gender));
|
var currentColorVarietyId = exists && existingColorVarietyByExtRef.TryGetValue(a.Id, out var ecv) ? ecv : null;
|
||||||
|
plan.Add(new AnimalPlan(a, gid, exists, wurfLitterId, currentLitter, colorVarietyId, gender, currentColorVarietyId));
|
||||||
}
|
}
|
||||||
|
|
||||||
// PASS 1.5: PEDIGREE-LINK — synthesize/reuse a litter from chart-position parentRefs for
|
// PASS 1.5: PEDIGREE-LINK — synthesize/reuse a litter from chart-position parentRefs for
|
||||||
@@ -460,6 +463,51 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
if (execute && parentFksBackfilled > 0) await _db.SaveChangesAsync();
|
if (execute && parentFksBackfilled > 0) await _db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FARBSCHLAG-RE-MATCH — no-op counter (safety mechanism pending god/Julian sign-off).
|
||||||
|
// Counts already-imported animals where the current extract matched a DIFFERENT
|
||||||
|
// ColorVariety than what is currently stored in the DB. Does NOT update any row.
|
||||||
|
int farbschlagWouldRebackfill = plan.Count(p =>
|
||||||
|
p.Exists && p.ColorVarietyId is not null && p.ColorVarietyId != p.CurrentColorVarietyId);
|
||||||
|
|
||||||
|
// HERKUNFT BACKFILL (fill-NULL-only, safe): sweep all resident animals whose
|
||||||
|
// OriginBreeder is null and fill it with a derived value or 'Zucht der Kleinen Chaoten'.
|
||||||
|
// NEVER overwrites a non-null OriginBreeder (Julian: "alle Schreibweisen unterstützen").
|
||||||
|
const string DefaultClanBreeder = "Zucht der Kleinen Chaoten";
|
||||||
|
int herkunftBackfilled = 0;
|
||||||
|
{
|
||||||
|
var needsHerkunft = await _db.Gerbils
|
||||||
|
.Where(g => g.OriginBreeder == null && g.IsResident)
|
||||||
|
.Select(g => new { g.Id, g.LitterId })
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var g in needsHerkunft)
|
||||||
|
{
|
||||||
|
herkunftBackfilled++;
|
||||||
|
if (execute)
|
||||||
|
{
|
||||||
|
// Prefer the OriginBreeder of an existing parent (father first, then mother).
|
||||||
|
string? derived = null;
|
||||||
|
if (g.LitterId is not null)
|
||||||
|
{
|
||||||
|
var parentIds = await _db.Litters
|
||||||
|
.Where(l => l.Id == g.LitterId)
|
||||||
|
.Select(l => new { l.FatherId, l.MotherId })
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
if (parentIds?.FatherId is Guid fid)
|
||||||
|
derived = await _db.Gerbils.Where(gb => gb.Id == fid && gb.OriginBreeder != null)
|
||||||
|
.Select(gb => gb.OriginBreeder).FirstOrDefaultAsync();
|
||||||
|
if (derived is null && parentIds?.MotherId is Guid mid)
|
||||||
|
derived = await _db.Gerbils.Where(gb => gb.Id == mid && gb.OriginBreeder != null)
|
||||||
|
.Select(gb => gb.OriginBreeder).FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
var row = await _db.Gerbils.FindAsync(g.Id);
|
||||||
|
if (row is not null && row.OriginBreeder is null)
|
||||||
|
row.OriginBreeder = derived ?? DefaultClanBreeder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (execute && herkunftBackfilled > 0) await _db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
if (littersWithoutDate > 0)
|
if (littersWithoutDate > 0)
|
||||||
notes.Add($"Würfe ohne Datum: {littersWithoutDate} Wurfchronik-Einträge ohne parsbares Geburtsdatum übersprungen (weder erstellt noch verknüpft).");
|
notes.Add($"Würfe ohne Datum: {littersWithoutDate} Wurfchronik-Einträge ohne parsbares Geburtsdatum übersprungen (weder erstellt noch verknüpft).");
|
||||||
notes.Add("Quarantäne (kein Import): Konflikte + Stubs ohne Geburtsdatum + unsichere Wurf-Zuordnungen — warten auf die Prüfung durch die Züchterin.");
|
notes.Add("Quarantäne (kein Import): Konflikte + Stubs ohne Geburtsdatum + unsichere Wurf-Zuordnungen — warten auf die Prüfung durch die Züchterin.");
|
||||||
@@ -469,6 +517,10 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
if (parentFksBackfilled > 0)
|
if (parentFksBackfilled > 0)
|
||||||
notes.Add($"Parent-FK-Backfill: {parentFksBackfilled} bereits importierte Würfe haben jetzt eine Eltern-Verknüpfung (Elternteil war zuvor in Quarantäne, jetzt geladen).");
|
notes.Add($"Parent-FK-Backfill: {parentFksBackfilled} bereits importierte Würfe haben jetzt eine Eltern-Verknüpfung (Elternteil war zuvor in Quarantäne, jetzt geladen).");
|
||||||
notes.Add($"Bestand/Herkunft: {residentTotal} im Bestand (Clan Kleine Chaoten), {externalTotal} externe Ahnen ({flippedByParentRule} davon über die Eltern-Regel als Bestand erkannt).");
|
notes.Add($"Bestand/Herkunft: {residentTotal} im Bestand (Clan Kleine Chaoten), {externalTotal} externe Ahnen ({flippedByParentRule} davon über die Eltern-Regel als Bestand erkannt).");
|
||||||
|
if (herkunftBackfilled > 0)
|
||||||
|
notes.Add($"Herkunft-Backfill: {herkunftBackfilled} Bestand-Tier(e) mit leerem Herkunft-Feld befüllt (Zucht der Kleinen Chaoten oder von Elternteil abgeleitet). Niemals überschrieben.");
|
||||||
|
if (farbschlagWouldRebackfill > 0)
|
||||||
|
notes.Add($"Farbschlag-Hinweis (kein Overwrite): {farbschlagWouldRebackfill} bereits importierte Tier(e) haben einen veralteten Farbschlag, den der aktuelle Extraktor korrigieren würde — Overwrite-Mechanismus ausstehend (god/Julian Freigabe).");
|
||||||
int conflictsResolvedByDecision = loadable.Count(a => a.ResolvedByDecision);
|
int conflictsResolvedByDecision = loadable.Count(a => a.ResolvedByDecision);
|
||||||
if (conflictsResolvedByDecision > 0)
|
if (conflictsResolvedByDecision > 0)
|
||||||
notes.Add($"Konfliktauflösungen: {conflictsResolvedByDecision} Tier(e) anhand von conflict-decisions.json un-quarantänet (Genotyp/Farbschlag der Züchterin ist maßgeblich).");
|
notes.Add($"Konfliktauflösungen: {conflictsResolvedByDecision} Tier(e) anhand von conflict-decisions.json un-quarantänet (Genotyp/Farbschlag der Züchterin ist maßgeblich).");
|
||||||
@@ -484,7 +536,8 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
Photos: new PhotoSummary(photosAttached, photosMissing),
|
Photos: new PhotoSummary(photosAttached, photosMissing),
|
||||||
Samples: samples,
|
Samples: samples,
|
||||||
Notes: notes,
|
Notes: notes,
|
||||||
Residency: new ResidencySummary(residentTotal, externalTotal, flippedByParentRule));
|
Residency: new ResidencySummary(residentTotal, externalTotal, flippedByParentRule,
|
||||||
|
HerkunftBackfilled: herkunftBackfilled, FarbschlagWouldRebackfill: farbschlagWouldRebackfill));
|
||||||
}
|
}
|
||||||
|
|
||||||
private T? Load<T>(string file)
|
private T? Load<T>(string file)
|
||||||
@@ -574,7 +627,8 @@ namespace GerbilManagerWebAPI.Import
|
|||||||
/// <summary>Per-animal plan computed before any write so synthesis can run in dry-run too.</summary>
|
/// <summary>Per-animal plan computed before any write so synthesis can run in dry-run too.</summary>
|
||||||
private sealed record AnimalPlan(
|
private sealed record AnimalPlan(
|
||||||
SourceAnimal A, Guid Gid, bool Exists, Guid? WurfLitterId,
|
SourceAnimal A, Guid Gid, bool Exists, Guid? WurfLitterId,
|
||||||
Guid? CurrentLitterId, Guid? ColorVarietyId, Gender Gender);
|
Guid? CurrentLitterId, Guid? ColorVarietyId, Gender Gender,
|
||||||
|
Guid? CurrentColorVarietyId = null);
|
||||||
|
|
||||||
/// <summary>A litter synthesized from chart-position parentRefs (PEDIGREE-LINK).</summary>
|
/// <summary>A litter synthesized from chart-position parentRefs (PEDIGREE-LINK).</summary>
|
||||||
private sealed record SynthLitter(Guid Id, Guid? Father, Guid? Mother, DateOnly? Date, string Confidence);
|
private sealed record SynthLitter(Guid Id, Guid? Father, Guid? Mother, DateOnly? Date, string Confidence);
|
||||||
|
|||||||
90
gerbil-manager-web/e2e/gehege-regression.spec.ts
Normal file
90
gerbil-manager-web/e2e/gehege-regression.spec.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* QA-REGRESSION-1: GEHEGE-RENAME Lock-in.
|
||||||
|
*
|
||||||
|
* Sichert das Verhalten von drei GEHEGE-RENAME-Änderungen ab, die leicht still
|
||||||
|
* brechen können (Redirects, Nav-Einträge, Landing-URL):
|
||||||
|
* (a) /becken/* → /gehege/* Redirect (bookmarks / externe Links)
|
||||||
|
* (b) Nav zeigt 'Gehege' + 🏜️, kein 'Becken', kein 'Start'-Eintrag
|
||||||
|
* (c) '/' (Index) landet auf /rennmaeuse (nicht mehr auf einer Startseite)
|
||||||
|
*
|
||||||
|
* Bereits abgedeckt (NICHT dupliziert):
|
||||||
|
* - gotoSection(de.nav.enclosures) navigiert zu Gehege-Heading: navigation.spec.ts:19-20
|
||||||
|
* - CRUD + 409 auf /gehege: becken-kontakte.spec.ts
|
||||||
|
* - Wurf-Link + Herkunft: tiere.spec.ts:101-128
|
||||||
|
*/
|
||||||
|
import { de, expect, test } from './fixtures'
|
||||||
|
|
||||||
|
// ── (a) /becken/* → /gehege/* Redirect ──────────────────────────────────────
|
||||||
|
|
||||||
|
test('GEHEGE-RENAME: /becken leitet auf /gehege weiter (bookmark-safe)', async ({ page }) => {
|
||||||
|
await page.goto('/becken')
|
||||||
|
// React Router BeckenRedirect navigiert → /gehege; Gehege-Heading muss sichtbar sein
|
||||||
|
await expect(page.getByRole('heading', { name: de.pages.becken.title, exact: true })).toBeVisible()
|
||||||
|
await expect(page).toHaveURL(/\/gehege$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('GEHEGE-RENAME: /becken/neu leitet auf /gehege/neu weiter (deep redirect)', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto('/becken/neu')
|
||||||
|
// Neues-Gehege-Formular muss sichtbar sein, URL muss /gehege/neu sein
|
||||||
|
await expect(page.getByRole('heading', { name: de.pages.becken.form?.createTitle ?? de.pages.becken.newButton })).toBeVisible({ timeout: 6_000 })
|
||||||
|
await expect(page).toHaveURL(/\/gehege\/neu$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── (b) Nav: 'Gehege' ✓, 'Becken' ✗, 'Start' ✗ ──────────────────────────────
|
||||||
|
|
||||||
|
test('GEHEGE-RENAME: Nav hat "Gehege"-Eintrag mit Terrarium-Icon, kein "Becken"', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto('/rennmaeuse')
|
||||||
|
const nav = page.getByRole('navigation', { name: de.nav.mainNavigation })
|
||||||
|
|
||||||
|
// Gehege-Link soll vorhanden sein (via „Mehr"-Blatt auf Phone oder direkt auf Desktop)
|
||||||
|
// Wir öffnen „Mehr" um sekundäre Links sichtbar zu machen
|
||||||
|
const moreBtn = nav.getByRole('button', { name: de.nav.more })
|
||||||
|
if (await moreBtn.isVisible()) {
|
||||||
|
await moreBtn.click()
|
||||||
|
}
|
||||||
|
await expect(nav.getByRole('link', { name: de.nav.enclosures })).toBeVisible()
|
||||||
|
// "Becken" als separater Nav-Eintrag darf nicht mehr existieren
|
||||||
|
await expect(nav.getByRole('link', { name: 'Becken', exact: true })).toBeHidden()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('GEHEGE-RENAME: Nav hat keinen "Start"-Eintrag mehr (de.nav.home entfernt)', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto('/rennmaeuse')
|
||||||
|
// Warten bis die Tierliste geladen ist, dann Nav prüfen
|
||||||
|
await expect(page.getByRole('heading', { name: de.pages.gerbils.title, exact: true })).toBeVisible()
|
||||||
|
const nav = page.getByRole('navigation', { name: de.nav.mainNavigation })
|
||||||
|
// 'Start' (de.nav.home wurde entfernt) darf kein Nav-Link mehr sein
|
||||||
|
await expect(nav.getByRole('link', { name: 'Start', exact: true })).toBeHidden()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ── (c) '/' landet auf /rennmaeuse ──────────────────────────────────────────
|
||||||
|
|
||||||
|
test('GEHEGE-RENAME: "/" leitet auf /rennmaeuse weiter — keine Startseite mehr', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto('/')
|
||||||
|
// URL muss /rennmaeuse sein (Navigate replace)
|
||||||
|
await expect(page).toHaveURL(/\/rennmaeuse$/)
|
||||||
|
// Tierliste-Heading sichtbar — nicht die alte Startseite
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: de.pages.gerbils.title, exact: true }),
|
||||||
|
).toBeVisible()
|
||||||
|
// Alten Willkommen-Text darf es nicht mehr geben (de.pages.home wurde entfernt)
|
||||||
|
await expect(page.getByText('Willkommen')).toBeHidden()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('GEHEGE-RENAME: 404-Seite "Zur Startseite" landet weiterhin auf Tierliste', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.goto('/diese-seite-gibt-es-nicht')
|
||||||
|
await expect(page.getByText(de.pages.notFound.message)).toBeVisible()
|
||||||
|
await page.getByRole('link', { name: de.pages.notFound.backHome }).click()
|
||||||
|
// Nach Klick auf "Zur Startseite" → '/' → redirect → /rennmaeuse
|
||||||
|
await expect(page.getByRole('heading', { name: de.pages.gerbils.title, exact: true })).toBeVisible()
|
||||||
|
await expect(page).toHaveURL(/\/rennmaeuse$/)
|
||||||
|
})
|
||||||
@@ -129,7 +129,9 @@ def norm_zucht(z):
|
|||||||
if not z:
|
if not z:
|
||||||
return ""
|
return ""
|
||||||
n = z.lower()
|
n = z.lower()
|
||||||
n = re.sub(r"\bv\.\s?d\.\b", " ", n)
|
# TOLERANT KC-MATCHER FIX: trailing \b after '.' never fires when next char is ' '
|
||||||
|
# (both are non-word chars), so "v.d. kleinen" was NOT stripped. Drop the trailing \b.
|
||||||
|
n = re.sub(r"\bv\.\s?d\.", " ", n)
|
||||||
n = re.sub(r"\b(zucht|privatzucht|der|die|den|des|dem|von|of)\b", " ", n)
|
n = re.sub(r"\b(zucht|privatzucht|der|die|den|des|dem|von|of)\b", " ", n)
|
||||||
n = re.sub(r"[^a-z0-9äöüß ]", " ", n)
|
n = re.sub(r"[^a-z0-9äöüß ]", " ", n)
|
||||||
words = [w[:-1] if len(w) > 4 and w.endswith("n") else w for w in n.split()]
|
words = [w[:-1] if len(w) > 4 and w.endswith("n") else w for w in n.split()]
|
||||||
@@ -137,6 +139,15 @@ def norm_zucht(z):
|
|||||||
return ZUCHT_ALIASES.get(key, key)
|
return ZUCHT_ALIASES.get(key, key)
|
||||||
|
|
||||||
|
|
||||||
|
def is_clan_zucht(z):
|
||||||
|
"""True if the zucht name (raw or canonical) identifies the Kleine Chaoten home cattery.
|
||||||
|
|
||||||
|
Accepts all known spellings: 'Zucht der Kleinen Chaoten', 'kleinen Chaoten',
|
||||||
|
'v.d. Kleinen Chaoten', '[ZdkC]', 'kleinechaote', etc.
|
||||||
|
"""
|
||||||
|
return norm_zucht(z) == "kleinechaote"
|
||||||
|
|
||||||
|
|
||||||
def canon_pair(raw):
|
def canon_pair(raw):
|
||||||
"""Full raw name -> (normalised call-name, canonical zucht)."""
|
"""Full raw name -> (normalised call-name, canonical zucht)."""
|
||||||
name, zucht = split_name_zucht(raw)
|
name, zucht = split_name_zucht(raw)
|
||||||
|
|||||||
@@ -280,6 +280,33 @@ check("gen.+v.d. name rejected", e.looks_like_animal_name("Victoria Welby gen. W
|
|||||||
check("real Farbschlag accepted", not e.looks_like_animal_name("Kohlfuchsschimmel"))
|
check("real Farbschlag accepted", not e.looks_like_animal_name("Kohlfuchsschimmel"))
|
||||||
check("real Farbschlag accepted 2", not e.looks_like_animal_name("Orangeschimmel, hell"))
|
check("real Farbschlag accepted 2", not e.looks_like_animal_name("Orangeschimmel, hell"))
|
||||||
|
|
||||||
|
# --- TOLERANT KC-MATCHER (IMPORT-BACKFILL): all clan spelling variants -> canon 'kleinechaote' ---
|
||||||
|
# Julian-Entscheidung: Zucht = Kleine Chaoten wenn 'klein'+'chaoten' ODER bekannte Abkürzungen.
|
||||||
|
# The v.d. fix: trailing \b after '.' failed when next char is ' ' (non-word), so
|
||||||
|
# "v.d. kleinen chaoten" was NOT stripped before. Fix: drop the trailing \b.
|
||||||
|
check("KC-matcher: 'Zucht der Kleinen Chaoten'",
|
||||||
|
e.is_clan_zucht("Zucht der Kleinen Chaoten"))
|
||||||
|
check("KC-matcher: 'kleinen Chaoten' (no prefix)",
|
||||||
|
e.is_clan_zucht("kleinen Chaoten"))
|
||||||
|
check("KC-matcher: 'v.d. Kleinen Chaoten' (v.d. prefix — was broken before fix)",
|
||||||
|
e.is_clan_zucht("v.d. Kleinen Chaoten"))
|
||||||
|
check("KC-matcher: '[ZdkC]' shorthand (bracket form, alias in ZUCHT_ALIASES)",
|
||||||
|
e.is_clan_zucht("ZdkC"))
|
||||||
|
check("KC-matcher: 'von den Kleinen Chaoten' (full long form)",
|
||||||
|
e.is_clan_zucht("von den Kleinen Chaoten"))
|
||||||
|
check("KC-matcher: empty string -> NOT clan",
|
||||||
|
not e.is_clan_zucht(""))
|
||||||
|
check("KC-matcher: 'Black Forest' -> NOT clan",
|
||||||
|
not e.is_clan_zucht("Black Forest"))
|
||||||
|
check("KC-matcher: norm_zucht regression — 'Kleine Chaoten' (base form still works)",
|
||||||
|
e.norm_zucht("Kleine Chaoten") == "kleinechaote")
|
||||||
|
check("KC-matcher: norm_zucht regression — 'von den Kleinen Chaoten'",
|
||||||
|
e.norm_zucht("von den Kleinen Chaoten") == "kleinechaote")
|
||||||
|
# Decision-matching FIX-1 already tested above; v.d. in decision matches 'von den' in record
|
||||||
|
# because both reduce to the same canon_pair. Verify norm_zucht directly for v.d.:
|
||||||
|
check("KC-matcher: norm_zucht('v.d. Kleinen Chaoten') == 'kleinechaote' (was broken before fix)",
|
||||||
|
e.norm_zucht("v.d. Kleinen Chaoten") == "kleinechaote")
|
||||||
|
|
||||||
if failed:
|
if failed:
|
||||||
print(f"\n{failed} test(s) FAILED")
|
print(f"\n{failed} test(s) FAILED")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user