// One engine, one set of thresholds. // // The chat panel and the Exceptions banner used to run different code for the // same five rules, with the thresholds hardcoded in one and tunable in the // other. They disagreed out of the box — a doorstep stall was 25 minutes in the // chat panel and 20 on the banner — and moving a slider in Agent Studio changed // one surface and not the other. // // These tests exist to stop that coming back. They are less about any single // rule than about the property that both surfaces are the same engine. import { buildBriefing } from '@/lib/assistant/agent/briefing'; import { AgentFactory } from '@/lib/assistant/agent/AgentFactory'; import { SkillRegistry } from '@/lib/assistant/skills/SkillRegistry'; import { toAgentRows } from '@/lib/assistant/agent/normalise'; import * as signals from '@/lib/assistant/agent/signals'; const NOW = '2026-08-29 14:00:00'; const rawBooking = (over = {}) => ({ bookingid: 1, bookingno: 'DM-BK-001', status: 'Pickup_Scheduled', createdat: '2026-08-29 13:50:00', serviceoptions: [], ...over }); // A board with something wrong on it: a breach, a doorstep stall and aging work. const busyScan = () => ({ truncated: false, rows: [ rawBooking({ bookingid: 1, bookingno: 'DM-BK-001', serviceoptions: [{ estimateddeliveryat: '2026-08-29 12:30:00' }] }), rawBooking({ bookingid: 2, bookingno: 'DM-BK-002', createdat: '2026-08-29 10:00:00', reachedat: '2026-08-29 13:15:00', assignedmileruserid: 8, milername: 'Kumar' }), rawBooking({ bookingid: 3, bookingno: 'DM-BK-003', status: 'Pending_Pickup', createdat: '2026-08-29 11:30:00' }) ] }); beforeEach(() => { // Thresholds persist to localStorage, so one test's override would otherwise // leak into the next. SkillRegistry.clearRegistryConfig(); }); describe('the chat panel and the banner are the same engine', () => { it('produces identical findings for identical rows', () => { const scan = busyScan(); // What the chat panel shows. const fromBriefing = buildBriefing(scan, NOW).findings; // What the Exceptions banner shows — its exact call. const fromBanner = AgentFactory.synthesizeDefaultAgent().evaluateTelemetry(toAgentRows(scan.rows), NOW); expect(fromBriefing.map((f) => f.id)).toEqual(fromBanner.map((f) => f.id)); expect(fromBriefing.map((f) => f.count)).toEqual(fromBanner.map((f) => f.count)); }); // The regression that started this: the chat panel knew five rules while the // registry defined twelve findings across eight skills, so asking "what needs // attention" could never surface cash exposure, hub congestion or a rider on // a dying battery. it('the chat panel sees every enabled skill, not a private subset', () => { // Enable all eight through the registry, as an admin could. By default three // are off (no data source yet — see their `dataGap`), so "more than five" // no longer holds out of the box; the point is that whatever the registry // enables, the chat panel runs. SkillRegistry.applyRegistryConfig( SkillRegistry.getAllSkills().map((s) => ({ skillid: s.id, enabled: true, thresholds: {} })) ); const enabled = SkillRegistry.getActiveSkills(); expect(enabled).toHaveLength(8); const agent = AgentFactory.synthesizeDefaultAgent(); expect(agent.activeSkillCount).toBe(8); }); it('ships the three skills with no data source switched off', () => { SkillRegistry.clearRegistryConfig(); const off = SkillRegistry.getAllSkills().filter((s) => !s.enabled).map((s) => s.id).sort(); expect(off).toEqual(['skill_cash_exposure', 'skill_high_value_cod', 'skill_rider_battery_safety']); off.forEach((id) => expect(SkillRegistry.getSkill(id).dataGap).toMatch(/\S/)); }); }); describe('no hardcoded thresholds survive', () => { // signals.js is now a shared-primitives module. If a THRESHOLDS object or a // rule reappears here, the two-engine bug is back. it('signals.js exports no rules and no threshold table', () => { expect(signals.THRESHOLDS).toBeUndefined(); expect(signals.diagnose).toBeUndefined(); expect(signals.breachedSla).toBeUndefined(); expect(signals.atRiskSla).toBeUndefined(); expect(signals.unassignedAging).toBeUndefined(); expect(signals.stalledAtDoor).toBeUndefined(); expect(signals.saturatedRiders).toBeUndefined(); }); it('signals.js still provides the shared primitives', () => { expect(typeof signals.wallClockNow).toBe('function'); expect(typeof signals.ALL_CLEAR).toBe('string'); }); // Every threshold an operator can see must be declared by a skill, with the // metadata Agent Studio needs to render a slider for it. it('every skill threshold is declared with tunable bounds', () => { SkillRegistry.getAllSkills().forEach((skill) => { Object.entries(skill.thresholds || {}).forEach(([key, cfg]) => { expect(typeof cfg.value).toBe('number'); expect(typeof cfg.min).toBe('number'); expect(typeof cfg.max).toBe('number'); expect(cfg.min).toBeLessThanOrEqual(cfg.value); expect(cfg.value).toBeLessThanOrEqual(cfg.max); // A threshold with no label cannot be rendered as a slider, so it would // silently become un-tunable — hardcoded by omission. if (!cfg.label) { throw new Error(`${skill.id}.${key} has no label; Agent Studio cannot render a slider for it`); } }); }); }); }); describe('tuning a threshold moves both surfaces', () => { // The property that was broken: an override applied in Agent Studio reached // the banner and never reached the chat panel. it('an override changes what the chat panel reports', () => { const scan = { truncated: false, rows: [ rawBooking({ bookingid: 2, createdat: '2026-08-29 10:00:00', // Stalled for 22 minutes: inside the old hardcoded 25, outside the // skill's default 20. Exactly the parcel the two engines disagreed on. reachedat: '2026-08-29 13:38:00', assignedmileruserid: 8, milername: 'Kumar' }) ] }; const before = buildBriefing(scan, NOW).findings.map((f) => f.id); expect(before).toContain('stalled_at_door'); // Raise the stall timeout past 22 minutes; the finding must disappear from // the chat panel too, not just the banner. SkillRegistry.applyRegistryConfig([{ skillid: 'skill_doorstep_stall', enabled: true, thresholds: { arrivedStalledMin: 45 } }]); const after = buildBriefing(scan, NOW).findings.map((f) => f.id); expect(after).not.toContain('stalled_at_door'); }); });