import { buildBriefing } from '@/lib/assistant/agent/briefing'; import { toAgentRow } from '@/lib/assistant/agent/normalise'; const NOW = '2026-08-29 14:00:00'; // Raw rows as `/admin/bookings` actually returns them — backend status enums, // createdat, assignedmileruserid, and the promise buried in serviceoptions. // The agent's rules read none of these field names directly, which is the whole // reason the adapter exists. const rawBooking = (over = {}) => ({ bookingid: 1, bookingno: 'DM-BK-001', status: 'Pending_Pickup', createdat: '2026-08-29 13:50:00', serviceoptions: [{ estimateddeliveryat: undefined }], ...over }); describe('toAgentRow', () => { it('derives the normalised status from the backend enum', () => { expect(toAgentRow(rawBooking({ status: 'Pending_Pickup' })).orderstatus).toBe('pending'); }); it('reads the promised time out of the first service option', () => { const row = toAgentRow(rawBooking({ serviceoptions: [{ estimateddeliveryat: '2026-08-29 12:00:00' }] })); expect(row.expecteddeliverytime).toBe('2026-08-29 12:00:00'); }); it('leaves the promise undefined when there is no service option', () => { expect(toAgentRow(rawBooking({ serviceoptions: [] })).expecteddeliverytime).toBeUndefined(); expect(toAgentRow(rawBooking({ serviceoptions: undefined })).expecteddeliverytime).toBeUndefined(); }); it('maps the rider id off assignedmileruserid', () => { expect(toAgentRow(rawBooking({ assignedmileruserid: 7 })).userid).toBe(7); }); it('uses createdat as the order date', () => { expect(toAgentRow(rawBooking()).orderdate).toBe('2026-08-29 13:50:00'); }); it('falls back to a hashed id when the booking has no number', () => { expect(toAgentRow(rawBooking({ bookingno: undefined, bookingid: 42 })).orderid).toBe('#42'); }); it('does not throw on an empty row', () => { expect(() => toAgentRow({})).not.toThrow(); }); }); describe('buildBriefing over raw scan rows', () => { // The regression this file exists for. The rules were written against the // normalised Deliveries shape while the scan returns raw rows, so every rule // read undefined and the agent reported an all-clear board no matter what was // actually wrong. It failed silently and linted clean. it('finds a breach in a RAW row, not just a normalised one', () => { const scan = { truncated: false, rows: [ rawBooking({ status: 'Pickup_Scheduled', serviceoptions: [{ estimateddeliveryat: '2026-08-29 12:00:00' }] }) ] }; const briefing = buildBriefing(scan, NOW); expect(briefing.findings.map((f) => f.id)).toContain('sla_breached'); expect(briefing.headline).not.toMatch(/Nothing needs attention/i); }); it('finds aging unassigned work in a RAW row', () => { const scan = { truncated: false, rows: [rawBooking({ createdat: '2026-08-29 11:00:00' })] }; expect(buildBriefing(scan, NOW).findings.map((f) => f.id)).toContain('unassigned_aging'); }); it('reports an all-clear only when the board really is clear', () => { const scan = { truncated: false, rows: [rawBooking({ status: 'Delivered', createdat: '2026-08-29 13:55:00' })] }; const briefing = buildBriefing(scan, NOW); expect(briefing.findings).toEqual([]); expect(briefing.headline).toMatch(/Nothing needs attention/i); }); // A truncated scan is a floor, not a total — the assistant's standing // contract. Counts must say so rather than implying completeness. it('marks counts as a floor when the scan was truncated', () => { const scan = { truncated: true, rows: [ rawBooking({ status: 'Pickup_Scheduled', serviceoptions: [{ estimateddeliveryat: '2026-08-29 12:00:00' }] }) ] }; const briefing = buildBriefing(scan, NOW); expect(briefing.headline).toMatch(/at least/); expect(briefing.detail).toMatch(/floor/i); }); it('survives a scan with no rows at all', () => { expect(buildBriefing({ rows: [] }, NOW).findings).toEqual([]); expect(buildBriefing(undefined, NOW).findings).toEqual([]); }); // The bug this pins: buildBriefing used to default `now` to // new Date().toISOString(), which is UTC. parseDoormileTimestamp strips the // zone and reads the digits as IST wall-clock, so the rules received a clock // running 5h30m slow and nothing under five and a half hours overdue was ever // flagged. It failed silently — an all-clear board through most of a shift. // // Deliberately exercised with NO explicit `now`, because passing one is what // hid the bug from every other test in this file. it('uses IST wall-clock for "now", not a UTC ISO string', () => { jest.useFakeTimers(); try { jest.setSystemTime(new Date('2026-08-29T14:00:00')); const scan = { truncated: false, rows: [ rawBooking({ status: 'Pickup_Scheduled', // 90 minutes ago in local wall-clock. Under a UTC-shifted clock // this reads as still in the future and yields no finding. serviceoptions: [{ estimateddeliveryat: '2026-08-29 12:30:00' }] }) ] }; const briefing = buildBriefing(scan); expect(briefing.findings.map((f) => f.id)).toContain('sla_breached'); } finally { jest.useRealTimers(); } }); it('carries a metric the panel can render', () => { const scan = { truncated: false, rows: [rawBooking({ createdat: '2026-08-29 11:00:00' })] }; const { metric } = buildBriefing(scan, NOW); expect(metric.value).toBeGreaterThan(0); expect(metric.label).toEqual(expect.any(String)); }); });