update on the agents and registry side
This commit is contained in:
167
tests/lib/agentRouting.test.js
Normal file
167
tests/lib/agentRouting.test.js
Normal file
@@ -0,0 +1,167 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user