update on the agents and registry side
This commit is contained in:
67
tests/lib/skills/CashExposureSkill.test.js
Normal file
67
tests/lib/skills/CashExposureSkill.test.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { CashExposureSkill } from '@/lib/assistant/skills/definitions/CashExposureSkill';
|
||||
|
||||
describe('CashExposureSkill', () => {
|
||||
const NOW = '2026-08-31 14:00:00';
|
||||
|
||||
it('flags riders carrying total COD cash above max threshold', () => {
|
||||
const rows = [
|
||||
// Rider 1: 12000 total (Critical)
|
||||
{ bookingid: 1, userid: 101, ridername: 'Rider A', orderstatus: 'picked', paymenttype: 'cod', cod_amount: '5000' },
|
||||
{ bookingid: 2, userid: 101, ridername: 'Rider A', orderstatus: 'accepted', paymenttype: 'cod', cod_amount: '7000' },
|
||||
|
||||
// Rider 2: 8000 total (Warning)
|
||||
{ bookingid: 3, userid: 102, ridername: 'Rider B', orderstatus: 'picked', paymenttype: 'cod', cod_amount: '8000' },
|
||||
|
||||
// Rider 3: 4000 total (Safe)
|
||||
{ bookingid: 4, userid: 103, ridername: 'Rider C', orderstatus: 'picked', paymenttype: 'cod', cod_amount: '4000' },
|
||||
|
||||
// Non-COD order for Rider 1 (Ignored)
|
||||
{ bookingid: 5, userid: 101, ridername: 'Rider A', orderstatus: 'picked', paymenttype: 'prepaid', cod_amount: '0' }
|
||||
];
|
||||
|
||||
const findings = CashExposureSkill.evaluate(rows, NOW, { maxCashPerRider: 10000, warningCashPercent: 75 });
|
||||
|
||||
const critical = findings.find((f) => f.id === 'cash_exposure_critical');
|
||||
const warning = findings.find((f) => f.id === 'cash_exposure_warning');
|
||||
|
||||
expect(critical).toBeDefined();
|
||||
expect(critical.count).toBe(1); // Only Rider 1
|
||||
expect(critical.rows.length).toBe(2); // Rider 1 has 2 COD orders
|
||||
expect(critical.detail[0]).toContain('12,000');
|
||||
|
||||
expect(warning).toBeDefined();
|
||||
expect(warning.count).toBe(1); // Only Rider 2
|
||||
expect(warning.detail[0]).toContain('8,000');
|
||||
});
|
||||
|
||||
it('returns no findings when all riders are under warning threshold', () => {
|
||||
const rows = [
|
||||
{ bookingid: 1, userid: 101, orderstatus: 'picked', paymenttype: 'cod', cod_amount: '5000' }
|
||||
];
|
||||
const findings = CashExposureSkill.evaluate(rows, NOW, { maxCashPerRider: 10000, warningCashPercent: 75 });
|
||||
expect(findings.length).toBe(0);
|
||||
});
|
||||
|
||||
it('executes enforce_cash_handoff tool handler', async () => {
|
||||
const tool = CashExposureSkill.tools.find((t) => t.name === 'enforce_cash_handoff');
|
||||
expect(tool).toBeDefined();
|
||||
const result = await tool.handler({ milerId: 101, totalCash: 12000, hubId: 'HUB-1' });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.proposal.action).toBe('cash_handoff');
|
||||
expect(result.proposal.milerId).toBe(101);
|
||||
expect(result.proposal.totalCash).toBe(12000);
|
||||
expect(result.proposal.hubId).toBe('HUB-1');
|
||||
});
|
||||
|
||||
it('handles orders with non-numeric or missing amounts gracefully', () => {
|
||||
const rows = [
|
||||
{ bookingid: 1, userid: 101, orderstatus: 'picked', paymenttype: 'cod' }, // missing amount
|
||||
{ bookingid: 2, userid: 101, orderstatus: 'picked', paymenttype: 'cod', cod_amount: 'invalid' }, // invalid amount
|
||||
{ bookingid: 3, userid: 101, orderstatus: 'picked', paymenttype: 'cod', cod_amount: '12000' } // valid amount
|
||||
];
|
||||
const findings = CashExposureSkill.evaluate(rows, NOW, { maxCashPerRider: 10000, warningCashPercent: 75 });
|
||||
const critical = findings.find((f) => f.id === 'cash_exposure_critical');
|
||||
expect(critical).toBeDefined();
|
||||
expect(critical.detail[0]).toContain('12,000');
|
||||
});
|
||||
});
|
||||
42
tests/lib/skills/DoorstepStallSkill.test.js
Normal file
42
tests/lib/skills/DoorstepStallSkill.test.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { DoorstepStallSkill } from '@/lib/assistant/skills/definitions/DoorstepStallSkill';
|
||||
|
||||
describe('DoorstepStallSkill', () => {
|
||||
const fixedNow = '2026-08-31 12:00:00';
|
||||
|
||||
it('detects riders stalled at door past threshold', () => {
|
||||
const rows = [
|
||||
{
|
||||
bookingid: 1,
|
||||
bookingno: 'BK-1',
|
||||
orderstatus: 'arrived',
|
||||
reachedat: '2026-08-31 11:35:00', // 25 min waiting
|
||||
ridername: 'Karthik Raja',
|
||||
userid: 44,
|
||||
},
|
||||
{
|
||||
bookingid: 2,
|
||||
bookingno: 'BK-2',
|
||||
orderstatus: 'arrived',
|
||||
reachedat: '2026-08-31 11:55:00', // 5 min waiting
|
||||
ridername: 'Praveen',
|
||||
userid: 45,
|
||||
},
|
||||
];
|
||||
|
||||
const findings = DoorstepStallSkill.evaluate(rows, fixedNow, { arrivedStalledMin: 20 });
|
||||
expect(findings.length).toBe(1);
|
||||
expect(findings[0].id).toBe('stalled_at_door');
|
||||
expect(findings[0].count).toBe(1);
|
||||
expect(findings[0].rows[0].bookingid).toBe(1);
|
||||
});
|
||||
|
||||
it('executes ping_stalled_rider tool handler', async () => {
|
||||
const tool = DoorstepStallSkill.tools.find((t) => t.name === 'ping_stalled_rider');
|
||||
expect(tool).toBeDefined();
|
||||
|
||||
const result = await tool.handler({ milerId: 44 });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.proposal.action).toBe('notify_miler');
|
||||
expect(result.proposal.milerId).toBe(44);
|
||||
});
|
||||
});
|
||||
39
tests/lib/skills/FleetBalancerSkill.test.js
Normal file
39
tests/lib/skills/FleetBalancerSkill.test.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { FleetBalancerSkill } from '@/lib/assistant/skills/definitions/FleetBalancerSkill';
|
||||
|
||||
describe('FleetBalancerSkill', () => {
|
||||
it('detects saturated riders when pending orders are waiting in queue', () => {
|
||||
const rows = [
|
||||
// 3 active jobs for rider 10
|
||||
{ bookingid: 1, userid: 10, ridername: 'Rider A', orderstatus: 'picked' },
|
||||
{ bookingid: 2, userid: 10, ridername: 'Rider A', orderstatus: 'active' },
|
||||
{ bookingid: 3, userid: 10, ridername: 'Rider A', orderstatus: 'accepted' },
|
||||
// 1 pending order in queue
|
||||
{ bookingid: 4, userid: null, orderstatus: 'pending' },
|
||||
];
|
||||
|
||||
const findings = FleetBalancerSkill.evaluate(rows, '2026-08-31 12:00:00', { riderActiveCap: 3 });
|
||||
expect(findings.length).toBe(1);
|
||||
expect(findings[0].id).toBe('rider_saturation');
|
||||
expect(findings[0].count).toBe(1);
|
||||
});
|
||||
|
||||
it('does not flag saturation if no unassigned orders are waiting in queue', () => {
|
||||
const rows = [
|
||||
{ bookingid: 1, userid: 10, ridername: 'Rider A', orderstatus: 'picked' },
|
||||
{ bookingid: 2, userid: 10, ridername: 'Rider A', orderstatus: 'active' },
|
||||
{ bookingid: 3, userid: 10, ridername: 'Rider A', orderstatus: 'accepted' },
|
||||
];
|
||||
|
||||
const findings = FleetBalancerSkill.evaluate(rows, '2026-08-31 12:00:00', { riderActiveCap: 3 });
|
||||
expect(findings.length).toBe(0);
|
||||
});
|
||||
|
||||
it('executes balance_rider_load tool handler', async () => {
|
||||
const tool = FleetBalancerSkill.tools.find((t) => t.name === 'balance_rider_load');
|
||||
expect(tool).toBeDefined();
|
||||
|
||||
const result = await tool.handler({ fromMilerId: 10, toMilerId: 20 });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.proposal.action).toBe('rebalance_workload');
|
||||
});
|
||||
});
|
||||
37
tests/lib/skills/HighValueCodSkill.test.js
Normal file
37
tests/lib/skills/HighValueCodSkill.test.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { HighValueCodSkill } from '@/lib/assistant/skills/definitions/HighValueCodSkill';
|
||||
|
||||
describe('HighValueCodSkill', () => {
|
||||
it('detects high value COD consignments at or above threshold', () => {
|
||||
const rows = [
|
||||
{
|
||||
bookingid: 1,
|
||||
bookingno: 'BK-COD-1',
|
||||
orderstatus: 'active',
|
||||
paymenttype: 'COD',
|
||||
orderamount: 4500,
|
||||
},
|
||||
{
|
||||
bookingid: 2,
|
||||
bookingno: 'BK-COD-2',
|
||||
orderstatus: 'active',
|
||||
paymenttype: 'COD',
|
||||
orderamount: 1500,
|
||||
},
|
||||
];
|
||||
|
||||
const findings = HighValueCodSkill.evaluate(rows, '2026-08-31 12:00:00', { codRiskThresholdAmount: 3000 });
|
||||
expect(findings.length).toBe(1);
|
||||
expect(findings[0].id).toBe('high_value_cod_alert');
|
||||
expect(findings[0].count).toBe(1);
|
||||
expect(findings[0].rows[0].bookingid).toBe(1);
|
||||
});
|
||||
|
||||
it('executes enforce_otp_verification tool handler', async () => {
|
||||
const tool = HighValueCodSkill.tools.find((t) => t.name === 'enforce_otp_verification');
|
||||
expect(tool).toBeDefined();
|
||||
|
||||
const result = await tool.handler({ bookingId: 1 });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.proposal.action).toBe('require_otp');
|
||||
});
|
||||
});
|
||||
50
tests/lib/skills/HubCongestionSkill.test.js
Normal file
50
tests/lib/skills/HubCongestionSkill.test.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import { HubCongestionSkill } from '@/lib/assistant/skills/definitions/HubCongestionSkill';
|
||||
|
||||
describe('HubCongestionSkill', () => {
|
||||
const NOW = '2026-08-31 14:00:00';
|
||||
|
||||
it('detects parcels stalled at hub beyond dwell threshold', () => {
|
||||
const rows = [
|
||||
{ bookingid: 1, bookingno: 'BK-1', orderstatus: 'pending', orderdate: '2026-08-31 13:00:00' }, // 60 min
|
||||
{ bookingid: 2, bookingno: 'BK-2', orderstatus: 'accepted', orderdate: '2026-08-31 13:10:00' }, // 50 min
|
||||
{ bookingid: 3, bookingno: 'BK-3', orderstatus: 'pending', orderdate: '2026-08-31 13:50:00' }, // 10 min — not stalled
|
||||
{ bookingid: 4, bookingno: 'BK-4', orderstatus: 'picked', orderdate: '2026-08-31 12:00:00' }, // in transit — excluded
|
||||
];
|
||||
|
||||
const findings = HubCongestionSkill.evaluate(rows, NOW, { hubDwellMinutes: 45 });
|
||||
expect(findings.length).toBe(1);
|
||||
expect(findings[0].id).toBe('hub_parcel_stalled');
|
||||
expect(findings[0].count).toBe(2);
|
||||
expect(findings[0].rows.map((r) => r.bookingid)).toContain(1);
|
||||
expect(findings[0].rows.map((r) => r.bookingid)).toContain(2);
|
||||
expect(findings[0].rows.map((r) => r.bookingid)).not.toContain(3);
|
||||
});
|
||||
|
||||
it('returns no findings when all parcels are within dwell threshold', () => {
|
||||
const rows = [
|
||||
{ bookingid: 5, orderstatus: 'pending', orderdate: '2026-08-31 13:50:00' }, // 10 min
|
||||
];
|
||||
const findings = HubCongestionSkill.evaluate(rows, NOW, { hubDwellMinutes: 45 });
|
||||
expect(findings.length).toBe(0);
|
||||
});
|
||||
|
||||
it('executes dispatch_hub_idle_parcels tool handler', async () => {
|
||||
const tool = HubCongestionSkill.tools.find((t) => t.name === 'dispatch_hub_idle_parcels');
|
||||
expect(tool).toBeDefined();
|
||||
const result = await tool.handler({ bookingIds: [1, 2], milerId: 99 });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.proposal.action).toBe('dispatch_hub_parcels');
|
||||
expect(result.proposal.bookingIds).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('respects custom dwell threshold', () => {
|
||||
const rows = [
|
||||
{ bookingid: 10, orderstatus: 'pending', orderdate: '2026-08-31 13:45:00' }, // 15 min
|
||||
];
|
||||
const findingsStrict = HubCongestionSkill.evaluate(rows, NOW, { hubDwellMinutes: 10 });
|
||||
expect(findingsStrict.length).toBe(1);
|
||||
|
||||
const findingsLenient = HubCongestionSkill.evaluate(rows, NOW, { hubDwellMinutes: 30 });
|
||||
expect(findingsLenient.length).toBe(0);
|
||||
});
|
||||
});
|
||||
53
tests/lib/skills/LateDispatchSkill.test.js
Normal file
53
tests/lib/skills/LateDispatchSkill.test.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import { LateDispatchSkill } from '@/lib/assistant/skills/definitions/LateDispatchSkill';
|
||||
|
||||
describe('LateDispatchSkill', () => {
|
||||
const NOW = '2026-08-31 14:00:00';
|
||||
|
||||
it('detects critically late dispatch orders', () => {
|
||||
const rows = [
|
||||
{ bookingid: 1, bookingno: 'BK-1', orderstatus: 'pending', orderdate: '2026-08-31 11:30:00' }, // 150 min
|
||||
{ bookingid: 2, bookingno: 'BK-2', orderstatus: 'pending', orderdate: '2026-08-31 12:30:00' }, // 90 min
|
||||
{ bookingid: 3, bookingno: 'BK-3', orderstatus: 'pending', orderdate: '2026-08-31 13:20:00' }, // 40 min — warning only
|
||||
{ bookingid: 4, bookingno: 'BK-4', orderstatus: 'pending', orderdate: '2026-08-31 13:50:00' }, // 10 min — no issue
|
||||
{ bookingid: 5, bookingno: 'BK-5', orderstatus: 'picked', orderdate: '2026-08-31 11:00:00' }, // dispatched — excluded
|
||||
];
|
||||
|
||||
const findings = LateDispatchSkill.evaluate(rows, NOW, { lateDispatchMinutes: 30, criticalDispatchMinutes: 60 });
|
||||
|
||||
const critical = findings.find((f) => f.id === 'late_dispatch_critical');
|
||||
const warning = findings.find((f) => f.id === 'late_dispatch_warning');
|
||||
|
||||
expect(critical).toBeDefined();
|
||||
expect(critical.count).toBe(2); // BK-1 and BK-2
|
||||
|
||||
expect(warning).toBeDefined();
|
||||
expect(warning.count).toBe(1); // BK-3
|
||||
});
|
||||
|
||||
it('returns no findings when all orders are dispatched quickly', () => {
|
||||
const rows = [
|
||||
{ bookingid: 1, orderstatus: 'pending', orderdate: '2026-08-31 13:55:00' }, // 5 min
|
||||
];
|
||||
const findings = LateDispatchSkill.evaluate(rows, NOW, { lateDispatchMinutes: 30, criticalDispatchMinutes: 60 });
|
||||
expect(findings.length).toBe(0);
|
||||
});
|
||||
|
||||
it('executes trigger_auto_dispatch tool handler', async () => {
|
||||
const tool = LateDispatchSkill.tools.find((t) => t.name === 'trigger_auto_dispatch');
|
||||
expect(tool).toBeDefined();
|
||||
const result = await tool.handler({ bookingIds: [1, 2], reason: 'Test late dispatch' });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.proposal.action).toBe('auto_dispatch');
|
||||
expect(result.proposal.bookingIds).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('excludes non-pending status orders', () => {
|
||||
const rows = [
|
||||
{ bookingid: 1, orderstatus: 'accepted', orderdate: '2026-08-31 11:00:00' },
|
||||
{ bookingid: 2, orderstatus: 'picked', orderdate: '2026-08-31 11:00:00' },
|
||||
{ bookingid: 3, orderstatus: 'delivered', orderdate: '2026-08-31 11:00:00' },
|
||||
];
|
||||
const findings = LateDispatchSkill.evaluate(rows, NOW, { lateDispatchMinutes: 30, criticalDispatchMinutes: 60 });
|
||||
expect(findings.length).toBe(0);
|
||||
});
|
||||
});
|
||||
39
tests/lib/skills/RiderBatterySafetySkill.test.js
Normal file
39
tests/lib/skills/RiderBatterySafetySkill.test.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import { RiderBatterySafetySkill } from '@/lib/assistant/skills/definitions/RiderBatterySafetySkill';
|
||||
|
||||
describe('RiderBatterySafetySkill', () => {
|
||||
it('detects active deliveries where rider device battery is critical', () => {
|
||||
const rows = [
|
||||
{
|
||||
bookingid: 1,
|
||||
bookingno: 'BK-1',
|
||||
orderstatus: 'picked',
|
||||
userid: 50,
|
||||
ridername: 'Karthik',
|
||||
battery_percentage: 12,
|
||||
},
|
||||
{
|
||||
bookingid: 2,
|
||||
bookingno: 'BK-2',
|
||||
orderstatus: 'picked',
|
||||
userid: 51,
|
||||
ridername: 'Praveen',
|
||||
battery_percentage: 85,
|
||||
},
|
||||
];
|
||||
|
||||
const findings = RiderBatterySafetySkill.evaluate(rows, '2026-08-31 12:00:00', { criticalBatteryPercent: 15 });
|
||||
expect(findings.length).toBe(1);
|
||||
expect(findings[0].id).toBe('rider_critical_battery');
|
||||
expect(findings[0].count).toBe(1);
|
||||
expect(findings[0].rows[0].bookingid).toBe(1);
|
||||
});
|
||||
|
||||
it('executes alert_low_battery_rider tool handler', async () => {
|
||||
const tool = RiderBatterySafetySkill.tools.find((t) => t.name === 'alert_low_battery_rider');
|
||||
expect(tool).toBeDefined();
|
||||
|
||||
const result = await tool.handler({ milerId: 50 });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.proposal.action).toBe('battery_alert');
|
||||
});
|
||||
});
|
||||
78
tests/lib/skills/SlaGuardianSkill.test.js
Normal file
78
tests/lib/skills/SlaGuardianSkill.test.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { SlaGuardianSkill } from '@/lib/assistant/skills/definitions/SlaGuardianSkill';
|
||||
|
||||
describe('SlaGuardianSkill', () => {
|
||||
const fixedNow = '2026-08-31 12:00:00';
|
||||
|
||||
it('detects breached SLA orders', () => {
|
||||
const rows = [
|
||||
{
|
||||
bookingid: 1,
|
||||
bookingno: 'BK-1',
|
||||
orderstatus: 'pending',
|
||||
expecteddeliverytime: '2026-08-31 11:30:00', // 30 min overdue
|
||||
},
|
||||
{
|
||||
bookingid: 2,
|
||||
bookingno: 'BK-2',
|
||||
orderstatus: 'delivered', // terminal, should be ignored
|
||||
expecteddeliverytime: '2026-08-31 11:30:00',
|
||||
},
|
||||
];
|
||||
|
||||
const findings = SlaGuardianSkill.evaluate(rows, fixedNow);
|
||||
const breach = findings.find((f) => f.id === 'sla_breached');
|
||||
|
||||
expect(breach).toBeDefined();
|
||||
expect(breach.count).toBe(1);
|
||||
expect(breach.rows[0].bookingid).toBe(1);
|
||||
});
|
||||
|
||||
it('detects at-risk SLA orders within custom threshold window', () => {
|
||||
const rows = [
|
||||
{
|
||||
bookingid: 10,
|
||||
bookingno: 'BK-10',
|
||||
orderstatus: 'accepted',
|
||||
expecteddeliverytime: '2026-08-31 12:20:00', // due in 20 min
|
||||
},
|
||||
];
|
||||
|
||||
// Default window is 45 min -> should match
|
||||
const findings = SlaGuardianSkill.evaluate(rows, fixedNow, { slaRiskWindowMin: 45 });
|
||||
expect(findings.some((f) => f.id === 'sla_at_risk')).toBe(true);
|
||||
|
||||
// If threshold overridden to 15 min -> should not match 20 min
|
||||
const restricted = SlaGuardianSkill.evaluate(rows, fixedNow, { slaRiskWindowMin: 15 });
|
||||
expect(restricted.some((f) => f.id === 'sla_at_risk')).toBe(false);
|
||||
});
|
||||
|
||||
it('detects unassigned aging orders', () => {
|
||||
const rows = [
|
||||
{
|
||||
bookingid: 20,
|
||||
bookingno: 'BK-20',
|
||||
orderstatus: 'pending',
|
||||
orderdate: '2026-08-31 10:30:00', // 90 min old
|
||||
},
|
||||
];
|
||||
|
||||
const findings = SlaGuardianSkill.evaluate(rows, fixedNow, { unassignedAgingMin: 60 });
|
||||
expect(findings.some((f) => f.id === 'unassigned_aging')).toBe(true);
|
||||
});
|
||||
|
||||
it('executes reassign_sla_critical_order tool handler', async () => {
|
||||
const tool = SlaGuardianSkill.tools.find((t) => t.name === 'reassign_sla_critical_order');
|
||||
expect(tool).toBeDefined();
|
||||
|
||||
const result = await tool.handler({
|
||||
bookingId: 101,
|
||||
targetMilerId: 55,
|
||||
reason: 'Urgent priority reassignment',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.proposal.action).toBe('reassign_miler');
|
||||
expect(result.proposal.bookingId).toBe(101);
|
||||
expect(result.proposal.milerId).toBe(55);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user