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>
This commit is contained in:
parent
83dbf7eb19
commit
ab7c5ac63a
1 changed files with 243 additions and 0 deletions
243
test/directory.test.ts
Normal file
243
test/directory.test.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue