Files
doormilxpress_astryx/tests/lib/agentClickThrough.test.jsx

173 lines
6.2 KiB
JavaScript

// The wiring nothing else covers.
//
// The executor is tested in isolation, and SlaRemediationCard is tested in
// isolation. What was never tested is the seam between them: does the banner
// actually hand the card an executor, does a click reach the real
// executeProposal, does the API get called, and does the board re-evaluate
// afterwards.
//
// That seam is where this feature would fail silently — a card rendered with no
// onRemediate looks identical to one wired correctly until somebody clicks it.
import React from 'react';
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import AgentOperationsBanner from '@/components/doormile/AgentOperationsBanner';
import * as scanModule from '@/lib/assistant/scan';
jest.mock('@/lib/assistant/scan');
jest.mock('lucide-react', () =>
new Proxy({}, { get: (_t, prop) => (prop === '__esModule' ? true : (props) => <span data-testid={`icon-${String(prop)}`} {...props} />) })
);
// Only the network is faked. AgentFactory, SkillRegistry, the skills, the
// adapter, briefing and the executor are all the real implementations.
jest.mock('@/api/doormile', () => {
const actual = jest.requireActual('@/api/doormile/queries');
return {
__esModule: true,
getMilers: jest.fn(),
notifyRider: jest.fn(),
batchAssignBookings: jest.fn(),
buildMilerLookup: actual.buildMilerLookup,
mapBookingStatusToDeliveryStatus: actual.mapBookingStatusToDeliveryStatus
};
});
jest.mock('@/api/doormile/notify', () => ({ OpenToast: jest.fn() }));
const api = require('@/api/doormile');
const { OpenToast } = require('@/api/doormile/notify');
const client = () => new QueryClient({ defaultOptions: { queries: { retry: false } } });
// A rider stamped arrival well over the 20-minute stall threshold.
const stalledScan = () => ({
truncated: false,
rows: [
{
bookingid: 2,
bookingno: 'DM-BK-002',
status: 'Pickup_Scheduled',
createdat: '2026-09-01 10:00:00',
reachedat: '2026-09-01 13:00:00',
assignedmileruserid: 8,
milername: 'Kumar',
serviceoptions: []
}
]
});
beforeEach(() => {
jest.clearAllMocks();
jest.useFakeTimers({ doNotFake: ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'queueMicrotask'] });
jest.setSystemTime(new Date('2026-09-01T14:00:00'));
scanModule.scanBookings.mockResolvedValue(stalledScan());
api.getMilers.mockResolvedValue([{ userid: 8, milerprofileid: 501, displayname: 'Kumar' }]);
api.notifyRider.mockResolvedValue({ success: true });
});
afterEach(() => {
jest.useRealTimers();
});
const renderBanner = () =>
render(
<QueryClientProvider client={client()}>
<MemoryRouter>
<AgentOperationsBanner />
</MemoryRouter>
</QueryClientProvider>
);
describe('click-through: banner to executor to API', () => {
it('surfaces a real stall finding from raw bookings', async () => {
renderBanner();
expect(await screen.findByText(/stalled at door/i)).toBeInTheDocument();
});
// The whole point. A card with no executor renders disabled and says
// "Review only"; if the banner failed to pass onRemediate this would fail.
it('renders the stall proposal as clickable, not review-only', async () => {
renderBanner();
await screen.findByText(/stalled at door/i);
expect(screen.queryByRole('button', { name: /review only/i })).not.toBeInTheDocument();
expect(await screen.findByRole('button', { name: /message waiting riders/i })).toBeEnabled();
});
it('a click reaches the real executor and pushes to the right rider', async () => {
renderBanner();
await screen.findByText(/stalled at door/i);
fireEvent.click(await screen.findByRole('button', { name: /message waiting riders/i }));
await waitFor(() => {
expect(api.notifyRider).toHaveBeenCalledTimes(1);
});
// Through the REAL buildMilerLookup: mileruserid 8 becomes profile 501.
expect(api.notifyRider.mock.calls[0][0]).toBe(501);
expect(api.notifyRider.mock.calls[0][0]).not.toBe(8);
});
it('re-scans after acting, which is what makes it a loop', async () => {
renderBanner();
await screen.findByText(/stalled at door/i);
const scansBefore = scanModule.scanBookings.mock.calls.length;
fireEvent.click(await screen.findByRole('button', { name: /message waiting riders/i }));
// The verify pass: the board is re-evaluated rather than assumed fixed.
await waitFor(() => {
expect(scanModule.scanBookings.mock.calls.length).toBeGreaterThan(scansBefore);
});
});
it('reports success only after the push actually landed', async () => {
renderBanner();
await screen.findByText(/stalled at door/i);
fireEvent.click(await screen.findByRole('button', { name: /message waiting riders/i }));
await waitFor(() => {
expect(OpenToast).toHaveBeenCalledWith(expect.stringMatching(/Notified 1 rider/), 'success');
});
});
// The failure that matters: the push is refused, and the operator must not be
// told a rider was messaged.
it('reports failure, not success, when the push is refused', async () => {
api.notifyRider.mockRejectedValue(new Error('device offline'));
renderBanner();
await screen.findByText(/stalled at door/i);
fireEvent.click(await screen.findByRole('button', { name: /message waiting riders/i }));
await waitFor(() => {
expect(OpenToast).toHaveBeenCalledWith(expect.any(String), 'error');
});
expect(OpenToast).not.toHaveBeenCalledWith(expect.anything(), 'success');
});
// A rider who has left the fleet cannot be pushed to. Nothing landed, so
// nothing may be reported as landed.
it('does not claim success when no rider could be reached', async () => {
api.getMilers.mockResolvedValue([]); // rider 8 is gone
renderBanner();
await screen.findByText(/stalled at door/i);
fireEvent.click(await screen.findByRole('button', { name: /message waiting riders/i }));
await waitFor(() => {
expect(OpenToast).toHaveBeenCalledWith(expect.any(String), 'error');
});
expect(api.notifyRider).not.toHaveBeenCalled();
expect(OpenToast).not.toHaveBeenCalledWith(expect.anything(), 'success');
});
});