Compare commits

..

1 Commits

Author SHA1 Message Date
55c298ad1f UI-POLISH-1: namenlose Tiere + Leerwert-Konsistenz
- de.ts: nameless = '(ohne Namen)' (neuer Platzhalter-String)
- GerbilsPage: g.name || t.nameless in Karten-Name + Checkbox-Aria
- GerbilDetailPage: g.name || nameless im h2-Header; g.causeOfDeath/notes via || null (leer-string-sicher wie Herkunft)
- AnimalPicker: g.name || nameless im Picker-Button

e2e: +1 nameless-stub in mock-data; 2 neue Tests (Liste + Detail-Header)

Gate: vitest 82/82, e2e 130/130, tsc clean
2026-06-06 17:32:26 +02:00
11 changed files with 31 additions and 199 deletions

View File

@@ -82,12 +82,8 @@ 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). /// Zuchtname animals made resident because they parented a Clan offspring (rule b).</summary>
/// HerkunftBackfilled = resident animals whose null OriginBreeder was filled (safe, no overwrite). public sealed record ResidencySummary(int Resident, int External, int FlippedByParentRule);
/// 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,

View File

@@ -137,14 +137,12 @@ 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, g.ColorVarietyId }).ToListAsync(); .Select(g => new { g.Id, g.Name, g.DateOfBirth, g.ExternalRef, g.LitterId }).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>();
@@ -171,8 +169,7 @@ 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;
var currentColorVarietyId = exists && existingColorVarietyByExtRef.TryGetValue(a.Id, out var ecv) ? ecv : null; plan.Add(new AnimalPlan(a, gid, exists, wurfLitterId, currentLitter, colorVarietyId, gender));
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
@@ -463,51 +460,6 @@ 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.");
@@ -517,10 +469,6 @@ 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).");
@@ -536,8 +484,7 @@ 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)
@@ -627,8 +574,7 @@ 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);

View File

@@ -1,90 +0,0 @@
/**
* 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$/)
})

View File

@@ -149,6 +149,8 @@ export function seedDb(): MockDb {
enclosureId: 'enc-leer', enclosureId: 'enc-leer',
notes: 'neugieriger Entdecker', notes: 'neugieriger Entdecker',
}, },
// UI-POLISH-1: Import-Stub ohne Namen — testet den '(ohne Namen)'-Platzhalter in Liste + Detail.
gerbil('nameless-stub', '', 'male', '2023-01-01', null, null),
] ]
const litters: Litter[] = [ const litters: Litter[] = [

View File

@@ -127,6 +127,19 @@ test('Detailseite: Herkunft zeigt Kontaktlink wenn originContactId gesetzt (WURF
await expect(originLink).toHaveAttribute('href', '/kontakte/con-meier') await expect(originLink).toHaveAttribute('href', '/kontakte/con-meier')
}) })
test('Namenlose Tiere zeigen Platzhalter in der Liste (UI-POLISH-1)', async ({ page }) => {
skipUnlessMock()
await page.goto('/rennmaeuse')
// 'nameless-stub' hat name='' → Platzhalter '(ohne Namen)' soll als Link sichtbar sein
await expect(page.getByRole('link', { name: de.pages.gerbils.nameless })).toBeVisible()
})
test('Namenlose Tiere zeigen Platzhalter im Detail-Header (UI-POLISH-1)', async ({ page }) => {
skipUnlessMock()
await page.goto('/rennmaeuse/nameless-stub')
await expect(page.getByRole('heading', { name: de.pages.gerbils.nameless })).toBeVisible()
})
test('Tier bearbeiten — Notizen ändern', async ({ page }) => { test('Tier bearbeiten — Notizen ändern', async ({ page }) => {
skipUnlessMock() skipUnlessMock()
await page.goto('/rennmaeuse/kruemel/bearbeiten') await page.goto('/rennmaeuse/kruemel/bearbeiten')

View File

@@ -1,5 +1,7 @@
import { useState } from 'react' import { useState } from 'react'
import { de } from '../strings/de' import { de } from '../strings/de'
const nameless = de.pages.gerbils.nameless
import { listGerbils } from '../api/gerbils' import { listGerbils } from '../api/gerbils'
import { andFilter, condition } from '../api/gridify' import { andFilter, condition } from '../api/gridify'
import type { Gender, Gerbil, Paged } from '../api/types' import type { Gender, Gerbil, Paged } from '../api/types'
@@ -62,7 +64,7 @@ export default function AnimalPicker({ gender, onPick }: AnimalPickerProps) {
setOpen(false) setOpen(false)
}} }}
> >
{g.name} {g.name || nameless}
</button> </button>
</li> </li>
))} ))}

View File

@@ -102,7 +102,7 @@ export default function GerbilDetailPage() {
<header className="page-head"> <header className="page-head">
<div> <div>
<GerbilProfilePhoto gerbilId={g.id} /> <GerbilProfilePhoto gerbilId={g.id} />
<h2>{g.name}</h2> <h2>{g.name || de.pages.gerbils.nameless}</h2>
<span className={`badge badge--${g.status.toLowerCase()}`}>{statusLabel(g.status)}</span> <span className={`badge badge--${g.status.toLowerCase()}`}>{statusLabel(g.status)}</span>
{g.isResident === false && ( {g.isResident === false && (
<span className="badge badge--external" title={de.pages.gerbils.externalTitle}> <span className="badge badge--external" title={de.pages.gerbils.externalTitle}>
@@ -153,7 +153,7 @@ export default function GerbilDetailPage() {
{g.status === 'Deceased' && ( {g.status === 'Deceased' && (
<> <>
<Row label={t.fields.dateOfDeath} value={formatDate(g.dateOfDeath)} /> <Row label={t.fields.dateOfDeath} value={formatDate(g.dateOfDeath)} />
<Row label={t.fields.causeOfDeath} value={g.causeOfDeath} /> <Row label={t.fields.causeOfDeath} value={g.causeOfDeath || null} />
</> </>
)} )}
{g.status === 'GivenAway' && ( {g.status === 'GivenAway' && (
@@ -190,7 +190,7 @@ export default function GerbilDetailPage() {
} }
/> />
<Row label={t.fields.receiver} value={lookup(contactName, g.receiverContactId)} /> <Row label={t.fields.receiver} value={lookup(contactName, g.receiverContactId)} />
<Row label={t.fields.notes} value={g.notes} /> <Row label={t.fields.notes} value={g.notes || null} />
</dl> </dl>
<h3>{t.detail.genetics}</h3> <h3>{t.detail.genetics}</h3>

View File

@@ -256,11 +256,11 @@ export default function GerbilsPage() {
className="gerbil-row__check" className="gerbil-row__check"
checked={selected.has(g.id)} checked={selected.has(g.id)}
onChange={() => toggleSelect(g.id)} onChange={() => toggleSelect(g.id)}
aria-label={g.name} aria-label={g.name || t.nameless}
/> />
<Link to={`/rennmaeuse/${g.id}`} className="gerbil-card"> <Link to={`/rennmaeuse/${g.id}`} className="gerbil-card">
<span className="gerbil-card__name"> <span className="gerbil-card__name">
{g.name} {g.name || t.nameless}
{g.isResident === false && ( {g.isResident === false && (
<span className="badge badge--external" title={t.externalTitle}> <span className="badge badge--external" title={t.externalTitle}>
{t.externalBadge} {t.externalBadge}

View File

@@ -38,6 +38,7 @@ export const de = {
pages: { pages: {
gerbils: { gerbils: {
title: 'Rennmäuse', title: 'Rennmäuse',
nameless: '(ohne Namen)',
// Liste // Liste
newButton: 'Neue Rennmaus', newButton: 'Neue Rennmaus',
empty: 'Keine Rennmäuse gefunden.', empty: 'Keine Rennmäuse gefunden.',

View File

@@ -129,9 +129,7 @@ def norm_zucht(z):
if not z: if not z:
return "" return ""
n = z.lower() n = z.lower()
# TOLERANT KC-MATCHER FIX: trailing \b after '.' never fires when next char is ' ' n = re.sub(r"\bv\.\s?d\.\b", " ", n)
# (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()]
@@ -139,15 +137,6 @@ 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)

View File

@@ -280,33 +280,6 @@ 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)