142 lines
5.6 KiB
JavaScript
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));
|
|
});
|
|
});
|