Files
doormilxpress_astryx/tests/assistant/pageSuggestions.test.js

123 lines
4.9 KiB
JavaScript

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([]);
});
});