MyKindlebox/mykindlebox/client/src/lib/pedigree.test.js

130 lines
5.3 KiB
JavaScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import {
createInitialPedigreeState,
buildPedigreePayload,
serializeManualAncestor,
parseManualAncestor,
formatManualAncestorDisplay,
formatManualAncestorDetails,
pedigreeStateFromScan,
uncertainScanSlots,
} from './pedigree.js'
test('manual ancestor serialize/parse round-trips', () => {
const json = serializeManualAncestor({
name: 'Old Bob',
farm: 'Sunnyside',
ear_number: 'B-12',
weight_lbs: 4.5,
dob: 'Unknown',
})
const parsed = parseManualAncestor(json)
assert.equal(parsed.name, 'Old Bob')
assert.equal(parsed.farm, 'Sunnyside')
assert.equal(parsed.ear_number, 'B-12')
assert.equal(parsed.weight_lbs, 4.5)
assert.equal(parsed.dob, 'Unknown')
})
test('parseManualAncestor accepts legacy plain-text names', () => {
const parsed = parseManualAncestor('Just A Name')
assert.equal(parsed.name, 'Just A Name')
assert.equal(parsed.farm, '')
assert.equal(parsed.ear_number, '')
})
test('formatManualAncestorDisplay composes name, farm, ear number', () => {
const json = serializeManualAncestor({ name: 'Old Bob', farm: 'Sunnyside', ear_number: 'B-12' })
assert.equal(formatManualAncestorDisplay(json), 'Old Bob (Sunnyside) • Ear #B-12')
assert.equal(formatManualAncestorDisplay(serializeManualAncestor({ name: 'Solo' })), 'Solo')
})
test('buildPedigreePayload: empty tree -> all null', () => {
const payload = buildPedigreePayload(createInitialPedigreeState(), new Map())
assert.equal(payload.sire_id, null)
assert.equal(payload.dam_id, null)
assert.equal(payload.paternal_grandsire_name_text, null)
})
test('buildPedigreePayload: a linked parent writes the FK and locks (nulls) its ancestors', () => {
const state = createInitialPedigreeState()
state.sire = { mode: 'linked', id: 5, name: 'Jimmy' }
// Cascade cache holds Jimmy's own (empty) ancestry subtree.
const cache = new Map([[5, { id: 5, name: 'Jimmy', sire: null, dam: null }]])
const payload = buildPedigreePayload(state, cache)
assert.equal(payload.sire_id, 5)
assert.equal(payload.sire_name_text, null)
// Gen2 paternal slots are chain-derived from the linked sire, so they must
// stay null on the subject rather than being written as freeform text.
assert.equal(payload.paternal_grandsire_name_text, null)
assert.equal(payload.paternal_granddam_name_text, null)
})
test('buildPedigreePayload: a manual parent leaves deeper slots editable and persisted', () => {
const state = createInitialPedigreeState()
state.sire = { mode: 'manual', id: null, name: 'Bob' }
state.paternal_grandsire = { mode: 'manual', id: null, name: 'Old Bob' }
const payload = buildPedigreePayload(state, new Map())
assert.equal(payload.sire_id, null)
assert.equal(payload.sire_name_text, 'Bob')
assert.equal(payload.paternal_grandsire_name_text, 'Old Bob')
})
test('pedigreeStateFromScan links matched parents and makes named ancestors manual', () => {
const state = pedigreeStateFromScan({
ancestors: {
sire: { name: 'Hoppin-pop', farm: "Peachy Buns'", ear_number: 'HP1', dob: null, weight_lbs: null, uncertain: [] },
dam: { name: 'Black Betty', farm: "Cody's", ear_number: 'BB2', dob: '2024-03-04', weight_lbs: 4.25, uncertain: ['dob'] },
paternal_grandsire: { name: null, farm: null, ear_number: null, dob: null, weight_lbs: null, uncertain: [] },
},
matches: { sire: { id: 7, name: 'Hoppin-pop' }, dam: null },
})
assert.deepEqual(state.sire, { mode: 'linked', id: 7, name: 'Hoppin-pop' })
assert.equal(state.dam.mode, 'manual')
const dam = parseManualAncestor(state.dam.name)
assert.equal(dam.name, 'Black Betty')
assert.equal(dam.farm, "Cody's")
assert.equal(dam.dob, '2024-03-04')
assert.equal(dam.weight_lbs, 4.25)
// Unnamed or missing ancestors stay at the empty default.
assert.equal(state.paternal_grandsire.mode, 'unknown')
assert.equal(state.mm_granddam.mode, 'unknown')
})
test('uncertainScanSlots skips matched parents and certain ancestors', () => {
const slots = uncertainScanSlots({
ancestors: {
sire: { name: 'A', uncertain: ['ear_number'] },
dam: { name: 'B', uncertain: ['dob'] },
maternal_grandsire: { name: 'C', uncertain: [] },
},
matches: { sire: { id: 1, name: 'A' } },
})
assert.deepEqual([...slots], ['dam'])
})
test('manual ancestors default to 3 lbs and Unknown DOB, including legacy 0-weight entries', () => {
const fresh = parseManualAncestor(serializeManualAncestor({ name: 'No Info', farm: 'X', ear_number: 'A1' }))
assert.equal(fresh.weight_lbs, 3)
assert.equal(fresh.dob, 'Unknown')
const legacy = parseManualAncestor(JSON.stringify({ name: 'Old', farm: '', ear_number: '', weight_lbs: 0, dob: '' }))
assert.equal(legacy.weight_lbs, 3)
assert.equal(legacy.dob, 'Unknown')
assert.equal(parseManualAncestor('Plain Name').weight_lbs, 3)
const real = parseManualAncestor(serializeManualAncestor({ name: 'Real', weight_lbs: '4.5', dob: '2024-03-04' }))
assert.equal(real.weight_lbs, 4.5)
assert.equal(real.dob, '2024-03-04')
})
test('formatManualAncestorDetails shows weight and DOB', () => {
const fmt = (w) => `${w} lbs`
assert.equal(formatManualAncestorDetails(serializeManualAncestor({ name: 'A' }), fmt), '3 lbs · DOB Unknown')
assert.equal(
formatManualAncestorDetails(serializeManualAncestor({ name: 'B', weight_lbs: 5, dob: '2024-03-04' }), fmt),
'5 lbs · DOB 3/4/2024'
)
})