updates on the ui and updates agents
This commit is contained in:
@@ -38,16 +38,31 @@ beforeEach(() => {
|
||||
|
||||
describe('which proposals can be executed', () => {
|
||||
it('only verbs with a real endpoint behind them', () => {
|
||||
expect(executableTools().sort()).toEqual(['notifyRider']);
|
||||
// alert_low_battery_rider joined notifyRider: it targets the same
|
||||
// POST /admin/milers/:id/notify, so it was never a missing capability —
|
||||
// just a verb nothing had been wired to.
|
||||
expect(executableTools().sort()).toEqual([
|
||||
'alert_low_battery_rider', 'assignMiler', 'notifyRider', 'trigger_auto_dispatch'
|
||||
]);
|
||||
});
|
||||
|
||||
it('canExecuteProposal is false for the skills tools with no endpoint', () => {
|
||||
['trigger_auto_dispatch', 'enforce_cash_handoff', 'enforce_otp_verification', 'alert_low_battery_rider']
|
||||
// Still review-only. Each needs an endpoint that does not exist yet —
|
||||
// seed.go marks all three Target: "none yet".
|
||||
['enforce_cash_handoff', 'enforce_otp_verification', 'dispatch_hub_idle_parcels']
|
||||
.forEach((tool) => {
|
||||
expect(canExecuteProposal(finding(tool, [row()]))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('alert_low_battery_rider notifies the riders on the finding', async () => {
|
||||
const result = await executeProposal(finding('alert_low_battery_rider', [row()]));
|
||||
expect(result.ok).toBe(true);
|
||||
// Keyed on milerprofileid, not userid — the two are different small
|
||||
// integers on the same record and the wrong one fails silently.
|
||||
expect(api.notifyRider).toHaveBeenCalledWith(99, expect.any(String), expect.stringMatching(/charge/i));
|
||||
});
|
||||
|
||||
// Resolving quietly would let a caller mistake "nothing happened" for success.
|
||||
it('throws rather than silently doing nothing', async () => {
|
||||
await expect(executeProposal(finding('enforce_cash_handoff', [row()]))).rejects.toThrow(/review-only/i);
|
||||
@@ -130,18 +145,92 @@ describe('notifyRider', () => {
|
||||
});
|
||||
|
||||
describe('assignMiler', () => {
|
||||
// Deliberately review-only in the console. The branch executed it through
|
||||
// POST /hub/bookings/batch-assign, which is behind HubStaffAuth (roleid 6
|
||||
// only) — every admin/manager/executive click would 403. Until an admin
|
||||
// batch-assign route exists, the card must say "Review only", not fail.
|
||||
it('is not executable, so its card renders "Review only"', () => {
|
||||
expect(canExecuteProposal(finding('assignMiler', [row({ bookingid: 1 })]))).toBe(false);
|
||||
// This was review-only because POST /hub/bookings/batch-assign is behind
|
||||
// HubStaffAuth (roleid 6 only) and 403s for every token this console issues.
|
||||
// It is executable now that POST /admin/bookings/batch-assign runs the same
|
||||
// solver under admin auth.
|
||||
it('is executable', () => {
|
||||
expect(canExecuteProposal(finding('assignMiler', [row({ bookingid: 1 })]))).toBe(true);
|
||||
});
|
||||
|
||||
it('throws instead of pretending, and never calls the hub-only endpoint', async () => {
|
||||
await expect(executeProposal(finding('assignMiler', [row()]))).rejects.toThrow(/review-only/);
|
||||
it('sends the findings distinct booking ids, deduplicated', async () => {
|
||||
api.batchAssignBookings.mockResolvedValue({ assigned: 2, skipped: 0, riderssequenced: 1, results: [] });
|
||||
await executeProposal(finding('assignMiler', [
|
||||
row({ bookingid: 11 }), row({ bookingid: 12 }), row({ bookingid: 11 })
|
||||
]));
|
||||
expect(api.batchAssignBookings).toHaveBeenCalledWith([11, 12]);
|
||||
});
|
||||
|
||||
// The rule the whole module exists for: never report an action that did not
|
||||
// happen. Four of nine landing is four of nine.
|
||||
it('reports a partial assignment as partial, naming the reasons', async () => {
|
||||
api.batchAssignBookings.mockResolvedValue({
|
||||
assigned: 1, skipped: 1, riderssequenced: 1,
|
||||
results: [
|
||||
{ bookingid: 11, bookingno: 'DM-11', assigned: true, mileruserid: 7 },
|
||||
{ bookingid: 12, bookingno: 'DM-12', assigned: false, reason: 'no available rider under capacity' }
|
||||
]
|
||||
});
|
||||
const res = await executeProposal(finding('assignMiler', [row({ bookingid: 11 }), row({ bookingid: 12 })]));
|
||||
expect(res.ok).toBe(true);
|
||||
// Declared so reportActed records "partial" rather than "ok". Caught by
|
||||
// driving the real page: the message said partial, the telemetry said ok.
|
||||
expect(res.partial).toBe(true);
|
||||
expect(res.message).toMatch(/Assigned 1 order/);
|
||||
expect(res.message).toMatch(/DM-12 \(no available rider under capacity\)/);
|
||||
});
|
||||
|
||||
// The mocks above return a FLAT object, but batchAssignBookings returns
|
||||
// `response.data` — and utils.OK wraps everything as {success, data}. So
|
||||
// production hands the executor {data: {assigned, skipped, ...}}. Testing
|
||||
// only the flat shape would leave the real one unproven.
|
||||
it('reads the real utils.OK envelope, not just a flat object', async () => {
|
||||
api.batchAssignBookings.mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
assigned: 1, skipped: 1, riderssequenced: 1,
|
||||
results: [
|
||||
{ bookingid: 11, bookingno: 'DM-11', assigned: true, mileruserid: 7 },
|
||||
{ bookingid: 12, bookingno: 'DM-12', assigned: false, reason: 'no available rider under capacity' }
|
||||
]
|
||||
}
|
||||
});
|
||||
const res = await executeProposal(finding('assignMiler', [row({ bookingid: 11 }), row({ bookingid: 12 })]));
|
||||
expect(res.ok).toBe(true);
|
||||
expect(res.message).toMatch(/Assigned 1 order/);
|
||||
expect(res.message).toMatch(/DM-12/);
|
||||
});
|
||||
|
||||
it('is not ok when nothing was assigned', async () => {
|
||||
api.batchAssignBookings.mockResolvedValue({
|
||||
assigned: 0, skipped: 1, riderssequenced: 0,
|
||||
results: [{ bookingid: 12, bookingno: 'DM-12', assigned: false, reason: 'no available rider under capacity' }]
|
||||
});
|
||||
const res = await executeProposal(finding('assignMiler', [row({ bookingid: 12 })]));
|
||||
expect(res.ok).toBe(false);
|
||||
expect(res.sourceCalls[0].status).toBe('error');
|
||||
});
|
||||
|
||||
// Stop sequencing needs ROUTE_OPTIMIZER_URL set in the cluster. Saying
|
||||
// nothing when it is zero leaves the operator assuming routes were ordered.
|
||||
it('says so when stops were not sequenced', async () => {
|
||||
api.batchAssignBookings.mockResolvedValue({ assigned: 2, skipped: 0, riderssequenced: 0, results: [] });
|
||||
const res = await executeProposal(finding('assignMiler', [row({ bookingid: 11 }), row({ bookingid: 12 })]));
|
||||
expect(res.message).toMatch(/not sequenced/i);
|
||||
});
|
||||
|
||||
it('refuses findings with no booking rather than calling the endpoint', async () => {
|
||||
const res = await executeProposal(finding('assignMiler', [row({ bookingid: undefined })]));
|
||||
expect(res.ok).toBe(false);
|
||||
expect(api.batchAssignBookings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reports a thrown error instead of claiming success', async () => {
|
||||
api.batchAssignBookings.mockRejectedValue(new Error('403 forbidden'));
|
||||
const res = await executeProposal(finding('assignMiler', [row({ bookingid: 11 })]));
|
||||
expect(res.ok).toBe(false);
|
||||
expect(res.message).toMatch(/403/);
|
||||
});
|
||||
});
|
||||
|
||||
// The gap the tests above have: they mock buildMilerLookup, so the id
|
||||
|
||||
Reference in New Issue
Block a user