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>
243 lines
7.2 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|