import { getPageContext, CHIP_LABELS } from '@/components/assistant/DoormileAI/pageContext'; // The LIVE catalogue. src/components/assistant/*.js are dead duplicates with // no importers — AIPanel and Home both import from @/lib/assistant. Testing // the copy that never runs is how a green suite hides a broken page. import { resolveIntentId } from '@/lib/assistant/intents'; /** * Every suggestion a page offers must resolve — and must resolve to the * intent that page means. * * `pageContext.jsx` states the rule at the top of the file: "Every suggestion * here MUST be a phrasing the deterministic matcher in intents.js actually * resolves — a suggestion chip that returns 'I can't answer that yet' is worse * than no chip." It was enforced by reading. It is now enforced here. * * Resolving is not enough on its own, because INTENTS is an ordered catalog * and the FIRST match wins. A question can match the intent it was written for * and still be answered by a different one that happens to run earlier — * `ROADMAP.md` B1/B2 are two shipped instances of exactly that. So these tests * assert on which intent actually wins, not merely that something does. */ // The route of every page that carries the assistant. Sourced from App.jsx. const PAGES = [ '/doormile/orders', '/doormile/bookings', '/doormile/deliveries', '/doormile/control-x', '/doormile/milers', '/doormile/vehicles', '/doormile/hubs', '/doormile/tenants', '/doormile/customers', '/doormile/tripsheets', '/doormile/exceptions', '/doormile/pricing', '/doormile/app-users', '/doormile/competitive-intel', '/doormile/reports' ]; /** The intent the ordered catalog actually reaches for a question. */ const winningIntent = (text) => resolveIntentId(text); const chipsFor = (route) => getPageContext(route).suggestions; describe('every page suggestion resolves to an intent', () => { PAGES.forEach((route) => { it(`${route}`, () => { const chips = chipsFor(route); expect(chips.length).toBeGreaterThan(0); chips.forEach((chip) => { expect(winningIntent(chip.text)).not.toBeNull(); }); }); }); }); describe('a page offers its own subject, not another page\'s', () => { // The intent id a page's questions are allowed to reach. This is the actual // separation being asked for: on Orders, order intents; on Deliveries, // delivery intents. Not "an intent exists" — the RIGHT one. const ALLOWED = { '/doormile/orders': /order|repeat|create|assign|summary|revenue|delay/i, '/doormile/bookings': /order|delay/i, '/doormile/deliveries': /consignment|order|delay|rider/i, '/doormile/control-x': /order|batch|rider|assign|delay/i, '/doormile/milers': /rider|assign/i, '/doormile/vehicles': /vehicle/i, '/doormile/hubs': /hub/i, '/doormile/tenants': /tenant|partner/i, '/doormile/customers': /customer/i, '/doormile/tripsheets': /tripsheet/i, '/doormile/exceptions': /exception|delay/i, '/doormile/pricing': /pricing/i, '/doormile/app-users': /appuser/i, '/doormile/competitive-intel': /competitor|carrier/i }; Object.entries(ALLOWED).forEach(([route, allowed]) => { it(`${route} stays on topic`, () => { chipsFor(route).forEach((chip) => { const id = winningIntent(chip.text); expect(`${route} · "${chip.text}" → ${id}`).toMatch(allowed); }); }); }); }); describe('the two bugs this change fixes', () => { it('Deliveries is no longer the Orders list wearing a different name', () => { const deliveries = chipsFor('/doormile/deliveries').map((c) => c.text); const orders = chipsFor('/doormile/orders').map((c) => c.text); expect(deliveries).not.toEqual(orders); // It used to be `{ ...ORDERS, label: 'Deliveries' }`, so a page of // consignments offered "Create an order" and "Repeat yesterday's orders". expect(deliveries).not.toContain('Create an order'); expect(deliveries).not.toContain("Repeat yesterday's orders"); expect(deliveries).toContain('How many consignments do we have?'); }); it('"carrier pricing rules" would be answered by the WRONG intent', () => { // Kept as a live assertion rather than a comment. carrierPricingCount // documents this exact phrasing for itself, and pricingCount matches // /\bpricing\b/ 57 lines earlier, so the catalog never reaches it. expect(winningIntent('How many carrier pricing rules?')).toBe('pricingCount'); // Which is why Competitive Intel asks it the other way. expect(winningIntent('How many carriers do we have?')).toBe('carrierPricingCount'); }); }); describe('chips are readable in a 393px panel', () => { it('every suggestion has a short label', () => { const missing = []; PAGES.forEach((route) => { chipsFor(route).forEach((chip) => { if (!CHIP_LABELS[chip.text]) missing.push(`${route} · ${chip.text}`); }); }); expect(missing).toEqual([]); }); });