updates on the ui and updates agents
This commit is contained in:
157
tests/lib/findingReport.test.js
Normal file
157
tests/lib/findingReport.test.js
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Finding persistence.
|
||||
*
|
||||
* The fingerprint is the part worth testing hardest. It decides whether a
|
||||
* finding seen on two consecutive polls is the SAME finding — and if it is
|
||||
* wrong, the "how long has this been open" measurement, which is the single
|
||||
* most useful thing the table records, is destroyed.
|
||||
*/
|
||||
jest.mock('@/api/doormile', () => ({
|
||||
reportFindings: jest.fn().mockResolvedValue({}),
|
||||
reportFindingActed: jest.fn().mockResolvedValue({})
|
||||
}));
|
||||
|
||||
const api = require('@/api/doormile');
|
||||
const { fingerprintOf, reportScan, reportActed, __resetForTests } =
|
||||
require('@/lib/assistant/agent/findingReport');
|
||||
|
||||
const finding = (skillId, bookingids, extra = {}) => ({
|
||||
skillId,
|
||||
severity: 'warning',
|
||||
title: 'Something needs attention',
|
||||
proposal: { tool: 'notifyRider', scope: bookingids.map((bookingid) => ({ bookingid })) },
|
||||
...extra
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
__resetForTests();
|
||||
// The module reads tenantid from localStorage.
|
||||
localStorage.setItem('tenantid', '7');
|
||||
});
|
||||
|
||||
describe('fingerprintOf', () => {
|
||||
it('is stable across scans regardless of scope order', () => {
|
||||
// Scope order is not guaranteed between scans; an unsorted fingerprint
|
||||
// would make the same problem look new every poll.
|
||||
expect(fingerprintOf(finding('SlaGuardian', [3, 1, 2])))
|
||||
.toBe(fingerprintOf(finding('SlaGuardian', [1, 2, 3])));
|
||||
});
|
||||
|
||||
it('ignores severity, so an escalating finding stays the same finding', () => {
|
||||
// An ageing SLA breach climbs warning -> critical while the underlying
|
||||
// problem is unchanged. Including severity would reset firstseenat and
|
||||
// report a six-hour-old problem as brand new.
|
||||
const a = fingerprintOf(finding('SlaGuardian', [1], { severity: 'warning' }));
|
||||
const b = fingerprintOf(finding('SlaGuardian', [1], { severity: 'critical' }));
|
||||
expect(a).toBe(b);
|
||||
});
|
||||
|
||||
it('distinguishes two proposals from ONE skill over the same bookings', () => {
|
||||
// SlaGuardian raises both "notify the riders" and "assign these orders"
|
||||
// for the same parcels. Keyed on skill + scope alone these collide, and
|
||||
// since the fingerprint is the upsert key one would silently overwrite the
|
||||
// other. Found by running the real page, not by these tests.
|
||||
const notify = finding('SlaGuardian', [1, 2, 3]);
|
||||
const assign = { ...finding('SlaGuardian', [1, 2, 3]), proposal: { tool: 'assignMiler', scope: [1, 2, 3].map((bookingid) => ({ bookingid })) } };
|
||||
expect(fingerprintOf(notify)).not.toBe(fingerprintOf(assign));
|
||||
});
|
||||
|
||||
it('distinguishes different skills on the same booking', () => {
|
||||
expect(fingerprintOf(finding('SlaGuardian', [1])))
|
||||
.not.toBe(fingerprintOf(finding('CashExposure', [1])));
|
||||
});
|
||||
|
||||
it('distinguishes different scopes', () => {
|
||||
expect(fingerprintOf(finding('SlaGuardian', [1])))
|
||||
.not.toBe(fingerprintOf(finding('SlaGuardian', [1, 2])));
|
||||
});
|
||||
|
||||
it('survives a missing scope rather than throwing', () => {
|
||||
expect(fingerprintOf({ skillId: 'SlaGuardian' })).toBe('SlaGuardian:none:');
|
||||
expect(fingerprintOf({})).toBe('unknown:none:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('reportScan', () => {
|
||||
it('sends ids only, never personal data', async () => {
|
||||
await reportScan([finding('SlaGuardian', [11])]);
|
||||
const payload = api.reportFindings.mock.calls[0][0];
|
||||
const serialised = JSON.stringify(payload);
|
||||
expect(payload.findings[0].bookingids).toEqual([11]);
|
||||
// The findings table measures the skills; it is not a second copy of the
|
||||
// bookings.
|
||||
expect(serialised).not.toMatch(/phone|address|recipient|ridername/i);
|
||||
});
|
||||
|
||||
it('reports what cleared since the previous scan', async () => {
|
||||
await reportScan([finding('SlaGuardian', [1]), finding('CashExposure', [2])]);
|
||||
api.reportFindings.mockClear();
|
||||
|
||||
await reportScan([finding('SlaGuardian', [1])]);
|
||||
const payload = api.reportFindings.mock.calls[0][0];
|
||||
expect(payload.cleared).toEqual(['CashExposure:notifyRider:2']);
|
||||
});
|
||||
|
||||
it('skips the round trip when nothing is open and nothing cleared', async () => {
|
||||
await reportScan([]);
|
||||
expect(api.reportFindings).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('still reports when everything clears at once', async () => {
|
||||
await reportScan([finding('SlaGuardian', [1])]);
|
||||
api.reportFindings.mockClear();
|
||||
await reportScan([]);
|
||||
expect(api.reportFindings).toHaveBeenCalledWith({
|
||||
findings: [], cleared: ['SlaGuardian:notifyRider:1']
|
||||
});
|
||||
});
|
||||
|
||||
it('never throws when the backend refuses', async () => {
|
||||
api.reportFindings.mockRejectedValueOnce(new Error('403'));
|
||||
// A dispatcher looking at a stalled parcel does not care that telemetry is
|
||||
// down, and must not be shown an error about it.
|
||||
await expect(reportScan([finding('SlaGuardian', [1])])).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('retries the clear set after a failure instead of losing it', async () => {
|
||||
await reportScan([finding('SlaGuardian', [1])]);
|
||||
api.reportFindings.mockClear();
|
||||
|
||||
// The scan where it clears fails...
|
||||
api.reportFindings.mockRejectedValueOnce(new Error('503'));
|
||||
await reportScan([]);
|
||||
|
||||
// ...so the next scan must still report it cleared. Advancing `previous`
|
||||
// on a failed post would drop the clear permanently and leave the finding
|
||||
// open forever.
|
||||
await reportScan([]);
|
||||
const last = api.reportFindings.mock.calls.at(-1)[0];
|
||||
expect(last.cleared).toEqual(['SlaGuardian:notifyRider:1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reportActed', () => {
|
||||
it('records a clean success as ok', async () => {
|
||||
await reportActed(finding('SlaGuardian', [1]), { ok: true });
|
||||
expect(api.reportFindingActed).toHaveBeenCalledWith('SlaGuardian:notifyRider:1', 'ok');
|
||||
});
|
||||
|
||||
it('records a partial success as partial, not ok', async () => {
|
||||
// Six riders notified out of eight is not a success. Flattening it would
|
||||
// lose exactly the distinction actions.js preserves.
|
||||
await reportActed(finding('SlaGuardian', [1, 2]), { ok: true, unreachable: ['DM-2 (no device)'] });
|
||||
expect(api.reportFindingActed).toHaveBeenCalledWith('SlaGuardian:notifyRider:1,2', 'partial');
|
||||
});
|
||||
|
||||
it('records a failure', async () => {
|
||||
await reportActed(finding('SlaGuardian', [1]), { ok: false });
|
||||
expect(api.reportFindingActed).toHaveBeenCalledWith('SlaGuardian:notifyRider:1', 'failed');
|
||||
});
|
||||
|
||||
it('never throws when the backend refuses', async () => {
|
||||
api.reportFindingActed.mockRejectedValueOnce(new Error('500'));
|
||||
// The action itself already happened and was reported to the operator.
|
||||
await expect(reportActed(finding('SlaGuardian', [1]), { ok: true })).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user