183 lines
7.9 KiB
TypeScript
183 lines
7.9 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'fs';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
|
|
// db/client opens (and migrates) the SQLite file under DATA_DIR at import
|
|
// time, so point it at a throwaway directory before loading anything that
|
|
// imports it.
|
|
process.env.DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'mkb-ancestors-'));
|
|
const { db } = require('../db/client') as typeof import('../db/client');
|
|
const { rabbits } = require('../db/schema') as typeof import('../db/schema');
|
|
const { materializeAncestors, findExistingSubject, mergeIntoExisting } = require('./ancestors') as typeof import('./ancestors');
|
|
const { eq } = require('drizzle-orm') as typeof import('drizzle-orm');
|
|
|
|
type RabbitInsert = typeof rabbits.$inferInsert;
|
|
|
|
const manual = (name: string, extra: Record<string, unknown> = {}) =>
|
|
JSON.stringify({ name, farm: 'Test Farm', ear_number: '', weight_lbs: 3, dob: 'Unknown', ...extra });
|
|
|
|
function save(values: RabbitInsert) {
|
|
return db.transaction((tx) => {
|
|
materializeAncestors(tx, values);
|
|
return tx.insert(rabbits).values(values).returning().get();
|
|
});
|
|
}
|
|
|
|
// Mirrors POST /api/rabbits: top up an existing rabbit, else create.
|
|
function submit(values: RabbitInsert) {
|
|
return db.transaction((tx) => {
|
|
const existing = findExistingSubject(tx, values);
|
|
if (existing) return { id: existing.id, report: mergeIntoExisting(tx, existing, values) };
|
|
materializeAncestors(tx, values);
|
|
return { id: tx.insert(rabbits).values(values).returning().get().id, report: null };
|
|
});
|
|
}
|
|
|
|
const byId = (id: number) => db.select().from(rabbits).where(eq(rabbits.id, id)).get()!;
|
|
|
|
test('typed ancestors become linked reference rabbits', () => {
|
|
const subject = save({
|
|
name: 'Kit One',
|
|
sireNameText: manual('Big Buck', { ear_number: 'bb1', weight_lbs: 9.5, dob: '2022-01-02' }),
|
|
damNameText: manual('Nice Doe', { ear_number: 'ND1' }),
|
|
paternalGrandsireNameText: manual('Grandpa', { ear_number: 'GP1' }),
|
|
ppGranddamNameText: manual('Great Gran', { ear_number: 'GG1' }),
|
|
});
|
|
|
|
assert.ok(subject.sireId && subject.damId);
|
|
assert.equal(subject.sireNameText, null);
|
|
assert.equal(subject.paternalGrandsireNameText, null);
|
|
assert.equal(subject.ppGranddamNameText, null);
|
|
|
|
const sire = byId(subject.sireId!);
|
|
assert.equal(sire.name, 'Big Buck');
|
|
assert.equal(sire.sex, 'buck');
|
|
assert.equal(sire.status, 'other');
|
|
assert.equal(sire.earNumber, 'BB1');
|
|
assert.equal(sire.weightLbs, 9.5);
|
|
assert.equal(sire.dob, '2022-01-02');
|
|
assert.equal(sire.damId, null);
|
|
|
|
const grandpa = byId(sire.sireId!);
|
|
assert.equal(grandpa.name, 'Grandpa');
|
|
assert.equal(byId(grandpa.damId!).name, 'Great Gran');
|
|
assert.equal(byId(grandpa.damId!).sex, 'doe');
|
|
|
|
const dam = byId(subject.damId!);
|
|
assert.equal(dam.weightLbs, 3);
|
|
assert.equal(dam.dob, null); // "Unknown" isn't a date
|
|
});
|
|
|
|
test('a sibling reuses the ancestors already created', () => {
|
|
const before = db.select().from(rabbits).all().length;
|
|
const sibling = save({
|
|
name: 'Kit Two',
|
|
sireNameText: manual('Big Buck', { ear_number: 'BB1' }),
|
|
damNameText: manual('Nice Doe', { ear_number: 'nd1' }),
|
|
});
|
|
assert.equal(db.select().from(rabbits).all().length, before + 1); // only the sibling itself
|
|
const first = db.select().from(rabbits).where(eq(rabbits.name, 'Kit One')).get()!;
|
|
assert.equal(sibling.sireId, first.sireId);
|
|
assert.equal(sibling.damId, first.damId);
|
|
});
|
|
|
|
test('a line-bred pedigree repeating an animal creates it once', () => {
|
|
const subject = save({
|
|
name: 'Line Bred',
|
|
sireNameText: manual('Son', { ear_number: 'S1' }),
|
|
damNameText: manual('Daughter', { ear_number: 'D1' }),
|
|
paternalGrandsireNameText: manual('Patriarch', { ear_number: 'PAT' }),
|
|
maternalGrandsireNameText: manual('Patriarch', { ear_number: 'PAT' }),
|
|
});
|
|
const sire = byId(subject.sireId!);
|
|
const dam = byId(subject.damId!);
|
|
assert.equal(sire.sireId, dam.sireId);
|
|
});
|
|
|
|
test('name + farm matches only without a conflicting ear number; linked parents are untouched', () => {
|
|
const sameName = save({ name: 'Child A', sireNameText: manual('Common Name', { ear_number: 'X1' }) });
|
|
const otherEar = save({ name: 'Child B', sireNameText: manual('Common Name', { ear_number: 'X2' }) });
|
|
assert.notEqual(sameName.sireId, otherEar.sireId);
|
|
|
|
const linked = save({ name: 'Child C', sireId: sameName.sireId, sireNameText: manual('Ignored') });
|
|
assert.equal(linked.sireId, sameName.sireId);
|
|
assert.equal(db.select().from(rabbits).where(eq(rabbits.name, 'Ignored')).all().length, 0);
|
|
});
|
|
|
|
test('an Unknown parent keeps grandparent text on the rabbit', () => {
|
|
const subject = save({ name: 'Partial', paternalGrandsireNameText: manual('Lonely Grandsire') });
|
|
assert.equal(subject.sireId ?? null, null);
|
|
assert.ok(subject.paternalGrandsireNameText);
|
|
});
|
|
|
|
test('adding an existing rabbit again fills gaps and adds missing ancestors, never overwrites', () => {
|
|
const first = submit({ name: 'Repeat', farm: 'Home', earNumber: 'RP1', breed: 'Holland Lop', sex: 'doe' });
|
|
assert.equal(first.report, null);
|
|
|
|
const again = submit({
|
|
name: 'Repeat',
|
|
farm: 'Home',
|
|
earNumber: 'rp1',
|
|
breed: 'Mini Rex', // already set: must not change
|
|
colorName: 'Broken Black',
|
|
weightLbs: 3.5,
|
|
sireNameText: manual('Repeat Sire', { ear_number: 'RS1' }),
|
|
paternalGrandsireNameText: manual('Repeat Grandsire', { ear_number: 'RG1' }),
|
|
});
|
|
assert.equal(again.id, first.id);
|
|
const row = byId(first.id);
|
|
assert.equal(row.breed, 'Holland Lop');
|
|
assert.equal(row.colorName, 'Broken Black');
|
|
assert.equal(row.weightLbs, 3.5);
|
|
assert.equal(byId(row.sireId!).name, 'Repeat Sire');
|
|
assert.equal(byId(byId(row.sireId!).sireId!).name, 'Repeat Grandsire');
|
|
assert.equal(again.report!.ancestorsAdded, 2);
|
|
assert.deepEqual(again.report!.filled.sort(), ['colorName', 'weightLbs']);
|
|
|
|
// A third pass adds the sire's dam to the existing sire record.
|
|
const third = submit({
|
|
name: 'Repeat',
|
|
earNumber: 'RP1',
|
|
farm: 'Home',
|
|
sireNameText: manual('Repeat Sire', { ear_number: 'RS1', dob: '2021-05-06' }),
|
|
paternalGranddamNameText: manual('Repeat Granddam', { ear_number: 'RD1' }),
|
|
});
|
|
const sire = byId(byId(first.id).sireId!);
|
|
assert.equal(sire.dob, '2021-05-06');
|
|
assert.equal(byId(sire.damId!).name, 'Repeat Granddam');
|
|
assert.equal(third.report!.ancestorsUpdated, 1);
|
|
});
|
|
|
|
test('adding a rabbit that exists only as a reference ancestor promotes it', () => {
|
|
const kit = submit({ name: 'Promo Kit', damNameText: manual('Promo Doe', { ear_number: 'PD1' }) });
|
|
const damId = byId(kit.id).damId!;
|
|
assert.equal(byId(damId).status, 'other');
|
|
|
|
const added = submit({ name: 'Promo Doe', farm: 'Test Farm', earNumber: 'PD1', status: 'active', sex: 'doe' });
|
|
assert.equal(added.id, damId);
|
|
assert.equal(byId(damId).status, 'active');
|
|
});
|
|
|
|
test('a different existing parent is kept and reported as a conflict', () => {
|
|
submit({ name: 'Conflicted', earNumber: 'CF1', sireNameText: manual('Real Sire', { ear_number: 'RSX' }) });
|
|
const again = submit({ name: 'Conflicted', earNumber: 'CF1', sireNameText: manual('Other Sire', { ear_number: 'OSX' }) });
|
|
assert.equal(byId(byId(again.id).sireId!).name, 'Real Sire');
|
|
assert.equal(again.report!.conflicts.length, 1);
|
|
});
|
|
|
|
test('when an ear number is shared, the herd rabbit wins over a reference record', () => {
|
|
const ref = db.insert(rabbits).values({ name: 'Dup', earNumber: 'DUP1', status: 'other' }).returning().get();
|
|
const herd = db.insert(rabbits).values({ name: 'Dup', earNumber: 'DUP1', status: 'active' }).returning().get();
|
|
const kit = submit({ name: 'Dup Kit', sireNameText: manual('Dup', { ear_number: 'DUP1' }) });
|
|
assert.equal(byId(kit.id).sireId, herd.id);
|
|
assert.notEqual(byId(kit.id).sireId, ref.id);
|
|
});
|
|
|
|
test('same name and farm but a different DOB is a different rabbit', () => {
|
|
const a = submit({ name: 'Doe 1', farm: 'Kits', dob: '2025-01-01' });
|
|
const b = submit({ name: 'Doe 1', farm: 'Kits', dob: '2025-06-01' });
|
|
assert.notEqual(a.id, b.id);
|
|
});
|