This commit is contained in:
307
packages/server/tests/routes/agents.test.ts
Normal file
307
packages/server/tests/routes/agents.test.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||
import { buildServer } from '../../src/index.js';
|
||||
import { users, agents, agentGroups, agentGroupMembers, agentJobs, teams } from '../../src/db/schema.js';
|
||||
import { sql, eq } from 'drizzle-orm';
|
||||
|
||||
describe('Agent API', () => {
|
||||
let server: Awaited<ReturnType<typeof buildServer>>;
|
||||
let user1Id: string;
|
||||
let user2Id: string;
|
||||
let teamId: string;
|
||||
let agent1Id: string;
|
||||
let agent2Id: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
server = await buildServer();
|
||||
|
||||
// Clean up leftover test data
|
||||
await server.db.execute(sql`DELETE FROM agent_group_members WHERE group_id IN (SELECT id FROM agent_groups WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'agtest_%'))`);
|
||||
await server.db.execute(sql`DELETE FROM agent_jobs WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'agtest_%')`);
|
||||
await server.db.execute(sql`DELETE FROM agent_groups WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'agtest_%')`);
|
||||
await server.db.execute(sql`DELETE FROM agents WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'agtest_%')`);
|
||||
await server.db.execute(sql`DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'agtest-%')`);
|
||||
await server.db.execute(sql`DELETE FROM team_capability_requests WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'agtest-%')`);
|
||||
await server.db.execute(sql`DELETE FROM team_capability_overrides WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'agtest-%')`);
|
||||
await server.db.execute(sql`DELETE FROM team_capability_policies WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'agtest-%')`);
|
||||
await server.db.execute(sql`DELETE FROM teams WHERE slug LIKE 'agtest-%'`);
|
||||
await server.db.execute(sql`DELETE FROM users WHERE clerk_id LIKE 'agtest_%'`);
|
||||
|
||||
// Create test users
|
||||
const [u1] = await server.db.insert(users).values({
|
||||
clerkId: 'agtest_user1',
|
||||
displayName: 'Agent User 1',
|
||||
email: 'agentuser1@test.com',
|
||||
}).returning();
|
||||
user1Id = u1.id;
|
||||
|
||||
const [u2] = await server.db.insert(users).values({
|
||||
clerkId: 'agtest_user2',
|
||||
displayName: 'Agent User 2',
|
||||
email: 'agentuser2@test.com',
|
||||
}).returning();
|
||||
user2Id = u2.id;
|
||||
|
||||
// Create a team for job tests
|
||||
const [team] = await server.db.insert(teams).values({
|
||||
name: 'Agent Test Team',
|
||||
slug: 'agtest-team',
|
||||
ownerId: user1Id,
|
||||
}).returning();
|
||||
teamId = team.id;
|
||||
|
||||
// Override auth handler for tests
|
||||
server._authHandler.fn = async function (request, reply) {
|
||||
const testUserId = request.headers['x-test-user-id'] as string;
|
||||
if (!testUserId) {
|
||||
return reply.code(401).send({ error: 'Missing x-test-user-id header' });
|
||||
}
|
||||
request.userId = testUserId;
|
||||
request.clerkId = 'test';
|
||||
};
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await server.db.execute(sql`DELETE FROM agent_group_members WHERE group_id IN (SELECT id FROM agent_groups WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'agtest_%'))`);
|
||||
await server.db.execute(sql`DELETE FROM agent_jobs WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'agtest_%')`);
|
||||
await server.db.execute(sql`DELETE FROM agent_groups WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'agtest_%')`);
|
||||
await server.db.execute(sql`DELETE FROM agents WHERE user_id IN (SELECT id FROM users WHERE clerk_id LIKE 'agtest_%')`);
|
||||
await server.db.execute(sql`DELETE FROM team_members WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'agtest-%')`);
|
||||
await server.db.execute(sql`DELETE FROM team_capability_requests WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'agtest-%')`);
|
||||
await server.db.execute(sql`DELETE FROM team_capability_overrides WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'agtest-%')`);
|
||||
await server.db.execute(sql`DELETE FROM team_capability_policies WHERE team_id IN (SELECT id FROM teams WHERE slug LIKE 'agtest-%')`);
|
||||
await server.db.execute(sql`DELETE FROM teams WHERE slug LIKE 'agtest-%'`);
|
||||
await server.db.execute(sql`DELETE FROM users WHERE clerk_id LIKE 'agtest_%'`);
|
||||
await server.close();
|
||||
});
|
||||
|
||||
it('creates an agent with custom model, tools, and system prompt', async () => {
|
||||
const response = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/agents',
|
||||
headers: { 'x-test-user-id': user1Id },
|
||||
payload: {
|
||||
name: 'Research Agent',
|
||||
role: 'researcher',
|
||||
systemPrompt: 'You are a research agent.',
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
tools: ['web_search', 'read_file'],
|
||||
config: { maxTokens: 4096 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(201);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.name).toBe('Research Agent');
|
||||
expect(body.role).toBe('researcher');
|
||||
expect(body.systemPrompt).toBe('You are a research agent.');
|
||||
expect(body.model).toBe('claude-sonnet-4-20250514');
|
||||
expect(body.tools).toEqual(['web_search', 'read_file']);
|
||||
expect(body.config).toEqual({ maxTokens: 4096 });
|
||||
expect(body.userId).toBe(user1Id);
|
||||
agent1Id = body.id;
|
||||
});
|
||||
|
||||
it('creates a second agent for user1', async () => {
|
||||
const response = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/agents',
|
||||
headers: { 'x-test-user-id': user1Id },
|
||||
payload: {
|
||||
name: 'Coding Agent',
|
||||
model: 'claude-haiku-4-5',
|
||||
tools: ['execute_code'],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(201);
|
||||
agent2Id = JSON.parse(response.body).id;
|
||||
});
|
||||
|
||||
it('lists agents returns only the user\'s agents', async () => {
|
||||
// Create an agent for user2
|
||||
await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/agents',
|
||||
headers: { 'x-test-user-id': user2Id },
|
||||
payload: { name: 'User2 Agent' },
|
||||
});
|
||||
|
||||
const response = await server.inject({
|
||||
method: 'GET',
|
||||
url: '/api/agents',
|
||||
headers: { 'x-test-user-id': user1Id },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
expect(body.length).toBe(2);
|
||||
expect(body.every((a: { userId: string }) => a.userId === user1Id)).toBe(true);
|
||||
});
|
||||
|
||||
it('updates agent model and config', async () => {
|
||||
const response = await server.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/agents/${agent1Id}`,
|
||||
headers: { 'x-test-user-id': user1Id },
|
||||
payload: {
|
||||
model: 'claude-haiku-4-5',
|
||||
config: { maxTokens: 2048, temperature: 0.7 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.model).toBe('claude-haiku-4-5');
|
||||
expect(body.config).toEqual({ maxTokens: 2048, temperature: 0.7 });
|
||||
});
|
||||
|
||||
it('user cannot update another user\'s agent', async () => {
|
||||
const response = await server.inject({
|
||||
method: 'PATCH',
|
||||
url: `/api/agents/${agent1Id}`,
|
||||
headers: { 'x-test-user-id': user2Id },
|
||||
payload: { model: 'hacked-model' },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('user cannot delete another user\'s agent', async () => {
|
||||
const response = await server.inject({
|
||||
method: 'DELETE',
|
||||
url: `/api/agents/${agent1Id}`,
|
||||
headers: { 'x-test-user-id': user2Id },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
it('creates an agent group with parallel strategy and 2 members', async () => {
|
||||
const response = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/agent-groups',
|
||||
headers: { 'x-test-user-id': user1Id },
|
||||
payload: {
|
||||
name: 'Research Squad',
|
||||
description: 'Parallel research team',
|
||||
strategy: 'parallel',
|
||||
members: [
|
||||
{ agentId: agent1Id, roleInGroup: 'worker', executionOrder: 0 },
|
||||
{ agentId: agent2Id, roleInGroup: 'worker', executionOrder: 0 },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(201);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.name).toBe('Research Squad');
|
||||
expect(body.strategy).toBe('parallel');
|
||||
expect(body.members).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('creates a group with coordinator strategy and a lead agent', async () => {
|
||||
const response = await server.inject({
|
||||
method: 'POST',
|
||||
url: '/api/agent-groups',
|
||||
headers: { 'x-test-user-id': user1Id },
|
||||
payload: {
|
||||
name: 'Coordinated Team',
|
||||
strategy: 'coordinator',
|
||||
members: [
|
||||
{ agentId: agent1Id, roleInGroup: 'lead', executionOrder: 0 },
|
||||
{ agentId: agent2Id, roleInGroup: 'worker', executionOrder: 1 },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(201);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.strategy).toBe('coordinator');
|
||||
const lead = body.members.find((m: { roleInGroup: string; agentId: string }) => m.roleInGroup === 'lead');
|
||||
expect(lead).toBeDefined();
|
||||
expect(lead.agentId).toBe(agent1Id);
|
||||
});
|
||||
|
||||
it('gets group with members', async () => {
|
||||
// List groups to get an ID
|
||||
const listResponse = await server.inject({
|
||||
method: 'GET',
|
||||
url: '/api/agent-groups',
|
||||
headers: { 'x-test-user-id': user1Id },
|
||||
});
|
||||
|
||||
expect(listResponse.statusCode).toBe(200);
|
||||
const groups = JSON.parse(listResponse.body);
|
||||
expect(groups.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
const groupId = groups[0].id;
|
||||
const response = await server.inject({
|
||||
method: 'GET',
|
||||
url: `/api/agent-groups/${groupId}`,
|
||||
headers: { 'x-test-user-id': user1Id },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.id).toBe(groupId);
|
||||
expect(body.members).toBeDefined();
|
||||
expect(Array.isArray(body.members)).toBe(true);
|
||||
});
|
||||
|
||||
it('executes group run — queues a job and returns 202 with jobId', async () => {
|
||||
const listResponse = await server.inject({
|
||||
method: 'GET',
|
||||
url: '/api/agent-groups',
|
||||
headers: { 'x-test-user-id': user1Id },
|
||||
});
|
||||
const groups = JSON.parse(listResponse.body);
|
||||
const groupId = groups[0].id;
|
||||
|
||||
const response = await server.inject({
|
||||
method: 'POST',
|
||||
url: `/api/agent-groups/${groupId}/run`,
|
||||
headers: { 'x-test-user-id': user1Id },
|
||||
payload: {
|
||||
task: 'Research the latest AI papers',
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(202);
|
||||
const body = JSON.parse(response.body);
|
||||
expect(body.jobId).toBeDefined();
|
||||
expect(typeof body.jobId).toBe('string');
|
||||
|
||||
// Verify the job was created in the database
|
||||
const [job] = await server.db
|
||||
.select()
|
||||
.from(agentJobs)
|
||||
.where(eq(agentJobs.id, body.jobId))
|
||||
.limit(1);
|
||||
expect(job).toBeDefined();
|
||||
expect(job.status).toBe('queued');
|
||||
expect(job.jobType).toBe('group');
|
||||
expect(job.userId).toBe(user1Id);
|
||||
});
|
||||
|
||||
it('deletes an agent', async () => {
|
||||
const response = await server.inject({
|
||||
method: 'DELETE',
|
||||
url: `/api/agents/${agent2Id}`,
|
||||
headers: { 'x-test-user-id': user1Id },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(204);
|
||||
|
||||
// Verify it's gone
|
||||
const listResponse = await server.inject({
|
||||
method: 'GET',
|
||||
url: '/api/agents',
|
||||
headers: { 'x-test-user-id': user1Id },
|
||||
});
|
||||
const remaining = JSON.parse(listResponse.body);
|
||||
const ids = remaining.map((a: { id: string }) => a.id);
|
||||
expect(ids).not.toContain(agent2Id);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user