Files
doormilxpress_astryx/tests/lib/agentBriefing.test.js

142 lines
5.6 KiB
JavaScript

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));
});
});