123 lines
4.9 KiB
JavaScript
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([]);
|
|
});
|
|
});
|