Files
mcpctl/src/db/tests/agent-schema.test.ts

205 lines
6.4 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import type { PrismaClient } from '@prisma/client';
import { setupTestDb, cleanupTestDb, clearAllTables } from './helpers.js';
describe('agent / chat-thread / chat-message schema', () => {
let prisma: PrismaClient;
beforeAll(async () => {
prisma = await setupTestDb();
}, 30_000);
afterAll(async () => {
await cleanupTestDb();
});
beforeEach(async () => {
await clearAllTables(prisma);
});
async function makeUser(suffix = '') {
return prisma.user.create({
data: {
email: `agent-test-${Date.now()}${suffix}@example.com`,
name: 'Agent Tester',
passwordHash: 'x',
},
});
}
async function makeLlm(name: string) {
return prisma.llm.create({
data: { name, type: 'openai', model: 'qwen3-thinking' },
});
}
async function makeProject(ownerId: string, name: string) {
return prisma.project.create({ data: { name, ownerId } });
}
async function makeAgent(opts: {
name: string;
llmId: string;
ownerId: string;
projectId?: string;
}) {
return prisma.agent.create({
data: {
name: opts.name,
llmId: opts.llmId,
ownerId: opts.ownerId,
projectId: opts.projectId ?? null,
},
});
}
it('creates an agent with required fields and JSON defaults', async () => {
const user = await makeUser();
const llm = await makeLlm('llm-default-fields');
const agent = await makeAgent({ name: 'a1', llmId: llm.id, ownerId: user.id });
expect(agent.id).toBeDefined();
expect(agent.description).toBe('');
expect(agent.systemPrompt).toBe('');
expect(agent.defaultParams).toEqual({});
expect(agent.extras).toEqual({});
expect(agent.version).toBe(1);
});
it('enforces unique agent name', async () => {
const user = await makeUser();
const llm = await makeLlm('llm-uniq');
await makeAgent({ name: 'dup', llmId: llm.id, ownerId: user.id });
await expect(
makeAgent({ name: 'dup', llmId: llm.id, ownerId: user.id }),
).rejects.toThrow();
});
it('blocks deleting an Llm referenced by an agent (Restrict)', async () => {
const user = await makeUser();
const llm = await makeLlm('llm-in-use');
await makeAgent({ name: 'pinned', llmId: llm.id, ownerId: user.id });
await expect(prisma.llm.delete({ where: { id: llm.id } })).rejects.toThrow();
});
it('sets agent.projectId NULL when its Project is deleted (SetNull)', async () => {
const user = await makeUser();
const llm = await makeLlm('llm-setnull');
const project = await makeProject(user.id, 'proj-detach');
const agent = await makeAgent({
name: 'detachable',
llmId: llm.id,
ownerId: user.id,
projectId: project.id,
});
expect(agent.projectId).toBe(project.id);
await prisma.project.delete({ where: { id: project.id } });
const reloaded = await prisma.agent.findUnique({ where: { id: agent.id } });
expect(reloaded?.projectId).toBeNull();
});
it('cascades thread + message delete when an Agent is deleted', async () => {
const user = await makeUser();
const llm = await makeLlm('llm-cascade');
const agent = await makeAgent({ name: 'doomed', llmId: llm.id, ownerId: user.id });
const thread = await prisma.chatThread.create({
data: { agentId: agent.id, ownerId: user.id, title: 't' },
});
await prisma.chatMessage.create({
data: {
threadId: thread.id,
turnIndex: 0,
role: 'user',
content: 'hello',
},
});
await prisma.agent.delete({ where: { id: agent.id } });
expect(await prisma.chatThread.findUnique({ where: { id: thread.id } })).toBeNull();
expect(await prisma.chatMessage.count({ where: { threadId: thread.id } })).toBe(0);
});
it('blocks duplicate (threadId, turnIndex)', async () => {
const user = await makeUser();
const llm = await makeLlm('llm-turn-uniq');
const agent = await makeAgent({ name: 'orderly', llmId: llm.id, ownerId: user.id });
const thread = await prisma.chatThread.create({
data: { agentId: agent.id, ownerId: user.id },
});
await prisma.chatMessage.create({
data: { threadId: thread.id, turnIndex: 0, role: 'user', content: 'a' },
});
await expect(
prisma.chatMessage.create({
data: { threadId: thread.id, turnIndex: 0, role: 'assistant', content: 'b' },
}),
).rejects.toThrow();
});
it('persists tool-call shape on assistant + tool turns', async () => {
const user = await makeUser();
const llm = await makeLlm('llm-tools');
const agent = await makeAgent({ name: 'toolish', llmId: llm.id, ownerId: user.id });
const thread = await prisma.chatThread.create({
data: { agentId: agent.id, ownerId: user.id },
});
await prisma.chatMessage.create({
data: { threadId: thread.id, turnIndex: 0, role: 'user', content: 'do x' },
});
await prisma.chatMessage.create({
data: {
threadId: thread.id,
turnIndex: 1,
role: 'assistant',
content: '',
toolCalls: [
{ id: 'call_1', name: 'do_thing', arguments: { x: 1 } },
],
status: 'pending',
},
});
await prisma.chatMessage.create({
data: {
threadId: thread.id,
turnIndex: 2,
role: 'tool',
content: 'ok',
toolCallId: 'call_1',
},
});
const messages = await prisma.chatMessage.findMany({
where: { threadId: thread.id },
orderBy: { turnIndex: 'asc' },
});
expect(messages).toHaveLength(3);
expect(messages[1]?.toolCalls).toEqual([
{ id: 'call_1', name: 'do_thing', arguments: { x: 1 } },
]);
expect(messages[2]?.toolCallId).toBe('call_1');
});
it('orders threads by lastTurnAt DESC for an agent', async () => {
const user = await makeUser();
const llm = await makeLlm('llm-order');
const agent = await makeAgent({ name: 'history', llmId: llm.id, ownerId: user.id });
const t1 = await prisma.chatThread.create({
data: { agentId: agent.id, ownerId: user.id, lastTurnAt: new Date(2000, 0, 1) },
});
const t2 = await prisma.chatThread.create({
data: { agentId: agent.id, ownerId: user.id, lastTurnAt: new Date(2030, 0, 1) },
});
const ordered = await prisma.chatThread.findMany({
where: { agentId: agent.id },
orderBy: { lastTurnAt: 'desc' },
});
expect(ordered.map((t) => t.id)).toEqual([t2.id, t1.id]);
});
});