135 lines
5.5 KiB
JavaScript
135 lines
5.5 KiB
JavaScript
import React from 'react';
|
|
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
|
import AgentOperationsBanner from '@/components/doormile/AgentOperationsBanner';
|
|
import * as scanModule from '@/lib/assistant/scan';
|
|
import { AgentFactory } from '@/lib/assistant/agent/AgentFactory';
|
|
import { SkillRegistry } from '@/lib/assistant/skills/SkillRegistry';
|
|
|
|
jest.mock('lucide-react', () =>
|
|
new Proxy({}, { get: (_t, prop) => (prop === '__esModule' ? true : (props) => <span data-testid={`icon-${String(prop)}`} {...props} />) })
|
|
);
|
|
jest.mock('@/lib/assistant/scan');
|
|
jest.mock('@/lib/assistant/agent/AgentFactory');
|
|
|
|
const createTestQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
|
|
function Where() {
|
|
const loc = useLocation();
|
|
return <div data-testid="where">{loc.pathname + loc.search}</div>;
|
|
}
|
|
|
|
const renderBanner = (props = {}) =>
|
|
render(
|
|
<QueryClientProvider client={createTestQueryClient()}>
|
|
<MemoryRouter initialEntries={['/doormile/exceptions']}>
|
|
<Routes>
|
|
<Route path="*" element={<><AgentOperationsBanner {...props} /><Where /></>} />
|
|
</Routes>
|
|
</MemoryRouter>
|
|
</QueryClientProvider>
|
|
);
|
|
|
|
const SLA_FINDING = {
|
|
id: 'sla_breached',
|
|
skillId: 'skill_sla_guardian',
|
|
severity: 'critical',
|
|
title: '1 parcel is past the promised delivery time',
|
|
why: 'Estimated delivery time has passed.',
|
|
count: 1,
|
|
rows: [{ bookingid: 101, bookingno: 'BK-101' }],
|
|
detail: ['BK-101 — 35 min overdue'],
|
|
proposal: {
|
|
label: 'Notify the assigned riders',
|
|
tool: 'notifyRider',
|
|
scope: [{ bookingid: 101 }],
|
|
blastRadius: 'Sends a message per rider.',
|
|
},
|
|
};
|
|
|
|
describe('AgentOperationsBanner', () => {
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
SkillRegistry.clearRegistryConfig();
|
|
});
|
|
|
|
it('says nothing is flagged, naming how many skills looked', async () => {
|
|
scanModule.scanBookings.mockResolvedValue({ rows: [], truncated: false });
|
|
AgentFactory.synthesizeDefaultAgent.mockReturnValue({ evaluateTelemetry: () => [] });
|
|
renderBanner();
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Nothing flagged by the 5 enabled skills/i)).toBeInTheDocument();
|
|
});
|
|
expect(screen.getByRole('button', { name: /Configure skills/i })).toBeInTheDocument();
|
|
});
|
|
|
|
it('renders remediation cards when findings are detected', async () => {
|
|
scanModule.scanBookings.mockResolvedValue({ rows: [{ bookingid: 101 }], truncated: false });
|
|
AgentFactory.synthesizeDefaultAgent.mockReturnValue({ evaluateTelemetry: () => [SLA_FINDING] });
|
|
renderBanner({ title: 'Custom Title' });
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Custom Title/i)).toBeInTheDocument();
|
|
expect(screen.getByText(/1 parcel is past the promised delivery time/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('filters findings by category', async () => {
|
|
scanModule.scanBookings.mockResolvedValue({ rows: [{ bookingid: 101 }], truncated: false });
|
|
AgentFactory.synthesizeDefaultAgent.mockReturnValue({
|
|
evaluateTelemetry: () => [
|
|
{ ...SLA_FINDING, proposal: undefined },
|
|
{
|
|
id: 'rider_critical_battery',
|
|
skillId: 'skill_rider_battery_safety', // category: rider_safety
|
|
severity: 'warning',
|
|
title: 'Rider battery critical',
|
|
why: 'Low battery',
|
|
count: 1,
|
|
rows: [{ bookingid: 102 }],
|
|
},
|
|
],
|
|
});
|
|
renderBanner({ category: 'rider_safety', title: 'Rider Safety Only' });
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Rider Safety Only/i)).toBeInTheDocument();
|
|
expect(screen.getByText(/Rider battery critical/i)).toBeInTheDocument();
|
|
expect(screen.queryByText(/1 parcel is past the promised delivery time/i)).toBeNull();
|
|
});
|
|
});
|
|
|
|
// The branch opened a localStorage-only settings modal here. Settings now
|
|
// live in the agent registry, edited on one page.
|
|
it('"Configure skills" goes to Settings → Skills & Tools', async () => {
|
|
scanModule.scanBookings.mockResolvedValue({ rows: [], truncated: false });
|
|
AgentFactory.synthesizeDefaultAgent.mockReturnValue({ evaluateTelemetry: () => [] });
|
|
renderBanner();
|
|
|
|
fireEvent.click(await screen.findByRole('button', { name: /Configure skills/i }));
|
|
expect(screen.getByTestId('where')).toHaveTextContent('/doormile/settings?tab=skills');
|
|
});
|
|
|
|
it('says when it is running on default thresholds, and stops once the registry answers', async () => {
|
|
scanModule.scanBookings.mockResolvedValue({ rows: [], truncated: false });
|
|
AgentFactory.synthesizeDefaultAgent.mockReturnValue({ evaluateTelemetry: () => [] });
|
|
renderBanner();
|
|
|
|
expect(await screen.findByText(/Running on default thresholds/i)).toBeInTheDocument();
|
|
|
|
SkillRegistry.applyRegistryConfig([{ skillid: 'skill_sla_guardian', enabled: true, thresholds: {} }]);
|
|
await waitFor(() => expect(screen.queryByText(/Running on default thresholds/i)).toBeNull());
|
|
});
|
|
|
|
it('never calls anything autonomous', async () => {
|
|
scanModule.scanBookings.mockResolvedValue({ rows: [], truncated: false });
|
|
AgentFactory.synthesizeDefaultAgent.mockReturnValue({ evaluateTelemetry: () => [SLA_FINDING] });
|
|
renderBanner();
|
|
await screen.findByText(/1 parcel is past the promised delivery time/i);
|
|
expect(screen.queryByText(/autonomous/i)).toBeNull();
|
|
expect(screen.queryByText(/self-healing/i)).toBeNull();
|
|
});
|
|
});
|