import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { buildApp } from '../src/app.js'; import type { FastifyInstance } from 'fastify'; import { loadConfig } from '../src/config.js'; import { pool } from '../src/db/pool.js'; import { sql } from 'drizzle-orm'; import { drizzle } from 'drizzle-orm/node-postgres'; import { agents, auditEvents } from '../src/db/schema.js'; import { recordAuditEvent } from '../src/lib/audit.js'; describe('Directory API', () => { let app: FastifyInstance; let testAgentId: string; let testAgent2Id: string; let testAgent3Id: string; beforeAll(async () => { const config = loadConfig(); app = await buildApp({ config }); const db = drizzle(pool); // Clean up test data await db.execute(sql`TRUNCATE agents CASCADE`); // Create test agents with different roles const [agent1] = await db .insert(agents) .values({ name: 'test-engineer', displayName: 'Test Engineer', role: 'agent', urlKey: 'test-engineer', description: 'A test engineer agent', specialties: ['typescript', 'testing'], }) .returning(); const [agent2] = await db .insert(agents) .values({ name: 'test-admin', displayName: 'Test Admin', role: 'admin', urlKey: 'test-admin', description: 'An admin agent', specialties: ['management', 'operations'], }) .returning(); const [agent3] = await db .insert(agents) .values({ name: 'test-agent-idle', displayName: 'Idle Agent', role: 'agent', urlKey: 'idle-agent', description: null, specialties: null, }) .returning(); testAgentId = agent1!.id; testAgent2Id = agent2!.id; testAgent3Id = agent3!.id; // Create recent activity for agent1 (should be "active") await recordAuditEvent(pool, 'agent-created', testAgentId, { test: true }); // Create old activity for agent3 (should be "offline") await db .insert(auditEvents) .values({ type: 'agent-created', agentId: testAgent3Id, payloadHash: Buffer.from('test'), ts: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago }); }); afterAll(async () => { await app.close(); }); it('should return empty list when no agents exist', async () => { // Clean up const db = drizzle(pool); await db.execute(sql`TRUNCATE agents CASCADE`); const response = await app.inject({ method: 'GET', url: '/api/companies/test-company/agents/directory', headers: { 'x-agent-id': 'dummy-agent-id', }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.agents).toEqual([]); expect(body.total).toBe(0); // Recreate test data for other tests await db.insert(agents).values({ name: 'test-engineer', displayName: 'Test Engineer', role: 'agent', urlKey: 'test-engineer', description: 'A test engineer agent', specialties: ['typescript', 'testing'], }); }); it('should return all agents with enriched data', async () => { const response = await app.inject({ method: 'GET', url: '/api/companies/test-company/agents/directory', headers: { 'x-agent-id': testAgentId, }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.agents).toBeDefined(); expect(body.agents.length).toBeGreaterThan(0); expect(body.total).toBeGreaterThan(0); // Check first agent structure const firstAgent = body.agents[0]; expect(firstAgent).toHaveProperty('id'); expect(firstAgent).toHaveProperty('name'); expect(firstAgent).toHaveProperty('urlKey'); expect(firstAgent).toHaveProperty('role'); expect(firstAgent).toHaveProperty('description'); expect(firstAgent).toHaveProperty('specialties'); expect(firstAgent).toHaveProperty('lastActivityAt'); expect(firstAgent).toHaveProperty('status'); expect(firstAgent).toHaveProperty('chainOfCommand'); expect(firstAgent).toHaveProperty('socialChannels'); expect(firstAgent).toHaveProperty('profileUrl'); // Validate types expect(['active', 'idle', 'offline']).toContain(firstAgent.status); expect(Array.isArray(firstAgent.specialties)).toBe(true); expect(Array.isArray(firstAgent.socialChannels)).toBe(true); expect(firstAgent.profileUrl).toMatch(/^\/BARAAA\/agents\//); }); it('should filter agents by role', async () => { const response = await app.inject({ method: 'GET', url: '/api/companies/test-company/agents/directory?role=admin', headers: { 'x-agent-id': testAgentId, }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.agents.length).toBeGreaterThan(0); body.agents.forEach((agent: any) => { expect(agent.role).toBe('admin'); }); }); it('should respect limit parameter', async () => { const response = await app.inject({ method: 'GET', url: '/api/companies/test-company/agents/directory?limit=1', headers: { 'x-agent-id': testAgentId, }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); expect(body.agents.length).toBeLessThanOrEqual(1); if (body.agents.length === 1) { expect(body.hasMore).toBe(true); } }); it('should calculate status correctly based on lastActivityAt', async () => { const response = await app.inject({ method: 'GET', url: '/api/companies/test-company/agents/directory', headers: { 'x-agent-id': testAgentId, }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); // Find agent1 (recent activity) and agent3 (old activity) const activeAgent = body.agents.find((a: any) => a.id === testAgentId); const offlineAgent = body.agents.find((a: any) => a.id === testAgent3Id); // Agent with recent activity should be active or idle if (activeAgent) { expect(['active', 'idle']).toContain(activeAgent.status); } // Agent with 2-hour-old activity should be offline if (offlineAgent) { expect(offlineAgent.status).toBe('offline'); } }); it('should return 401 without x-agent-id header', async () => { const response = await app.inject({ method: 'GET', url: '/api/companies/test-company/agents/directory', }); expect(response.statusCode).toBe(401); const body = JSON.parse(response.body); expect(body.error).toBe('Missing x-agent-id header'); }); it('should handle null description and specialties gracefully', async () => { const response = await app.inject({ method: 'GET', url: '/api/companies/test-company/agents/directory', headers: { 'x-agent-id': testAgentId, }, }); expect(response.statusCode).toBe(200); const body = JSON.parse(response.body); const agentWithNulls = body.agents.find((a: any) => a.id === testAgent3Id); if (agentWithNulls) { expect(agentWithNulls.description).toBeNull(); expect(Array.isArray(agentWithNulls.specialties)).toBe(true); } }); });