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