From e724d9c2255caee8507c5bf27b8d6fe7b092a881 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Mon, 16 Mar 2026 16:39:47 +0000 Subject: [PATCH] test(integration): add agentDefinitionsRepository integration tests --- .../db/agentDefinitionsRepository.test.ts | 398 ++++++++++++++++++ tests/integration/helpers/db.ts | 1 + tests/integration/helpers/seed.ts | 47 +++ 3 files changed, 446 insertions(+) create mode 100644 tests/integration/db/agentDefinitionsRepository.test.ts diff --git a/tests/integration/db/agentDefinitionsRepository.test.ts b/tests/integration/db/agentDefinitionsRepository.test.ts new file mode 100644 index 00000000..a55ea72c --- /dev/null +++ b/tests/integration/db/agentDefinitionsRepository.test.ts @@ -0,0 +1,398 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { AgentDefinitionSchema } from '../../../src/agents/definitions/schema.js'; +import type { AgentDefinition } from '../../../src/agents/definitions/schema.js'; +import { + deleteAgentDefinition, + getAgentDefinition, + listAgentDefinitions, + upsertAgentDefinition, +} from '../../../src/db/repositories/agentDefinitionsRepository.js'; +import { truncateAll } from '../helpers/db.js'; +import { MINIMAL_AGENT_DEFINITION, seedAgentDefinition } from '../helpers/seed.js'; + +describe('agentDefinitionsRepository (integration)', () => { + beforeEach(async () => { + await truncateAll(); + }); + + // ========================================================================= + // upsertAgentDefinition — create + // ========================================================================= + + describe('upsertAgentDefinition (create)', () => { + it('creates a new definition and can be retrieved', async () => { + await upsertAgentDefinition('custom-agent', MINIMAL_AGENT_DEFINITION); + + const result = await getAgentDefinition('custom-agent'); + expect(result).not.toBeNull(); + expect(result?.identity.label).toBe('Test Agent'); + expect(result?.hints).toBeUndefined(); + }); + + it('validates via AgentDefinitionSchema before inserting', async () => { + // Upserting should succeed for a valid definition + await expect( + upsertAgentDefinition('validated-agent', MINIMAL_AGENT_DEFINITION), + ).resolves.not.toThrow(); + }); + + it('rejects invalid definitions — missing required taskPrompt', async () => { + const invalidDefinition = { + ...MINIMAL_AGENT_DEFINITION, + prompts: { taskPrompt: '' }, // empty taskPrompt violates z.string().min(1) + } as AgentDefinition; + + await expect(upsertAgentDefinition('invalid-agent', invalidDefinition)).rejects.toThrow(); + }); + + it('rejects invalid definitions — missing required capabilities', async () => { + const invalidDefinition = { + identity: { + emoji: '🤖', + label: 'Bad Agent', + roleHint: 'missing', + initialMessage: 'hello', + }, + // missing capabilities, triggers, strategies, hint, prompts + } as unknown as AgentDefinition; + + await expect(upsertAgentDefinition('bad-agent', invalidDefinition)).rejects.toThrow(); + }); + + it('stores isBuiltin as false by default', async () => { + await upsertAgentDefinition('default-builtin-agent', MINIMAL_AGENT_DEFINITION); + + const list = await listAgentDefinitions(); + const entry = list.find((d) => d.agentType === 'default-builtin-agent'); + expect(entry).toBeDefined(); + expect(entry?.isBuiltin).toBe(false); + }); + + it('stores isBuiltin as true when explicitly set', async () => { + await upsertAgentDefinition('builtin-agent', MINIMAL_AGENT_DEFINITION, true); + + const list = await listAgentDefinitions(); + const entry = list.find((d) => d.agentType === 'builtin-agent'); + expect(entry).toBeDefined(); + expect(entry?.isBuiltin).toBe(true); + }); + }); + + // ========================================================================= + // upsertAgentDefinition — update (conflict semantics) + // ========================================================================= + + describe('upsertAgentDefinition (update semantics)', () => { + it('upserting same agentType updates definition, does not duplicate', async () => { + await upsertAgentDefinition('shared-agent', MINIMAL_AGENT_DEFINITION); + + const updatedDefinition: AgentDefinition = { + ...MINIMAL_AGENT_DEFINITION, + identity: { + ...MINIMAL_AGENT_DEFINITION.identity, + label: 'Updated Label', + }, + }; + await upsertAgentDefinition('shared-agent', updatedDefinition); + + const list = await listAgentDefinitions(); + const entries = list.filter((d) => d.agentType === 'shared-agent'); + expect(entries).toHaveLength(1); + expect(entries[0].definition.identity.label).toBe('Updated Label'); + }); + + it('upserting same agentType updates isBuiltin flag', async () => { + await upsertAgentDefinition('flag-agent', MINIMAL_AGENT_DEFINITION, false); + + const listBefore = await listAgentDefinitions(); + const before = listBefore.find((d) => d.agentType === 'flag-agent'); + expect(before?.isBuiltin).toBe(false); + + await upsertAgentDefinition('flag-agent', MINIMAL_AGENT_DEFINITION, true); + + const listAfter = await listAgentDefinitions(); + const after = listAfter.find((d) => d.agentType === 'flag-agent'); + expect(after?.isBuiltin).toBe(true); + }); + + it('upserting multiple different agentTypes creates separate entries', async () => { + await upsertAgentDefinition('agent-alpha', MINIMAL_AGENT_DEFINITION); + await upsertAgentDefinition('agent-beta', { + ...MINIMAL_AGENT_DEFINITION, + identity: { ...MINIMAL_AGENT_DEFINITION.identity, label: 'Beta Agent' }, + }); + + const list = await listAgentDefinitions(); + const agentTypes = list.map((d) => d.agentType).sort(); + expect(agentTypes).toContain('agent-alpha'); + expect(agentTypes).toContain('agent-beta'); + }); + }); + + // ========================================================================= + // getAgentDefinition + // ========================================================================= + + describe('getAgentDefinition', () => { + it('returns null when no definition exists for the agentType', async () => { + const result = await getAgentDefinition('nonexistent-agent'); + expect(result).toBeNull(); + }); + + it('retrieves the inserted definition by agentType', async () => { + await upsertAgentDefinition('get-test-agent', MINIMAL_AGENT_DEFINITION); + + const result = await getAgentDefinition('get-test-agent'); + expect(result).not.toBeNull(); + expect(result?.identity.label).toBe('Test Agent'); + expect(result?.hint).toBe('This is a test hint for iteration guidance.'); + }); + + it('returns a Zod-parsed AgentDefinition (proper type)', async () => { + await upsertAgentDefinition('parsed-agent', MINIMAL_AGENT_DEFINITION); + + const result = await getAgentDefinition('parsed-agent'); + // Validate that Zod defaults are applied (e.g., triggers defaults to []) + expect(Array.isArray(result?.triggers)).toBe(true); + // Should parse without error — i.e., it's a valid AgentDefinition + expect(() => AgentDefinitionSchema.parse(result)).not.toThrow(); + }); + + it('retrieves the correct definition when multiple agentTypes exist', async () => { + await upsertAgentDefinition('agent-x', { + ...MINIMAL_AGENT_DEFINITION, + identity: { ...MINIMAL_AGENT_DEFINITION.identity, label: 'Agent X' }, + }); + await upsertAgentDefinition('agent-y', { + ...MINIMAL_AGENT_DEFINITION, + identity: { ...MINIMAL_AGENT_DEFINITION.identity, label: 'Agent Y' }, + }); + + const x = await getAgentDefinition('agent-x'); + const y = await getAgentDefinition('agent-y'); + + expect(x?.identity.label).toBe('Agent X'); + expect(y?.identity.label).toBe('Agent Y'); + }); + }); + + // ========================================================================= + // listAgentDefinitions + // ========================================================================= + + describe('listAgentDefinitions', () => { + it('returns empty array when no definitions exist', async () => { + const list = await listAgentDefinitions(); + expect(list).toHaveLength(0); + }); + + it('returns all inserted definitions', async () => { + await upsertAgentDefinition('list-agent-1', MINIMAL_AGENT_DEFINITION); + await upsertAgentDefinition('list-agent-2', MINIMAL_AGENT_DEFINITION); + + const list = await listAgentDefinitions(); + expect(list).toHaveLength(2); + const agentTypes = list.map((d) => d.agentType).sort(); + expect(agentTypes).toEqual(['list-agent-1', 'list-agent-2']); + }); + + it('returns entries with agentType, definition, and isBuiltin fields', async () => { + await upsertAgentDefinition('list-fields-agent', MINIMAL_AGENT_DEFINITION, true); + + const list = await listAgentDefinitions(); + expect(list).toHaveLength(1); + + const entry = list[0]; + expect(entry.agentType).toBe('list-fields-agent'); + expect(entry.definition).toBeDefined(); + expect(entry.isBuiltin).toBe(true); + }); + + it('returns Zod-parsed definitions for each entry', async () => { + await upsertAgentDefinition('list-zod-agent', MINIMAL_AGENT_DEFINITION); + + const list = await listAgentDefinitions(); + expect(list).toHaveLength(1); + + // Each definition should be parseable by AgentDefinitionSchema without error + for (const entry of list) { + expect(() => AgentDefinitionSchema.parse(entry.definition)).not.toThrow(); + } + }); + }); + + // ========================================================================= + // deleteAgentDefinition + // ========================================================================= + + describe('deleteAgentDefinition', () => { + it('removes the definition by agentType', async () => { + await upsertAgentDefinition('delete-me', MINIMAL_AGENT_DEFINITION); + + await deleteAgentDefinition('delete-me'); + + const result = await getAgentDefinition('delete-me'); + expect(result).toBeNull(); + }); + + it('does not affect other definitions when deleting one', async () => { + await upsertAgentDefinition('keep-me', MINIMAL_AGENT_DEFINITION); + await upsertAgentDefinition('delete-me-2', MINIMAL_AGENT_DEFINITION); + + await deleteAgentDefinition('delete-me-2'); + + const list = await listAgentDefinitions(); + expect(list).toHaveLength(1); + expect(list[0].agentType).toBe('keep-me'); + }); + + it('is idempotent — deleting a non-existent agentType does not throw', async () => { + await expect(deleteAgentDefinition('nonexistent-agent')).resolves.not.toThrow(); + }); + }); + + // ========================================================================= + // Zod validation round-trip (JSONB) + // ========================================================================= + + describe('Zod validation round-trip', () => { + it('round-trips a valid AgentDefinition through upsert and read', async () => { + const definition: AgentDefinition = { + ...MINIMAL_AGENT_DEFINITION, + identity: { + emoji: '🔧', + label: 'Round-trip Agent', + roleHint: 'Tests round-trip fidelity', + initialMessage: 'Starting round-trip test...', + }, + capabilities: { + required: ['fs:read', 'fs:write'], + optional: ['pm:read'], + }, + triggers: [ + { + event: 'pm:status-changed', + label: 'Status changed', + defaultEnabled: false, + parameters: [], + }, + ], + hint: 'Round-trip hint.', + prompts: { + taskPrompt: 'Perform the round-trip task.', + systemPrompt: 'You are a round-trip test agent.', + }, + }; + + await upsertAgentDefinition('round-trip-agent', definition); + + const retrieved = await getAgentDefinition('round-trip-agent'); + expect(retrieved).not.toBeNull(); + + // Core identity fields + expect(retrieved?.identity.emoji).toBe('🔧'); + expect(retrieved?.identity.label).toBe('Round-trip Agent'); + + // Capabilities + expect(retrieved?.capabilities.required).toContain('fs:read'); + expect(retrieved?.capabilities.required).toContain('fs:write'); + expect(retrieved?.capabilities.optional).toContain('pm:read'); + + // Triggers + expect(retrieved?.triggers).toHaveLength(1); + expect(retrieved?.triggers[0].event).toBe('pm:status-changed'); + + // Prompts + expect(retrieved?.prompts.taskPrompt).toBe('Perform the round-trip task.'); + expect(retrieved?.prompts.systemPrompt).toBe('You are a round-trip test agent.'); + + // Hint + expect(retrieved?.hint).toBe('Round-trip hint.'); + }); + + it('Zod applies defaults on read (e.g., triggers defaults to [])', async () => { + // Insert a definition that will have triggers defaulted + await upsertAgentDefinition('defaults-agent', MINIMAL_AGENT_DEFINITION); + + const result = await getAgentDefinition('defaults-agent'); + // triggers has a .default([]) in the schema + expect(result?.triggers).toEqual([]); + }); + + it('rejects a definition with a capability not in the CAPABILITIES registry', async () => { + const invalidDefinition = { + ...MINIMAL_AGENT_DEFINITION, + capabilities: { + required: ['fs:read', 'not-a-real:capability'], + optional: [], + }, + } as unknown as AgentDefinition; + + await expect( + upsertAgentDefinition('invalid-capability-agent', invalidDefinition), + ).rejects.toThrow(); + }); + + it('rejects a definition where a capability is both required and optional', async () => { + const invalidDefinition = { + ...MINIMAL_AGENT_DEFINITION, + capabilities: { + required: ['fs:read'], + optional: ['fs:read'], // same as required — violates refine + }, + } as unknown as AgentDefinition; + + await expect( + upsertAgentDefinition('duplicate-capability-agent', invalidDefinition), + ).rejects.toThrow(); + }); + }); + + // ========================================================================= + // seedAgentDefinition helper + // ========================================================================= + + describe('seedAgentDefinition helper', () => { + it('creates a definition that appears in listAgentDefinitions', async () => { + await seedAgentDefinition({ agentType: 'seeded-agent' }); + + const list = await listAgentDefinitions(); + const entry = list.find((d) => d.agentType === 'seeded-agent'); + expect(entry).toBeDefined(); + expect(entry?.definition.identity.label).toBe('Test Agent'); + }); + + it('respects isBuiltin override', async () => { + await seedAgentDefinition({ agentType: 'seeded-builtin', isBuiltin: true }); + + const list = await listAgentDefinitions(); + const entry = list.find((d) => d.agentType === 'seeded-builtin'); + expect(entry?.isBuiltin).toBe(true); + }); + + it('respects definition overrides', async () => { + await seedAgentDefinition({ + agentType: 'seeded-custom', + definition: { + identity: { + emoji: '⭐', + label: 'Custom Seeded Agent', + roleHint: 'Custom role', + initialMessage: 'Custom message', + }, + }, + }); + + const result = await getAgentDefinition('seeded-custom'); + expect(result?.identity.label).toBe('Custom Seeded Agent'); + expect(result?.identity.emoji).toBe('⭐'); + }); + + it('defaults agentType to "test-agent" when not specified', async () => { + await seedAgentDefinition(); + + const result = await getAgentDefinition('test-agent'); + expect(result).not.toBeNull(); + }); + }); +}); diff --git a/tests/integration/helpers/db.ts b/tests/integration/helpers/db.ts index 0dea42d5..4fb2caf6 100644 --- a/tests/integration/helpers/db.ts +++ b/tests/integration/helpers/db.ts @@ -120,6 +120,7 @@ export async function truncateAll() { project_integrations, agent_trigger_configs, agent_configs, + agent_definitions, prompt_partials, sessions, users, diff --git a/tests/integration/helpers/seed.ts b/tests/integration/helpers/seed.ts index d35e8fe6..8d92cb6f 100644 --- a/tests/integration/helpers/seed.ts +++ b/tests/integration/helpers/seed.ts @@ -1,4 +1,6 @@ +import type { AgentDefinition } from '../../../src/agents/definitions/schema.js'; import { getDb } from '../../../src/db/client.js'; +import { upsertAgentDefinition } from '../../../src/db/repositories/agentDefinitionsRepository.js'; import { writeProjectCredential } from '../../../src/db/repositories/credentialsRepository.js'; import { agentConfigs, @@ -302,6 +304,51 @@ export async function seedSession(overrides: { return row; } +/** + * Minimal valid AgentDefinition fixture that satisfies AgentDefinitionSchema.parse(). + */ +export const MINIMAL_AGENT_DEFINITION: AgentDefinition = { + identity: { + emoji: '🤖', + label: 'Test Agent', + roleHint: 'A minimal test agent definition', + initialMessage: 'Starting test agent...', + }, + capabilities: { + required: ['fs:read'], + optional: [], + }, + triggers: [], + strategies: { + gadgetOptions: undefined, + }, + hint: 'This is a test hint for iteration guidance.', + prompts: { + taskPrompt: 'Perform the test task as described.', + }, +}; + +/** + * Seeds an agent definition via the repository's upsertAgentDefinition function. + * Merges overrides into the minimal valid AgentDefinition. + */ +export async function seedAgentDefinition( + overrides: { + agentType?: string; + definition?: Partial; + isBuiltin?: boolean; + } = {}, +) { + const agentType = overrides.agentType ?? 'test-agent'; + const definition: AgentDefinition = { + ...MINIMAL_AGENT_DEFINITION, + ...overrides.definition, + }; + const isBuiltin = overrides.isBuiltin ?? false; + await upsertAgentDefinition(agentType, definition, isBuiltin); + return { agentType, definition, isBuiltin }; +} + // ============================================================================ // Composite helpers for common integration setups // ============================================================================