178 lines
5.9 KiB
TypeScript
178 lines
5.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { scopeContextToTimeframe, scopeScalarsToTimeframe, UNAVAILABLE } from '../insightScope.ts';
|
|
|
|
/**
|
|
* Real shape of the FCR page's 7-day history for this cycle: the IoT scale
|
|
* stopped reporting at day 38 and has written 0 ever since, and the flock was
|
|
* fully harvested on day 48 so `ayam` ends at 0 too.
|
|
*/
|
|
const ROWS = [
|
|
{ day: 42, fcr: 1.8, pakan: 1064, bobot_kg: 0, ayam: 1424 },
|
|
{ day: 44, fcr: 1.81, pakan: 1067, bobot_kg: 0, ayam: 1314 },
|
|
{ day: 46, fcr: 1.82, pakan: 1072, bobot_kg: 0, ayam: 1253 },
|
|
{ day: 47, fcr: 1.78, pakan: 1073, bobot_kg: 1.38, ayam: 822 },
|
|
{ day: 48, fcr: 1.7, pakan: 1073, bobot_kg: 0, ayam: 0 },
|
|
];
|
|
|
|
const spec = (agg: 'last' | 'lastValid') => ({
|
|
historyKey: 'tren',
|
|
derive: {
|
|
bobot_kg_out: { field: 'bobot_kg', agg },
|
|
ayam_out: { field: 'ayam', agg },
|
|
},
|
|
});
|
|
|
|
describe('lastValid aggregation', () => {
|
|
it('last takes the trailing zero — the reported bug', () => {
|
|
const out = scopeScalarsToTimeframe({ tren: ROWS }, 'mingguan', spec('last'));
|
|
expect(out.bobot_kg_out).toBe(0);
|
|
expect(out.ayam_out).toBe(0);
|
|
});
|
|
|
|
it('lastValid walks back past the zeros', () => {
|
|
const out = scopeScalarsToTimeframe({ tren: ROWS }, 'mingguan', spec('lastValid'));
|
|
expect(out.bobot_kg_out).toBe(1.38);
|
|
expect(out.ayam_out).toBe(822);
|
|
});
|
|
|
|
it('reports unavailable when every reading in the window is zero', () => {
|
|
const allZero = [
|
|
{ day: 47, bobot_kg: 0, ayam: 0 },
|
|
{ day: 48, bobot_kg: 0, ayam: 0 },
|
|
];
|
|
const out = scopeScalarsToTimeframe({ tren: allZero }, 'mingguan', spec('lastValid'));
|
|
expect(out.bobot_kg_out).toBe(UNAVAILABLE);
|
|
expect(out.ayam_out).toBe(UNAVAILABLE);
|
|
});
|
|
|
|
it('leaves a genuine non-zero final reading alone', () => {
|
|
const rows = [
|
|
{ day: 47, bobot_kg: 1.3, ayam: 900 },
|
|
{ day: 48, bobot_kg: 1.45, ayam: 850 },
|
|
];
|
|
const out = scopeScalarsToTimeframe({ tren: rows }, 'mingguan', spec('lastValid'));
|
|
expect(out.bobot_kg_out).toBe(1.45);
|
|
expect(out.ayam_out).toBe(850);
|
|
});
|
|
});
|
|
|
|
describe('harian periode label starts at day 0', () => {
|
|
it('labels cumulative window as hari ke-0 s/d ke-N even if first row is day 2', () => {
|
|
const rows = [
|
|
{ day: 2, fcr: 1.1 },
|
|
{ day: 5, fcr: 1.2 },
|
|
{ day: 8, fcr: 1.3 },
|
|
];
|
|
const out = scopeScalarsToTimeframe(
|
|
{ tren: rows, fcr_terakhir: 1.3 },
|
|
'harian',
|
|
{
|
|
historyKey: 'tren',
|
|
derive: {
|
|
fcr_out: { field: 'fcr', agg: 'lastValid', replaces: 'fcr_terakhir' },
|
|
},
|
|
},
|
|
8
|
|
);
|
|
expect(out.periode).toBe('hari ke-0 s/d ke-8');
|
|
expect(out.fcr_out).toBe(1.3);
|
|
});
|
|
});
|
|
|
|
describe('replaces writes to both new key and original key for backend', () => {
|
|
it('writes to fcr_out AND fcr_terakhir when replaces is set', () => {
|
|
const rows = [
|
|
{ hari: 30, fcr_aktual: 1.7 },
|
|
{ hari: 35, fcr_aktual: 1.72 },
|
|
{ hari: 39, fcr_aktual: 1.68 },
|
|
{ hari: 48, fcr_aktual: 1.72 },
|
|
];
|
|
// First slice the history to day 39, then compute scalars
|
|
const sliced = scopeContextToTimeframe(
|
|
{ tren_fcr_harian: rows, fcr_terakhir: 1.72 },
|
|
'harian',
|
|
39
|
|
);
|
|
const out = scopeScalarsToTimeframe(
|
|
sliced as Record<string, unknown>,
|
|
'harian',
|
|
{
|
|
historyKey: 'tren_fcr_harian',
|
|
derive: {
|
|
fcrTerakhir_rasio: { field: 'fcr_aktual', agg: 'lastValid', replaces: 'fcr_terakhir' },
|
|
},
|
|
},
|
|
39
|
|
);
|
|
// New key with unit suffix
|
|
expect(out.fcrTerakhir_rasio).toBe(1.68);
|
|
// Original key for backend grading
|
|
expect(out.fcr_terakhir).toBe(1.68);
|
|
});
|
|
|
|
it('works for EEF page with multiple replaces', () => {
|
|
const rows = [
|
|
{ hari: 30, eef: 340, fcr: 1.55, persen_hidup: 96.5, bw_panen: 1800, umur_panen_hari: 30 },
|
|
{ hari: 35, eef: 350, fcr: 1.52, persen_hidup: 96.2, bw_panen: 2100, umur_panen_hari: 35 },
|
|
{ hari: 39, eef: 355, fcr: 1.5, persen_hidup: 95.8, bw_panen: 2250, umur_panen_hari: 39 },
|
|
{ hari: 48, eef: 229, fcr: 1.72, persen_hidup: 90.1, bw_panen: 2380, umur_panen_hari: 48 },
|
|
];
|
|
// First slice the history to day 39, then compute scalars
|
|
const sliced = scopeContextToTimeframe(
|
|
{
|
|
tren_eef_harian: rows,
|
|
eef_terakhir: 229,
|
|
fcr_terakhir: 1.72,
|
|
persen_hidup: 90.1,
|
|
bw_panen_gram: 2380,
|
|
umur_panen_rata_hari: 48,
|
|
},
|
|
'harian',
|
|
39
|
|
);
|
|
const out = scopeScalarsToTimeframe(
|
|
sliced as Record<string, unknown>,
|
|
'harian',
|
|
{
|
|
historyKey: 'tren_eef_harian',
|
|
derive: {
|
|
eefTerakhir_indeksTanpaSatuan: {
|
|
field: 'eef',
|
|
agg: 'lastValid',
|
|
replaces: 'eef_terakhir',
|
|
},
|
|
fcrTerakhir_rasio: { field: 'fcr', agg: 'lastValid', replaces: 'fcr_terakhir' },
|
|
persenHidupTerakhir_persen: {
|
|
field: 'persen_hidup',
|
|
agg: 'lastValid',
|
|
replaces: 'persen_hidup',
|
|
},
|
|
bobotPanenTerakhir_gram: {
|
|
field: 'bw_panen',
|
|
agg: 'lastValid',
|
|
replaces: 'bw_panen_gram',
|
|
},
|
|
umurPanenTerakhir_hari: {
|
|
field: 'umur_panen_hari',
|
|
agg: 'lastValid',
|
|
replaces: 'umur_panen_rata_hari',
|
|
},
|
|
},
|
|
},
|
|
39
|
|
);
|
|
// Verify day 39 values are used, not day 48
|
|
expect(out.eefTerakhir_indeksTanpaSatuan).toBe(355);
|
|
expect(out.fcrTerakhir_rasio).toBe(1.5);
|
|
expect(out.persenHidupTerakhir_persen).toBe(95.8);
|
|
expect(out.bobotPanenTerakhir_gram).toBe(2250);
|
|
expect(out.umurPanenTerakhir_hari).toBe(39);
|
|
// Verify original keys are also updated for backend
|
|
expect(out.eef_terakhir).toBe(355);
|
|
expect(out.fcr_terakhir).toBe(1.5);
|
|
expect(out.persen_hidup).toBe(95.8);
|
|
expect(out.bw_panen_gram).toBe(2250);
|
|
expect(out.umur_panen_rata_hari).toBe(39);
|
|
});
|
|
});
|