168 lines
6.2 KiB
JavaScript
168 lines
6.2 KiB
JavaScript
// Does asking for a status sweep actually reach the agent?
|
|
//
|
|
// The regex was verified in isolation, which proves nothing about routing:
|
|
// answerQuestion walks 26 intents in order and returns the FIRST whose match()
|
|
// recognises the text and whose run() resolves. Several later intents match on
|
|
// bare substrings ("orders", "riders", "status") that appear in almost every
|
|
// phrasing of "what needs attention", so ordering is the whole ballgame — and
|
|
// ordering is exactly what a unit test of the regex cannot see.
|
|
|
|
jest.mock('pages/api/doormileApi', () => ({
|
|
__esModule: true,
|
|
getBookingsPage: jest.fn(),
|
|
getHubs: jest.fn(async () => []),
|
|
getVehicles: jest.fn(async () => []),
|
|
getTripsheets: jest.fn(async () => []),
|
|
getExceptions: jest.fn(async () => []),
|
|
getAppUsers: jest.fn(async () => []),
|
|
getAdminCustomers: jest.fn(async () => []),
|
|
getAdminPricing: jest.fn(async () => []),
|
|
getMilers: jest.fn(async () => []),
|
|
getConsignments: jest.fn(async () => []),
|
|
getPartners: jest.fn(async () => []),
|
|
getCompetitorBranches: jest.fn(async () => []),
|
|
getCarrierPricing: jest.fn(async () => []),
|
|
getMilerSummary: jest.fn(async () => []),
|
|
getMilerActivity: jest.fn(async () => []),
|
|
trackConsignment: jest.fn(async () => null),
|
|
getConsignmentLogs: jest.fn(async () => []),
|
|
getAdminTenant: jest.fn(async () => null),
|
|
getTenantLocations: jest.fn(async () => [])
|
|
}));
|
|
|
|
jest.mock('pages/api/api', () => ({
|
|
__esModule: true,
|
|
getalltenants: jest.fn(async () => []),
|
|
getallridersummary: jest.fn(async () => [])
|
|
}));
|
|
|
|
let getBookingsPage;
|
|
|
|
// intents.js keeps a module-local page cache with a 20s TTL, so a second test
|
|
// would otherwise be answered from the first test's fixture. Re-requiring the
|
|
// module per test gives each one a clean cache.
|
|
//
|
|
// Time is pinned too: the agent's rules are all relative to `now`, and the
|
|
// intent reads the real clock. Without a fixed instant these assertions would
|
|
// pass or fail depending on the hour the suite happened to run.
|
|
const NOW = new Date('2026-08-29T14:00:00');
|
|
let answerQuestion;
|
|
|
|
// One page of raw bookings holding a breach, a stalled rider and aging work.
|
|
const page = () => ({
|
|
total: 3,
|
|
rows: [
|
|
{
|
|
bookingid: 1,
|
|
bookingno: 'DM-BK-001',
|
|
status: 'Pickup_Scheduled',
|
|
createdat: '2026-08-29 09:00:00',
|
|
assignedmileruserid: 7,
|
|
milername: 'Suresh',
|
|
serviceoptions: [{ estimateddeliveryat: '2026-08-29 12:30:00' }]
|
|
},
|
|
{
|
|
bookingid: 2,
|
|
bookingno: 'DM-BK-002',
|
|
status: 'Pickup_Scheduled',
|
|
createdat: '2026-08-29 10:00:00',
|
|
reachedat: '2026-08-29 13:15:00',
|
|
assignedmileruserid: 8,
|
|
milername: 'Kumar',
|
|
serviceoptions: []
|
|
},
|
|
{ bookingid: 3, bookingno: 'DM-BK-003', status: 'Pending_Pickup', createdat: '2026-08-29 11:30:00', serviceoptions: [] }
|
|
]
|
|
});
|
|
|
|
beforeEach(() => {
|
|
jest.useFakeTimers({ doNotFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval'] });
|
|
jest.setSystemTime(NOW);
|
|
jest.resetModules();
|
|
// resetModules rebuilds intents.js AND its dependencies, so the mock has to
|
|
// be re-required and re-configured here. Configuring the previous instance
|
|
// would leave the copy intents.js actually holds returning undefined.
|
|
|
|
({ getBookingsPage } = require('pages/api/doormileApi'));
|
|
getBookingsPage.mockResolvedValue(page());
|
|
({ answerQuestion } = require('@/lib/assistant/intents'));
|
|
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
describe('routing to the ops agent', () => {
|
|
const sweepPhrasings = [
|
|
'what needs attention',
|
|
'anything going wrong',
|
|
'what needs my attention right now',
|
|
'ops check',
|
|
'daily briefing',
|
|
'what should i do'
|
|
];
|
|
|
|
it.each(sweepPhrasings)('"%s" reaches the agent', async (question) => {
|
|
const result = await answerQuestion(question);
|
|
expect(result).toBeTruthy();
|
|
expect(result.intentId).toBe('opsBriefing');
|
|
});
|
|
|
|
// The other side of the contract. These must keep going to the intents that
|
|
// answer them properly — over-claiming here would silently change answers
|
|
// across the whole catalog.
|
|
const mustNotClaim = [
|
|
'how many orders today',
|
|
'how many riders are active',
|
|
'total revenue today',
|
|
'how many tenants',
|
|
// Narrowed on port: the branch claimed these, which would have replaced the
|
|
// delayed-orders count (Orders taxonomy) and the operations summary chip
|
|
// with a briefing. They keep going to the intents that answer them.
|
|
'late orders',
|
|
'which orders are delayed?',
|
|
"give me today's operations summary"
|
|
];
|
|
|
|
it.each(mustNotClaim)('"%s" is NOT swallowed by the agent', async (question) => {
|
|
const result = await answerQuestion(question);
|
|
expect(result?.intentId).not.toBe('opsBriefing');
|
|
});
|
|
});
|
|
|
|
describe('the agent answer itself', () => {
|
|
it('reports the real findings from the scanned rows', async () => {
|
|
const result = await answerQuestion('what needs attention');
|
|
expect(result.headline).toMatch(/need.*attention now/i);
|
|
|
|
// Assert on the evidence, not on a skill's prose. The wording belongs to
|
|
// the skill definition and an ops lead may reasonably reword it; the
|
|
// booking reference and the overdue figure are the substance.
|
|
expect(result.detail).toMatch(/DM-BK-001/);
|
|
expect(result.detail).toMatch(/90 min overdue/);
|
|
});
|
|
|
|
// The verifiability contract: every answer shows which endpoint produced it.
|
|
it('carries a source call naming the endpoint it queried', async () => {
|
|
const result = await answerQuestion('what needs attention');
|
|
expect(Array.isArray(result.sourceCalls)).toBe(true);
|
|
expect(result.sourceCalls.length).toBeGreaterThan(0);
|
|
expect(result.sourceCalls[0].target).toMatch(/\/admin\/bookings/);
|
|
});
|
|
|
|
it('carries a metric the panel can render', async () => {
|
|
const result = await answerQuestion('what needs attention');
|
|
expect(result.metric.value).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('says the board is clear when it is', async () => {
|
|
getBookingsPage.mockResolvedValue({ total: 1, rows: [
|
|
{ bookingid: 9, bookingno: 'DM-BK-009', status: 'Delivered', createdat: '2026-08-29 13:59:00', serviceoptions: [] }
|
|
] });
|
|
const result = await answerQuestion('what needs attention');
|
|
expect(result.intentId).toBe('opsBriefing');
|
|
expect(result.headline).toMatch(/Nothing needs attention/i);
|
|
});
|
|
});
|