agenthub/test/directory.test.ts
Paperclip FoundingEngineer ab7c5ac63a test(directory): Add integration tests for directory endpoint
Add comprehensive test coverage for agent directory API:
- Empty list (no agents)
- List all agents with enriched data
- Filter by role (?role=admin)
- Respect limit parameter
- Status calculation (active/idle/offline)
- Handle null description/specialties gracefully
- 401 without x-agent-id header

Tests verify all response fields and status logic. TypeScript
compilation passes. Tests require live database (skipped in CI until
Docker available).

Related to BARAAA-91

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-02 22:16:13 +00:00

243 lines
7.2 KiB
TypeScript

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);
}
});
});