- colourpointName(): A- cchm/ch now yields 'CP-<base>-Hell' (was 'CP-<base>'); A- cchm/cchm unchanged. Non-agouti branch (Marder/Siam/Zobel/Zobel-Hell) unchanged — already used the het token. - Added 5 new catalog entries: CP-Agouti-Hell, CP-Silberagouti-Hell, CP-Algierfuchs-Hell, CP-Polarfuchs-Hell, CP-Orangeschimmel-Hell. CP-Fuchs-Hell was already present. Catalog 61 -> 66. - Regenerated colorVarietySeed.generated.json (66 rows for Pam DATA-2 re-seed). - Tests: 82 -> 82 (reworded het case) + 1 new GEN-3g describe block. All pass. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
526 lines
22 KiB
TypeScript
526 lines
22 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import {
|
||
add,
|
||
divide,
|
||
frac,
|
||
multiply,
|
||
toNumber,
|
||
toString,
|
||
equals,
|
||
} from '../fraction'
|
||
import {
|
||
fromDisplayString,
|
||
makeGenotype,
|
||
toDisplayString,
|
||
toJSON,
|
||
fromJSON,
|
||
wildType,
|
||
extractGenotypeFlags,
|
||
} from '../genotype'
|
||
import { combineLocus } from '../punnett'
|
||
import { LOCI, type LocusKey } from '../loci'
|
||
import {
|
||
farbschlagFor,
|
||
genotypeToFarbschlag,
|
||
representativeGenotype,
|
||
BASE_COLORS,
|
||
CATALOG,
|
||
CATALOG_SIZE,
|
||
} from '../catalog'
|
||
|
||
/** The first 18 entries are the frozen contract names (must round-trip exactly). */
|
||
const FROZEN_COUNT = 17
|
||
import { breed } from '../breed'
|
||
import { GeneticsWarningCode } from '../warnings'
|
||
|
||
describe('Fraction', () => {
|
||
it('reduces and does exact arithmetic', () => {
|
||
expect(toString(frac(2, 4))).toBe('1/2')
|
||
expect(toString(add(frac(1, 4), frac(1, 4)))).toBe('1/2')
|
||
expect(toString(multiply(frac(1, 2), frac(1, 2)))).toBe('1/4')
|
||
expect(toString(divide(frac(1, 4), frac(3, 4)))).toBe('1/3')
|
||
expect(equals(frac(3, 6), frac(1, 2))).toBe(true)
|
||
})
|
||
})
|
||
|
||
describe('Genotype serialization', () => {
|
||
it('round-trips display string <-> structured form', () => {
|
||
const g = fromDisplayString('Aa CC Dd EE GG Pp Spsp rere')
|
||
expect(toDisplayString(g)).toBe('Aa CC Dd EE GG Pp Spsp rere')
|
||
expect(toDisplayString(fromJSON(toJSON(g)))).toBe('Aa CC Dd EE GG Pp Spsp rere')
|
||
})
|
||
|
||
it('parses multi-char C-series alleles via maximal munch', () => {
|
||
const g = fromDisplayString('aa cchmch DD EE GG PP spsp rere')
|
||
expect(g.C).toEqual(['cchm', 'ch'])
|
||
})
|
||
|
||
it('orders allele pairs most-dominant-first', () => {
|
||
const g = makeGenotype({
|
||
A: ['a', 'A'],
|
||
C: ['ch', 'C'],
|
||
D: ['D', 'D'],
|
||
E: ['E', 'E'],
|
||
G: ['G', 'G'],
|
||
P: ['p', 'P'],
|
||
Sp: ['sp', 'sp'],
|
||
Re: ['re', 're'],
|
||
Sls: ['sl', 'sl'],
|
||
})
|
||
expect(g.A).toEqual(['A', 'a'])
|
||
expect(g.C).toEqual(['C', 'ch'])
|
||
expect(g.P).toEqual(['P', 'p'])
|
||
})
|
||
|
||
it('wild type is AA CC DD EE GG PP spsp rere', () => {
|
||
expect(toDisplayString(wildType())).toBe('AA CC DD EE GG PP spsp rere')
|
||
})
|
||
})
|
||
|
||
describe('Punnett single locus', () => {
|
||
it('Aa x Aa -> 1/4 AA, 1/2 Aa, 1/4 aa', () => {
|
||
const dist = combineLocus('A', ['A', 'a'], ['A', 'a'])
|
||
expect(toString(dist.get('AA')!.probability)).toBe('1/4')
|
||
expect(toString(dist.get('Aa')!.probability)).toBe('1/2')
|
||
expect(toString(dist.get('aa')!.probability)).toBe('1/4')
|
||
})
|
||
|
||
it('AA x aa -> all Aa', () => {
|
||
const dist = combineLocus('A', ['A', 'A'], ['a', 'a'])
|
||
expect(dist.size).toBe(1)
|
||
expect(toString(dist.get('Aa')!.probability)).toBe('1')
|
||
})
|
||
})
|
||
|
||
describe('Worked example from research report', () => {
|
||
// Agouti AA CC DD EE GG PP x Platin aa CC DD EE GG pp -> all Aa Cc Dd Ee Gg Pp
|
||
// (every locus where parents differ becomes heterozygous with prob 1)
|
||
it('Agouti x Platin yields a single uniform heterozygous genotype', () => {
|
||
const father = fromDisplayString('AA CC DD EE GG PP spsp rere')
|
||
const mother = fromDisplayString('aa CC DD EE GG pp spsp rere')
|
||
const result = breed(father, mother)
|
||
|
||
expect(result.offspring).toHaveLength(1)
|
||
const only = result.offspring[0]
|
||
expect(only.genotype).toBe('Aa CC DD EE GG Pp spsp rere')
|
||
expect(only.probability.text).toBe('1')
|
||
expect(result.warnings).toHaveLength(0)
|
||
})
|
||
})
|
||
|
||
describe('Schecke × Schecke lethality (SpSp)', () => {
|
||
it('removes SpSp, renormalises, and warns ~25% fewer young', () => {
|
||
// Spsp x Spsp at the Sp locus -> 1/4 SpSp (lethal), 1/2 Spsp, 1/4 spsp.
|
||
// After removing SpSp the survivors renormalise to 2/3 Spsp : 1/3 spsp.
|
||
const father = fromDisplayString('AA CC DD EE GG PP Spsp rere')
|
||
const mother = fromDisplayString('AA CC DD EE GG PP Spsp rere')
|
||
const result = breed(father, mother)
|
||
|
||
// No surviving genotype is SpSp.
|
||
expect(result.offspring.every((o) => !o.genotype.includes('SpSp'))).toBe(true)
|
||
|
||
// Probabilities renormalise to sum to exactly 1.
|
||
const sum = result.offspring.reduce((acc, o) => acc + o.probability.value, 0)
|
||
expect(sum).toBeCloseTo(1, 10)
|
||
|
||
// Survivors: 2/3 checkered (Spsp), 1/3 non-checkered (spsp).
|
||
const spsp = result.offspring.find((o) => o.genotype.includes('Spsp'))!
|
||
const homRec = result.offspring.find((o) => o.genotype.includes('spsp'))!
|
||
expect(spsp.probability.text).toBe('2/3')
|
||
expect(homRec.probability.text).toBe('1/3')
|
||
|
||
// Warning emitted, carrying the 1/4 young-lost stat.
|
||
const warn = result.warnings.find((w) => w.code === GeneticsWarningCode.ScheckeLethal)
|
||
expect(warn).toBeDefined()
|
||
expect(warn!.detail?.youngLostFraction).toBe('1/4')
|
||
})
|
||
})
|
||
|
||
describe('Rex × Rex semi-lethality (ReRe)', () => {
|
||
it('keeps ReRe but emits a semi-lethal warning', () => {
|
||
const father = fromDisplayString('AA CC DD EE GG PP spsp Rere')
|
||
const mother = fromDisplayString('AA CC DD EE GG PP spsp Rere')
|
||
const result = breed(father, mother)
|
||
|
||
// ReRe survives (semi-lethal, not removed).
|
||
expect(result.offspring.some((o) => o.genotype.includes('ReRe'))).toBe(true)
|
||
const sum = result.offspring.reduce((acc, o) => acc + o.probability.value, 0)
|
||
expect(sum).toBeCloseTo(1, 10)
|
||
|
||
const warn = result.warnings.find((w) => w.code === GeneticsWarningCode.RexSemiLethal)
|
||
expect(warn).toBeDefined()
|
||
})
|
||
})
|
||
|
||
describe('Farbschlag catalog', () => {
|
||
it('names the wild type Agouti', () => {
|
||
expect(farbschlagFor(wildType()).name).toBe('Agouti')
|
||
})
|
||
|
||
it('names Platin (aa CC DD EE GG pp)', () => {
|
||
expect(farbschlagFor(fromDisplayString('aa CC DD EE GG pp spsp rere')).name).toBe('Platin')
|
||
})
|
||
|
||
it('names Schwarz (aa CC DD EE GG PP)', () => {
|
||
expect(farbschlagFor(fromDisplayString('aa CC DD EE GG PP spsp rere')).name).toBe('Schwarz')
|
||
})
|
||
|
||
it('appends Schecke and Rex modifiers', () => {
|
||
expect(farbschlagFor(fromDisplayString('AA CC DD EE GG PP Spsp rere')).name).toBe('Agouti Schecke')
|
||
expect(farbschlagFor(fromDisplayString('AA CC DD EE GG PP Spsp Rere')).name).toBe(
|
||
'Agouti Schecke Rex',
|
||
)
|
||
})
|
||
|
||
it('falls back to Unbekannter Farbschlag for uncatalogued genotypes', () => {
|
||
// E-dominant (no Fuchs/Schimmel family) + an uncatalogued cchm/dd/gg/pp combo.
|
||
const match = farbschlagFor(fromDisplayString('aa CC dd EE gg pp spsp rere'))
|
||
expect(match.unknown).toBe(true)
|
||
expect(match.name).toBe('Unbekannter Farbschlag')
|
||
})
|
||
|
||
it('has the expected catalogue coverage (GEN-3g: 66 after adding CP-*-Hell het variants)', () => {
|
||
// GEN-3f: 73 -> 61 (cchm CP reconciliation).
|
||
// GEN-3g: +5 het variants (CP-Agouti/Silberagouti/Algierfuchs/Polarfuchs/Orangeschimmel -Hell),
|
||
// giving 61 + 5 = 66. CP-Fuchs-Hell was already counted.
|
||
expect(CATALOG_SIZE).toBe(66)
|
||
})
|
||
|
||
it('frozen contract names round-trip to themselves (DB-key guard)', () => {
|
||
// The first 18 are the frozen ColorVariety keys — their representative
|
||
// genotype MUST resolve back to their own name, never a later variety.
|
||
for (const entry of BASE_COLORS.slice(0, FROZEN_COUNT)) {
|
||
expect(genotypeToFarbschlag(representativeGenotype(entry))).toBe(entry.name)
|
||
}
|
||
})
|
||
|
||
it('every entry resolves to a named variety (no self-Unbekannt)', () => {
|
||
// Each catalogued variety's representative genotype must resolve to SOME
|
||
// named variety — never "Unbekannt". Genotype-identical synonyms (e.g.
|
||
// "-hell") or entries that omit a locus may resolve to an earlier primary;
|
||
// that's expected first-match behaviour, not an error.
|
||
for (const entry of BASE_COLORS) {
|
||
expect(farbschlagFor(representativeGenotype(entry)).unknown).toBe(false)
|
||
}
|
||
})
|
||
|
||
it('has no duplicate variety names and only valid allele tokens', () => {
|
||
const names = BASE_COLORS.map((e) => e.name)
|
||
expect(new Set(names).size).toBe(names.length)
|
||
for (const entry of BASE_COLORS) {
|
||
for (const [locus, value] of Object.entries(entry.tokens)) {
|
||
// GEN-3f: a token may be a heterozygous pair "x/y" (e.g. C: 'cchm/ch'
|
||
// for the het colourpoints Siam/Zobel-Hell) — validate each allele.
|
||
for (const allele of value.split('/')) {
|
||
expect(LOCI[locus as LocusKey].alleles).toContain(allele)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
it('CATALOG seed view mirrors the ColorVariety table shape', () => {
|
||
expect(CATALOG).toHaveLength(CATALOG_SIZE)
|
||
expect(CATALOG[0]).toMatchObject({ name: 'Pink Eyed White (PEW)', sortOrder: 0 })
|
||
// Every row has a non-empty canonical genotype display string and unique name.
|
||
expect(new Set(CATALOG.map((c) => c.name)).size).toBe(CATALOG.length)
|
||
expect(CATALOG.every((c) => /^[A-Za-z?]+( [A-Za-z?]+){7}$/.test(c.canonicalGenotype))).toBe(true)
|
||
})
|
||
|
||
it('genotypeToFarbschlag (DATA-1 denormalization contract) returns the plain name', () => {
|
||
expect(genotypeToFarbschlag(wildType())).toBe('Agouti')
|
||
expect(genotypeToFarbschlag(fromDisplayString('aa CC DD EE GG pp spsp rere'))).toBe('Platin')
|
||
expect(genotypeToFarbschlag(fromDisplayString('aa CC dd EE gg pp spsp rere'))).toBe(
|
||
'Unbekannter Farbschlag',
|
||
)
|
||
})
|
||
})
|
||
|
||
describe('Partially-unknown parents (wildcards)', () => {
|
||
it('handles an A? parent (phenotype agouti, genotype unknown)', () => {
|
||
// A? x aa at the A locus -> father gamete: 1/2 A, 1/4 (each of A,a) from "?"
|
||
// = effectively 3/4 A, 1/4 a ; mother always a.
|
||
// Offspring: 3/4 Aa, 1/4 aa.
|
||
const father = makeGenotype({
|
||
A: ['A', '?'],
|
||
C: ['C', 'C'],
|
||
D: ['D', 'D'],
|
||
E: ['E', 'E'],
|
||
G: ['G', 'G'],
|
||
P: ['P', 'P'],
|
||
Sp: ['sp', 'sp'],
|
||
Re: ['re', 're'],
|
||
Sls: ['sl', 'sl'],
|
||
})
|
||
const mother = fromDisplayString('aa CC DD EE GG PP spsp rere')
|
||
const result = breed(father, mother)
|
||
|
||
const agouti = result.byFarbschlag.find((f) => f.farbschlag === 'Agouti')!
|
||
const schwarz = result.byFarbschlag.find((f) => f.farbschlag === 'Schwarz')!
|
||
expect(agouti.probability.text).toBe('3/4')
|
||
expect(schwarz.probability.text).toBe('1/4')
|
||
})
|
||
})
|
||
|
||
describe('Probabilities are exact fractions summing to 1', () => {
|
||
it('Aa Cc x Aa Cc -> 9 genotypes summing to 1', () => {
|
||
const father = fromDisplayString('Aa Cch DD EE GG PP spsp rere')
|
||
const mother = fromDisplayString('Aa Cch DD EE GG PP spsp rere')
|
||
const result = breed(father, mother)
|
||
const sum = result.offspring.reduce(
|
||
(acc, o) => add(acc, o.probability.fraction),
|
||
frac(0, 1),
|
||
)
|
||
expect(toNumber(sum)).toBeCloseTo(1, 10)
|
||
})
|
||
})
|
||
|
||
describe('GEN-3a: Uw=G alias', () => {
|
||
it('Uwuw parses as Gg, UwUw as GG', () => {
|
||
expect(fromDisplayString('AA CC DD EE Uwuw PP spsp rere').G).toEqual(['G', 'g'])
|
||
expect(fromDisplayString('AA CC DD EE UwUw PP spsp rere').G).toEqual(['G', 'G'])
|
||
expect(fromDisplayString('AA CC DD EE uwuw PP spsp rere').G).toEqual(['g', 'g'])
|
||
})
|
||
|
||
it('always RENDERS G, never Uw (breeder preference)', () => {
|
||
// Uw/uw is an input/import alias only; output must echo G/g.
|
||
expect(toDisplayString(fromDisplayString('AA CC DD EE Uwuw PP spsp rere'))).toBe(
|
||
'AA CC DD EE Gg PP spsp rere',
|
||
)
|
||
expect(toDisplayString(fromDisplayString('AA CC DD EE uwuw PP spsp rere'))).not.toContain('uw')
|
||
})
|
||
})
|
||
|
||
describe('GEN-3a: second spotting locus Sls (WP)', () => {
|
||
it('S(l)s(l) and WP both parse to the Sls heterozygote Slsl', () => {
|
||
expect(fromDisplayString('AA CC DD EE GG PP spsp rere S(l)s(l)').Sls).toEqual(['Sl', 'sl'])
|
||
expect(fromDisplayString('AA CC DD EE GG PP spsp rere WP').Sls).toEqual(['Sl', 'sl'])
|
||
})
|
||
|
||
it('toDisplayString omits wild-type Sls but shows Slsl', () => {
|
||
expect(toDisplayString(wildType())).toBe('AA CC DD EE GG PP spsp rere')
|
||
expect(toDisplayString(fromDisplayString('AA CC DD EE GG PP spsp rere WP'))).toBe(
|
||
'AA CC DD EE GG PP spsp rere Slsl',
|
||
)
|
||
})
|
||
|
||
it('WP × WP: S(l)S(l) is lethal — removed, renormalized, SLS_LETHAL warning', () => {
|
||
const parent = fromDisplayString('AA CC DD EE GG PP spsp rere Slsl')
|
||
const result = breed(parent, parent)
|
||
expect(result.offspring.every((o) => !o.genotype.includes('SlSl'))).toBe(true)
|
||
const sum = result.offspring.reduce((acc, o) => acc + o.probability.value, 0)
|
||
expect(sum).toBeCloseTo(1, 10)
|
||
// survivors 2/3 Slsl : 1/3 slsl
|
||
const wp = result.offspring.find((o) => o.genotype.includes('Slsl'))!
|
||
expect(wp.probability.text).toBe('2/3')
|
||
const warn = result.warnings.find((w) => w.code === GeneticsWarningCode.SlsLethal)
|
||
expect(warn?.detail?.youngLostFraction).toBe('1/4')
|
||
})
|
||
|
||
it('Sp × Sls → Superschecke deafness warning', () => {
|
||
const father = fromDisplayString('AA CC DD EE GG PP Spsp rere')
|
||
const mother = fromDisplayString('AA CC DD EE GG PP spsp rere Slsl')
|
||
const result = breed(father, mother)
|
||
expect(
|
||
result.warnings.some((w) => w.code === GeneticsWarningCode.SuperscheckeDeaf),
|
||
).toBe(true)
|
||
})
|
||
})
|
||
|
||
describe('GEN-3a: flag/metadata tokens tolerated', () => {
|
||
it('dea/taub/Dea/DP/WFNZ/RV/GV do not break parsing (stripped)', () => {
|
||
const g = fromDisplayString('AA CC DD EE GG PP spsp rere dea WFNZ DP RV GV')
|
||
expect(toDisplayString(g)).toBe('AA CC DD EE GG PP spsp rere')
|
||
})
|
||
|
||
it('extractGenotypeFlags reads deafness + tags', () => {
|
||
expect(extractGenotypeFlags('AA CC DD EE GG PP spsp rere dea WFNZ').deaf).toBe(true)
|
||
expect(extractGenotypeFlags('AA CC DD EE GG PP spsp rere Dea').deaf).toBe(false)
|
||
expect(extractGenotypeFlags('AA CC DD EE GG PP spsp rere WFNZ RV').tags).toEqual(['WFNZ', 'RV'])
|
||
})
|
||
})
|
||
|
||
describe('GEN-3a: Schimmel catalog fix (breeder C5)', () => {
|
||
it('efef base -> Orangeschimmel (not Schwarzschimmel)', () => {
|
||
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD efef GG PP spsp rere'))).toBe(
|
||
'Orangeschimmel',
|
||
)
|
||
})
|
||
it('efef pp -> Rotaugenschimmel, efef gg -> Silberschimmel', () => {
|
||
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD efef GG pp spsp rere'))).toBe(
|
||
'Rotaugenschimmel',
|
||
)
|
||
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD efef gg PP spsp rere'))).toBe(
|
||
'Silberschimmel',
|
||
)
|
||
})
|
||
it('no Schwarzschimmel anywhere in the catalog', () => {
|
||
expect(BASE_COLORS.some((e) => e.name === 'Schwarzschimmel')).toBe(false)
|
||
})
|
||
})
|
||
|
||
describe("GEN-3c: unknown allele displays as '-' (stored as '?')", () => {
|
||
it("accepts '-' input, stores '?', displays '-'", () => {
|
||
const g = fromDisplayString('Aa C- DD EE GG Pp spsp rere')
|
||
expect(g.C).toEqual(['C', '?']) // stored internal contract stays '?'
|
||
expect(toDisplayString(g)).toBe('Aa C- DD EE GG Pp spsp rere') // displayed as '-'
|
||
})
|
||
it("'?' and '-' inputs are equivalent", () => {
|
||
expect(toDisplayString(fromDisplayString('Aa C? DD EE GG Pp spsp rere'))).toBe(
|
||
'Aa C- DD EE GG Pp spsp rere',
|
||
)
|
||
})
|
||
})
|
||
|
||
describe('GEN-3c: no Unbekannt when the E locus is known (family fallback)', () => {
|
||
it('eef with unknown other loci -> Fuchsschimmel (the reported bug case)', () => {
|
||
expect(genotypeToFarbschlag(fromDisplayString('aa C- D- eef Gg Pp spsp --'))).toBe(
|
||
'Fuchsschimmel',
|
||
)
|
||
})
|
||
it('ee -> Fuchs family, efef -> a Schimmel (never Unbekannt) even with unknowns', () => {
|
||
expect(farbschlagFor(fromDisplayString('a- C- D- ee G- P-')).unknown).toBe(false)
|
||
expect(farbschlagFor(fromDisplayString('A- C- D- efef G- P-')).unknown).toBe(false)
|
||
})
|
||
it('unknown allele no longer blocks a match (wildcard, never Unbekannt)', () => {
|
||
// C unknown must not force Unbekannt — it resolves to SOME named variety.
|
||
expect(farbschlagFor(fromDisplayString('aa C- DD EE GG PP spsp rere')).unknown).toBe(false)
|
||
})
|
||
})
|
||
|
||
describe('GEN-3d: dominance tiebreak for unknown loci', () => {
|
||
it('unknown-C reads as full-colour, NOT a c^h/c^chm white', () => {
|
||
// 'aa C- DD EE GG PP' -> Schwarz (dominant C reading), never PEW/Hermelin/Himalaya.
|
||
const name = genotypeToFarbschlag(fromDisplayString('aa C- DD EE GG PP spsp rere'))
|
||
expect(name).toBe('Schwarz')
|
||
expect(['Pink Eyed White (PEW)', 'Hermelin', 'Himalaya']).not.toContain(name)
|
||
})
|
||
|
||
it('unknown second marker allele defaults UNMARKED, not Schecke (marker-aware)', () => {
|
||
// 'sp-' (one sp + unknown) -> sp/sp -> no Schecke (naive most-dominant would wrongly add it).
|
||
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD EE GG PP sp- rere'))).toBe('Agouti')
|
||
})
|
||
|
||
it('still: eef with unknowns -> Fuchsschimmel (family pin unaffected by tiebreak)', () => {
|
||
expect(genotypeToFarbschlag(fromDisplayString('aa C- D- eef Gg Pp spsp --'))).toBe(
|
||
'Fuchsschimmel',
|
||
)
|
||
})
|
||
})
|
||
|
||
describe('GEN-3e: C-locus colourpoint naming', () => {
|
||
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
|
||
|
||
it('A- + cchm/cchm -> CP-<base colour> (CP-Silberagouti example)', () => {
|
||
expect(name('AA cchmcchm DD EE gg PP spsp rere')).toBe('CP-Silberagouti')
|
||
expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti')
|
||
})
|
||
|
||
it('A- + cchm/ch -> CP-<base colour>-Hell (GEN-3g: het gets -Hell suffix)', () => {
|
||
expect(name('AA cchmch DD EE GG PP spsp rere')).toBe('CP-Agouti-Hell')
|
||
expect(name('AA cchmch DD EE gg PP spsp rere')).toBe('CP-Silberagouti-Hell')
|
||
expect(name('AA cchmch DD ee GG PP spsp rere')).toBe('CP-Algierfuchs-Hell')
|
||
expect(name('AA cchmch DD ee gg PP spsp rere')).toBe('CP-Polarfuchs-Hell')
|
||
expect(name('AA cchmch dd ee GG PP spsp rere')).toBe('CP-Fuchs-Hell')
|
||
})
|
||
|
||
it('aa fixed colourpoint names (Marder/Siam/Zobel/Zobel-Hell)', () => {
|
||
expect(name('aa cchmcchm DD EE GG PP spsp rere')).toBe('Marder')
|
||
expect(name('aa cchmch DD EE GG PP spsp rere')).toBe('Siam')
|
||
expect(name('aa cchmcchm DD EE gg PP spsp rere')).toBe('Zobel')
|
||
expect(name('aa cchmch DD EE gg PP spsp rere')).toBe('Zobel-Hell')
|
||
})
|
||
|
||
it('chch stays Hermelin (aa) / Himalaya (A-); full C => no CP', () => {
|
||
expect(name('aa chch DD EE GG PP spsp rere')).toBe('Hermelin')
|
||
expect(name('AA chch DD EE GG PP spsp rere')).toBe('Himalaya')
|
||
expect(name('AA CC DD EE GG PP spsp rere')).toBe('Agouti') // one full C -> no colourpoint
|
||
})
|
||
|
||
it('colourpoint composes with the Schecke modifier', () => {
|
||
expect(name('aa cchmcchm DD EE GG PP Spsp rere')).toBe('Marder Schecke')
|
||
})
|
||
})
|
||
|
||
describe('GEN-3f: CP catalog reconciled to the breeder CP- naming (matches her live data)', () => {
|
||
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
|
||
const has = (n: string) => BASE_COLORS.some((e) => e.name === n)
|
||
|
||
it('every CP- name her data uses exists as a catalog row (import name-match)', () => {
|
||
for (const n of [
|
||
'CP-Agouti', 'CP-Silberagouti', 'CP-Algierfuchs', 'CP-Polarfuchs',
|
||
'CP-Fuchs', 'CP-Fuchs-Hell', 'CP-Blaufuchs', 'CP-Orangeschimmel',
|
||
'Marder', 'Siam', 'Zobel', 'Zobel-Hell',
|
||
]) {
|
||
expect(has(n)).toBe(true)
|
||
}
|
||
})
|
||
|
||
it('the removed portal suffix-names are gone (collapsed)', () => {
|
||
for (const n of [
|
||
'Agouti CP', 'Agouti CP-Hell', 'Silberagouti CP', 'Algierfuchs CP',
|
||
'Polarfuchs CP', 'Blaufuchs CP', 'Kohlfuchs CP', 'Kohlfuchsschimmel CP',
|
||
'Marder dd', 'Zobel dd', 'Siam (Marder-Hell)', 'Agouti dd CP',
|
||
]) {
|
||
expect(has(n)).toBe(false)
|
||
}
|
||
})
|
||
|
||
it('the engine computes the agouti CP- names her data uses (E-family fallback covers fox/dilute)', () => {
|
||
expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti')
|
||
expect(name('AA cchmcchm DD EE gg PP spsp rere')).toBe('CP-Silberagouti')
|
||
expect(name('AA cchmcchm DD ee GG PP spsp rere')).toBe('CP-Algierfuchs')
|
||
expect(name('AA cchmcchm DD ee gg PP spsp rere')).toBe('CP-Polarfuchs')
|
||
// dd agouti fox has no dedicated base -> the eFamily fallback yields 'CP-Fuchs'.
|
||
expect(name('AA cchmcchm dd ee GG PP spsp rere')).toBe('CP-Fuchs')
|
||
// A- cchm efef -> CP-Orangeschimmel (Schimmel base under full C).
|
||
expect(name('AA cchmcchm DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel')
|
||
})
|
||
|
||
it('het cchm/ch colourpoints (Siam, Zobel-Hell, CP-*-Hell) resolve to their own names', () => {
|
||
const siam = BASE_COLORS.find((e) => e.name === 'Siam')!
|
||
const zh = BASE_COLORS.find((e) => e.name === 'Zobel-Hell')!
|
||
const cpah = BASE_COLORS.find((e) => e.name === 'CP-Agouti-Hell')!
|
||
const cpfh = BASE_COLORS.find((e) => e.name === 'CP-Fuchs-Hell')!
|
||
expect(genotypeToFarbschlag(representativeGenotype(siam))).toBe('Siam')
|
||
expect(genotypeToFarbschlag(representativeGenotype(zh))).toBe('Zobel-Hell')
|
||
expect(genotypeToFarbschlag(representativeGenotype(cpah))).toBe('CP-Agouti-Hell')
|
||
expect(genotypeToFarbschlag(representativeGenotype(cpfh))).toBe('CP-Fuchs-Hell')
|
||
})
|
||
})
|
||
|
||
describe('GEN-3g: "-Hell" in variety name == cchm/ch het; hom == cchm/cchm', () => {
|
||
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
|
||
const has = (n: string) => BASE_COLORS.some((e) => e.name === n)
|
||
|
||
it('all new -Hell het entries exist in catalog', () => {
|
||
for (const n of [
|
||
'CP-Agouti-Hell', 'CP-Silberagouti-Hell', 'CP-Algierfuchs-Hell',
|
||
'CP-Polarfuchs-Hell', 'CP-Orangeschimmel-Hell',
|
||
]) {
|
||
expect(has(n)).toBe(true)
|
||
}
|
||
})
|
||
|
||
it('engine correctly maps het (cchm/ch) to -Hell suffix for all A- bases', () => {
|
||
expect(name('AA cchmch DD EE GG PP spsp rere')).toBe('CP-Agouti-Hell')
|
||
expect(name('AA cchmch DD EE gg PP spsp rere')).toBe('CP-Silberagouti-Hell')
|
||
expect(name('AA cchmch DD ee GG PP spsp rere')).toBe('CP-Algierfuchs-Hell')
|
||
expect(name('AA cchmch DD ee gg PP spsp rere')).toBe('CP-Polarfuchs-Hell')
|
||
expect(name('AA cchmch dd ee GG PP spsp rere')).toBe('CP-Fuchs-Hell')
|
||
expect(name('AA cchmch DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel-Hell')
|
||
})
|
||
|
||
it('hom (cchm/cchm) still maps without -Hell suffix', () => {
|
||
expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti')
|
||
expect(name('AA cchmcchm DD EE gg PP spsp rere')).toBe('CP-Silberagouti')
|
||
expect(name('AA cchmcchm DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel')
|
||
})
|
||
|
||
it('aa non-agouti branch is unchanged (Marder/Siam/Zobel/Zobel-Hell)', () => {
|
||
expect(name('aa cchmcchm DD EE GG PP spsp rere')).toBe('Marder')
|
||
expect(name('aa cchmch DD EE GG PP spsp rere')).toBe('Siam')
|
||
expect(name('aa cchmcchm DD EE gg PP spsp rere')).toBe('Zobel')
|
||
expect(name('aa cchmch DD EE gg PP spsp rere')).toBe('Zobel-Hell')
|
||
})
|
||
})
|