diff --git a/backend/__tests__/unit/routes/stats.public.test.js b/backend/__tests__/unit/routes/stats.public.test.js index 088dd123..97b3bbca 100644 --- a/backend/__tests__/unit/routes/stats.public.test.js +++ b/backend/__tests__/unit/routes/stats.public.test.js @@ -3,12 +3,13 @@ const express = require('express'); const mockPgQuery = jest.fn(); const mockMessageCountDocuments = jest.fn().mockResolvedValue(88); +const mockUserCountDocuments = jest.fn().mockResolvedValue(262); jest.mock('../../../models/Pod', () => ({ countDocuments: jest.fn().mockResolvedValue(12), })); jest.mock('../../../models/User', () => ({ - countDocuments: jest.fn().mockResolvedValue(42), + countDocuments: mockUserCountDocuments, })); jest.mock('../../../models/Message', () => ({ countDocuments: mockMessageCountDocuments, @@ -42,13 +43,19 @@ describe('GET /api/stats/public', () => { activePods: 12, activeAgents: 3, messageCount24h: 1234, - registeredUsers: 42, + agentCount: 262, }); expect(mockPgQuery).toHaveBeenCalledWith( 'SELECT COUNT(*)::int AS count FROM messages WHERE created_at >= $1', [expect.any(Date)], ); expect(mockMessageCountDocuments).not.toHaveBeenCalled(); + expect(mockUserCountDocuments).toHaveBeenCalledWith({ + 'botMetadata.agentName': { $exists: true }, + }); + expect(mockUserCountDocuments).toHaveBeenCalledTimes(1); + expect(res.body).not.toHaveProperty('humanCount'); + expect(res.body).not.toHaveProperty('registeredUsers'); }); it('falls back to MongoDB when the PostgreSQL count fails', async () => { @@ -61,7 +68,7 @@ describe('GET /api/stats/public', () => { activePods: 12, activeAgents: 3, messageCount24h: 88, - registeredUsers: 42, + agentCount: 262, }); expect(mockMessageCountDocuments).toHaveBeenCalledWith({ createdAt: { $gte: expect.any(Date) }, diff --git a/backend/routes/stats.ts b/backend/routes/stats.ts index 329cec05..d979b1f4 100644 --- a/backend/routes/stats.ts +++ b/backend/routes/stats.ts @@ -26,17 +26,27 @@ router.get('/public', async (_req: unknown, res: { json: (d: unknown) => void; s const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000); - const [activePods, activeAgents, messageCount24h, registeredUsers] = await Promise.all([ + const [ + activePods, + activeAgents, + messageCount24h, + agentCount, + ] = await Promise.all([ Pod.countDocuments({ updatedAt: { $gte: sevenDaysAgo } }), AgentInstallation.distinct('agentName').then((names: string[]) => names.length), pgMessageCount24h(oneDayAgo).catch((err: { message?: string }) => { console.warn('stats: PG message count failed, falling back to Mongo:', err?.message); return Message.countDocuments({ createdAt: { $gte: oneDayAgo } }); }), - User.countDocuments(), + User.countDocuments({ 'botMetadata.agentName': { $exists: true } }), ]); - res.json({ activePods, activeAgents, messageCount24h, registeredUsers }); + res.json({ + activePods, + activeAgents, + messageCount24h, + agentCount, + }); } catch { res.status(500).json({ error: 'Failed to fetch stats' }); } diff --git a/frontend/src/v2/__tests__/V2LandingPage.stats.test.tsx b/frontend/src/v2/__tests__/V2LandingPage.stats.test.tsx new file mode 100644 index 00000000..72d9a445 --- /dev/null +++ b/frontend/src/v2/__tests__/V2LandingPage.stats.test.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { render, screen, within } from '@testing-library/react'; +import { MemoryRouter } from 'react-router-dom'; +import axios from 'axios'; +import { useAuth } from '../../context/AuthContext'; +import V2LandingPage from '../landing/V2LandingPage'; + +jest.mock('axios'); +jest.mock('../../context/AuthContext', () => ({ + useAuth: jest.fn(), +})); + +const mockAxiosGet = axios.get as jest.Mock; +const mockUseAuth = useAuth as jest.Mock; + +const renderLanding = () => render( + + + , +); + +describe('V2LandingPage proof stats', () => { + let playSpy: jest.SpyInstance; + + beforeAll(() => { + playSpy = jest.spyOn(window.HTMLMediaElement.prototype, 'play') + .mockResolvedValue(undefined); + }); + + afterAll(() => { + playSpy.mockRestore(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + mockUseAuth.mockReturnValue({ isAuthenticated: false }); + }); + + it('renders only agent and activity stats', async () => { + mockAxiosGet.mockResolvedValue({ + data: { + activePods: 12, + activeAgents: 3, + messageCount24h: 1234, + agentCount: 262, + }, + }); + + renderLanding(); + + const agentsLabel = await screen.findByText('agents'); + const statsRow = agentsLabel.closest('.v2-landing__proof-stats'); + expect(statsRow).not.toBeNull(); + + const stats = within(statsRow as HTMLElement); + expect(agentsLabel.previousElementSibling).toHaveTextContent('262'); + expect(stats.getByText('messages / 24h').previousElementSibling).toHaveTextContent('1,234'); + expect(stats.getByText('active pods').previousElementSibling).toHaveTextContent('12'); + expect(stats.queryByText('builders')).not.toBeInTheDocument(); + expect(stats.queryByText('people')).not.toBeInTheDocument(); + expect(stats.queryByText('ADRs')).not.toBeInTheDocument(); + expect(statsRow?.children).toHaveLength(3); + }); +}); diff --git a/frontend/src/v2/landing/V2LandingPage.tsx b/frontend/src/v2/landing/V2LandingPage.tsx index a8d09014..007a5c6f 100644 --- a/frontend/src/v2/landing/V2LandingPage.tsx +++ b/frontend/src/v2/landing/V2LandingPage.tsx @@ -41,7 +41,8 @@ const Mark: React.FC<{ size?: number }> = ({ size = 26 }) => ( interface Stats { activePods?: number; activeAgents?: number; - registeredUsers?: number; + messageCount24h?: number; + agentCount?: number; } const fmt = (n?: number): string => (typeof n === 'number' ? n.toLocaleString() : '—'); @@ -214,7 +215,11 @@ const V2LandingPage: React.FC = () => { if (p && typeof p.catch === 'function') p.catch(() => { /* poster stays */ }); }, [motion]); - const hasStats = Boolean(stats && (stats.activePods || stats.activeAgents || stats.registeredUsers)); + const hasStats = Boolean(stats && ( + stats.activePods + || stats.messageCount24h + || stats.agentCount + )); return (
@@ -544,10 +549,9 @@ const V2LandingPage: React.FC = () => {

{hasStats && (
+
{fmt(stats?.agentCount)}agents
+
{fmt(stats?.messageCount24h)}messages / 24h
{fmt(stats?.activePods)}active pods
-
{fmt(stats?.activeAgents)}agents connected
-
{fmt(stats?.registeredUsers)}people
-
{ADR_COUNT}ADRs
)}