Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion backend/src/routes/agents.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ async function getCachedAgents() {
return agentsCache;
}

function getAgentTier(score) {
if (score >= 1000) return 'elite';
if (score >= 900) return 'trusted';
if (score >= 600) return 'established';
if (score >= 300) return 'building';
return 'new';
}

function sortAgents(agents, sort) {
return [...agents].sort((a, b) => {
if (sort === 'score') return b.score - a.score;
Expand Down Expand Up @@ -97,9 +105,15 @@ router.get('/agents', requireAgentsContract, async (req, res) => {
const sort = ['score', 'payments', 'newest'].includes(req.query.sort)
? req.query.sort
: 'score';
const tier = ['all', 'new', 'building', 'established', 'trusted', 'elite'].includes(req.query.tier)
? req.query.tier
: 'all';

const allAgents = await getCachedAgents();
const sorted = sortAgents(allAgents, sort);
const filteredAgents = tier === 'all'
? allAgents
: allAgents.filter((agent) => getAgentTier(agent.score) === tier);
const sorted = sortAgents(filteredAgents, sort);
const total = sorted.length;
const agents = sorted.slice(page * pageSize, (page + 1) * pageSize);

Expand Down
231 changes: 231 additions & 0 deletions frontend/__tests__/AgentsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ const mockStats: AgentStats = {
totalVolumeStroops: '10000000',
};

function makeAgent(overrides: Partial<AgentEntry> = {}): AgentEntry {
return {
...mockAgent,
...overrides,
address: overrides.address ?? mockAgent.address,
name: overrides.name ?? mockAgent.name,
score: overrides.score ?? mockAgent.score,
total_payments: overrides.total_payments ?? mockAgent.total_payments,
successful_payments: overrides.successful_payments ?? mockAgent.successful_payments,
failed_payments: overrides.failed_payments ?? mockAgent.failed_payments,
};
}

describe('AgentsPage retry state', () => {
beforeEach(() => {
jest.clearAllMocks();
Expand All @@ -81,3 +94,221 @@ describe('AgentsPage retry state', () => {
expect(screen.queryByText('Network disconnected')).not.toBeInTheDocument();
});
});

describe('AgentsPage data rendering', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders the agent grid when agents exist even if stats are unavailable', async () => {
(fetchAgents as jest.Mock).mockResolvedValue({
agents: [makeAgent({ address: 'A', name: 'Visible Agent' })],
total: 1,
page: 0,
pageSize: PAGE_SIZE,
});
(fetchAgentStats as jest.Mock).mockRejectedValue(new Error('Stats unavailable'));

renderPage();

await waitFor(() => {
expect(screen.getByText('Visible Agent')).toBeInTheDocument();
});
expect(screen.queryByText('No agents registered yet.')).not.toBeInTheDocument();
});
});

describe('AgentsPage score tier filter', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('defaults to the All filter and shows all agents', async () => {
(fetchAgents as jest.Mock).mockResolvedValue({
agents: [
makeAgent({ address: 'A', name: 'New Agent', score: 100, total_payments: '1' }),
makeAgent({ address: 'B', name: 'Building Agent', score: 350, total_payments: '2' }),
makeAgent({ address: 'C', name: 'Established Agent', score: 650, total_payments: '3' }),
makeAgent({ address: 'D', name: 'Trusted Agent', score: 950, total_payments: '4' }),
makeAgent({ address: 'E', name: 'Elite Agent', score: 1000, total_payments: '5' }),
],
total: 5,
page: 0,
pageSize: PAGE_SIZE,
});
(fetchAgentStats as jest.Mock).mockResolvedValue(mockStats);

renderPage();

await waitFor(() => {
expect(screen.getByText('New Agent')).toBeInTheDocument();
expect(screen.getByText('Elite Agent')).toBeInTheDocument();
});

expect(screen.getByRole('button', { name: 'All' })).toHaveAttribute('aria-pressed', 'true');
});

it('filters agents by each score tier', async () => {
(fetchAgents as jest.Mock).mockImplementation((_page, _pageSize, _sort, tier) => {
if (tier === 'trusted') {
return Promise.resolve({
agents: [makeAgent({ address: 'D', name: 'Trusted Agent', score: 950, total_payments: '4' })],
total: 1,
page: 0,
pageSize: PAGE_SIZE,
});
}

return Promise.resolve({
agents: [
makeAgent({ address: 'A', name: 'New Agent', score: 100, total_payments: '1' }),
makeAgent({ address: 'B', name: 'Building Agent', score: 350, total_payments: '2' }),
makeAgent({ address: 'C', name: 'Established Agent', score: 650, total_payments: '3' }),
makeAgent({ address: 'D', name: 'Trusted Agent', score: 950, total_payments: '4' }),
makeAgent({ address: 'E', name: 'Elite Agent', score: 1000, total_payments: '5' }),
],
total: 5,
page: 0,
pageSize: PAGE_SIZE,
});
});
(fetchAgentStats as jest.Mock).mockResolvedValue(mockStats);

renderPage();

await waitFor(() => {
expect(screen.getByText('Trusted Agent')).toBeInTheDocument();
});

fireEvent.click(screen.getByRole('button', { name: 'Trusted' }));

await waitFor(() => {
expect(screen.getByText('Trusted Agent')).toBeInTheDocument();
});
expect(screen.queryByText('New Agent')).not.toBeInTheDocument();
expect(screen.queryByText('Elite Agent')).not.toBeInTheDocument();
});

it('uses the server total when a tier has matches beyond the first page', async () => {
(fetchAgents as jest.Mock).mockResolvedValue({
agents: [makeAgent({ address: 'A', name: 'Building One', score: 350, total_payments: '2' })],
total: 13,
page: 0,
pageSize: PAGE_SIZE,
});
(fetchAgentStats as jest.Mock).mockResolvedValue(mockStats);

renderPage();

await waitFor(() => {
expect(screen.getByText('Building One')).toBeInTheDocument();
});

fireEvent.click(screen.getByRole('button', { name: 'Building' }));

expect(fetchAgents).toHaveBeenLastCalledWith(0, PAGE_SIZE, 'score', 'building');
expect(screen.getByText('Showing 1–12 of 13')).toBeInTheDocument();
});

it('applies the selected tier filter alongside sorting', async () => {
(fetchAgents as jest.Mock).mockImplementation((_page, _pageSize, sort, tier) => {
if (tier === 'building') {
return Promise.resolve({
agents: [
makeAgent({ address: 'B', name: 'Building Two', score: 400, total_payments: '8' }),
makeAgent({ address: 'A', name: 'Building One', score: 350, total_payments: '2' }),
],
total: 2,
page: 0,
pageSize: PAGE_SIZE,
});
}

return Promise.resolve({
agents: [
makeAgent({ address: 'A', name: 'Building One', score: 350, total_payments: '2' }),
makeAgent({ address: 'C', name: 'Trusted Agent', score: 950, total_payments: '4' }),
],
total: 2,
page: 0,
pageSize: PAGE_SIZE,
});
});
(fetchAgentStats as jest.Mock).mockResolvedValue(mockStats);

renderPage();

await waitFor(() => {
expect(screen.getByText('Building One')).toBeInTheDocument();
});

fireEvent.change(screen.getByLabelText('Sort agents'), { target: { value: 'payments' } });
fireEvent.click(screen.getByRole('button', { name: 'Building' }));

await waitFor(() => {
expect(screen.getByText('Building Two')).toBeInTheDocument();
});

const visibleNames = screen.getAllByRole('link', { name: /Building/i }).map((element) => element.textContent);
expect(visibleNames).toContain('Building Two');
expect(visibleNames[0]).toBe('Building Two');
});

it('shows an empty state when a selected tier has no matches', async () => {
(fetchAgents as jest.Mock).mockImplementation((_page, _pageSize, _sort, tier) => {
if (tier === 'elite') {
return Promise.resolve({
agents: [],
total: 0,
page: 0,
pageSize: PAGE_SIZE,
});
}

return Promise.resolve({
agents: [makeAgent({ address: 'A', name: 'New Agent', score: 100, total_payments: '1' })],
total: 1,
page: 0,
pageSize: PAGE_SIZE,
});
});
(fetchAgentStats as jest.Mock).mockResolvedValue(mockStats);

renderPage();

await waitFor(() => {
expect(screen.getByText('New Agent')).toBeInTheDocument();
});

fireEvent.click(screen.getByRole('button', { name: 'Elite' }));

await waitFor(() => {
expect(screen.getByText('No agents match the selected tier.')).toBeInTheDocument();
});
});

it('resets the filter when the All chip is selected', async () => {
(fetchAgents as jest.Mock).mockResolvedValue({
agents: [
makeAgent({ address: 'A', name: 'New Agent', score: 100, total_payments: '1' }),
makeAgent({ address: 'B', name: 'Trusted Agent', score: 950, total_payments: '4' }),
],
total: 2,
page: 0,
pageSize: PAGE_SIZE,
});
(fetchAgentStats as jest.Mock).mockResolvedValue(mockStats);

renderPage();

await waitFor(() => {
expect(screen.getByText('Trusted Agent')).toBeInTheDocument();
});

fireEvent.click(screen.getByRole('button', { name: 'Trusted' }));
fireEvent.click(screen.getByRole('button', { name: 'All' }));

expect(screen.getByText('New Agent')).toBeInTheDocument();
expect(screen.getByText('Trusted Agent')).toBeInTheDocument();
});
});
Loading