import { describe, it, expect } from 'vitest'; import type { FeedSackColumnConfig } from '../../types/counting.ts'; /** * The classification the insight context now uses: camera -> type comes from * the saved column configuration, and a camera absent from it is not feed-sack * data at all. Mirrors `cameraTypeByName` / `sumFeedRows` in * FeedSackCountingPage. */ const classify = ( logs: Array<{ cameraName: string; count: number }>, config: FeedSackColumnConfig[] ) => { const byName = new Map(config.map((c) => [c.cameraName, c.type])); const totals = { masuk: 0, dituang: 0, keluar: 0, diabaikan: 0 }; for (const log of logs) { const type = byName.get(log.cameraName); if (type === 'inbound') totals.masuk += log.count; else if (type === 'usage') totals.dituang += log.count; else if (type === 'outbound') totals.keluar += log.count; else totals.diabaikan += log.count; } return totals; }; // Exactly what feed_sack_column_config holds in the database. const CONFIG: FeedSackColumnConfig[] = [ { id: 'a', label: 'Karung Masuk', cameraName: 'kandang_1_karung_masuk', color: '#22c55e', type: 'inbound', }, { id: 'b', label: 'Karung Dituang Kandang Atas', cameraName: 'karung_tuang_feeder_kiri_atas', color: '#f97316', type: 'usage', }, { id: 'c', label: 'Karung Dituang Kandang Bawah', cameraName: 'karung_tuang_feeder_kanan_bawah', color: '#a855f7', type: 'usage', }, { id: 'd', label: 'Karung Keluar', cameraName: 'kandang_1_karung_masuk_out', color: '#3b82f6', type: 'outbound', }, ]; // Real totals from the live API for cycle days 43-49 (3-9 Jul 2026). const WEEK_43_49 = [ { cameraName: 'karung_tuang_feeder_kanan_bawah', count: 11 }, { cameraName: 'karung_tuang_feeder_kiri_atas', count: 10 }, { cameraName: 'kandang_1_karung_masuk_out', count: 0 }, { cameraName: 'kandang_1_karung_masuk', count: 14 }, { cameraName: 'kandang_atas_feeder_kiri', count: 1057 }, { cameraName: 'kandang_bawah_feeder_kanan', count: 1127 }, ]; describe('feed sack classification', () => { it('counts only configured cameras', () => { const t = classify(WEEK_43_49, CONFIG); expect(t.dituang).toBe(21); // was 2205 when unconfigured cameras leaked in expect(t.masuk).toBe(14); expect(t.keluar).toBe(0); expect(t.diabaikan).toBe(2184); }); it('gives a believable daily average', () => { const t = classify(WEEK_43_49, CONFIG); expect(Math.round(t.dituang / 7)).toBe(3); // was 315 karung/hari }); it('files kandang_1_karung_masuk_out as outbound, not inbound', () => { // The old substring rule matched "karung_masuk" inside "..._out". const t = classify([{ cameraName: 'kandang_1_karung_masuk_out', count: 387 }], CONFIG); expect(t.keluar).toBe(387); expect(t.masuk).toBe(0); }); it('ignores a camera that is not feed-sack data', () => { const t = classify([{ cameraName: 'kandang_atas_feeder_kiri', count: 7695 }], CONFIG); expect(t.dituang).toBe(0); expect(t.diabaikan).toBe(7695); }); }); /** Manual entries for the same cameras, days 43-49 (3-9 Jul 2026). */ const MANUAL_43_49 = [ { cameraName: 'kandang_1_karung_masuk', count: 8 }, // 3 Jul { cameraName: 'kandang_1_karung_masuk', count: 6 }, // 6 Jul { cameraName: 'kandang_1_karung_masuk', count: 2 }, // 7 Jul ]; describe('manual figures shown beside the camera figures', () => { it('totals manual separately from IOT', () => { const iot = classify(WEEK_43_49, CONFIG); const manual = classify(MANUAL_43_49, CONFIG); expect(iot.masuk).toBe(14); expect(manual.masuk).toBe(16); }); it('never merges the two into one number', () => { // Both count the same sacks; 30 would be double-counting. const iot = classify(WEEK_43_49, CONFIG); const manual = classify(MANUAL_43_49, CONFIG); expect(iot.masuk + manual.masuk).toBe(30); expect(iot.masuk).not.toBe(30); expect(manual.masuk).not.toBe(30); }); it('reports zero manual for a camera with no entries in the window', () => { const manual = classify(MANUAL_43_49, CONFIG); expect(manual.dituang).toBe(0); // manual "dituang" entries stop at 2 Jul }); });