/** * DrizzleAgentRepository Tests * * Tests for the Agent repository adapter. */ import { describe, it, expect, beforeEach } from 'vitest'; import { DrizzleAgentRepository } from './agent.js'; import { DrizzleTaskRepository } from './task.js'; import { DrizzlePhaseRepository } from './phase.js'; import { DrizzleInitiativeRepository } from './initiative.js'; import { createTestDatabase } from './test-helpers.js'; import type { DrizzleDatabase } from '../../index.js'; describe('DrizzleAgentRepository', () => { let db: DrizzleDatabase; let agentRepo: DrizzleAgentRepository; let taskRepo: DrizzleTaskRepository; let phaseRepo: DrizzlePhaseRepository; let initiativeRepo: DrizzleInitiativeRepository; let testTaskId: string; beforeEach(async () => { db = createTestDatabase(); agentRepo = new DrizzleAgentRepository(db); taskRepo = new DrizzleTaskRepository(db); phaseRepo = new DrizzlePhaseRepository(db); initiativeRepo = new DrizzleInitiativeRepository(db); // Create full hierarchy for FK constraint const initiative = await initiativeRepo.create({ name: 'Test Initiative', }); const phase = await phaseRepo.create({ initiativeId: initiative.id, name: 'Test Phase', }); const task = await taskRepo.create({ phaseId: phase.id, name: 'Test Task', order: 1, }); testTaskId = task.id; }); describe('create', () => { it('should create an agent with generated id and timestamps', async () => { const agent = await agentRepo.create({ name: 'gastown', worktreeId: 'worktree-123', taskId: testTaskId, }); expect(agent.id).toBeDefined(); expect(agent.id.length).toBeGreaterThan(0); expect(agent.name).toBe('gastown'); expect(agent.worktreeId).toBe('worktree-123'); expect(agent.taskId).toBe(testTaskId); expect(agent.sessionId).toBeNull(); expect(agent.status).toBe('idle'); expect(agent.createdAt).toBeInstanceOf(Date); expect(agent.updatedAt).toBeInstanceOf(Date); }); it('should create agent without taskId', async () => { const agent = await agentRepo.create({ name: 'standalone', worktreeId: 'worktree-456', }); expect(agent.taskId).toBeNull(); }); it('should reject duplicate names', async () => { await agentRepo.create({ name: 'unique-name', worktreeId: 'worktree-1', }); await expect( agentRepo.create({ name: 'unique-name', worktreeId: 'worktree-2', }) ).rejects.toThrow(); }); }); describe('findById', () => { it('should return null for non-existent agent', async () => { const result = await agentRepo.findById('non-existent-id'); expect(result).toBeNull(); }); it('should find an existing agent', async () => { const created = await agentRepo.create({ name: 'findme', worktreeId: 'worktree-123', }); const found = await agentRepo.findById(created.id); expect(found).not.toBeNull(); expect(found!.id).toBe(created.id); expect(found!.name).toBe('findme'); }); }); describe('findByName', () => { it('should return null for non-existent name', async () => { const result = await agentRepo.findByName('nonexistent'); expect(result).toBeNull(); }); it('should find agent by human-readable name', async () => { await agentRepo.create({ name: 'chinatown', worktreeId: 'worktree-123', }); const found = await agentRepo.findByName('chinatown'); expect(found).not.toBeNull(); expect(found!.name).toBe('chinatown'); }); }); describe('findByTaskId', () => { it('should return null when no agent assigned to task', async () => { const result = await agentRepo.findByTaskId(testTaskId); expect(result).toBeNull(); }); it('should find agent by task', async () => { await agentRepo.create({ name: 'task-agent', worktreeId: 'worktree-123', taskId: testTaskId, }); const found = await agentRepo.findByTaskId(testTaskId); expect(found).not.toBeNull(); expect(found!.taskId).toBe(testTaskId); }); }); describe('findBySessionId', () => { it('should return null when no agent has session', async () => { const result = await agentRepo.findBySessionId('session-123'); expect(result).toBeNull(); }); it('should find agent by session ID', async () => { const agent = await agentRepo.create({ name: 'session-agent', worktreeId: 'worktree-123', }); await agentRepo.update(agent.id, { sessionId: 'session-abc' }); const found = await agentRepo.findBySessionId('session-abc'); expect(found).not.toBeNull(); expect(found!.sessionId).toBe('session-abc'); }); }); describe('findAll', () => { it('should return empty array when no agents', async () => { const agents = await agentRepo.findAll(); expect(agents).toEqual([]); }); it('should return all agents', async () => { await agentRepo.create({ name: 'agent-1', worktreeId: 'wt-1' }); await agentRepo.create({ name: 'agent-2', worktreeId: 'wt-2' }); await agentRepo.create({ name: 'agent-3', worktreeId: 'wt-3' }); const agents = await agentRepo.findAll(); expect(agents.length).toBe(3); }); }); describe('findByStatus', () => { it('should return empty array when no agents have status', async () => { const agents = await agentRepo.findByStatus('running'); expect(agents).toEqual([]); }); it('should filter by status correctly', async () => { const agent1 = await agentRepo.create({ name: 'idle-agent', worktreeId: 'wt-1', }); const agent2 = await agentRepo.create({ name: 'running-agent', worktreeId: 'wt-2', }); await agentRepo.update(agent2.id, { status: 'running' }); const idleAgents = await agentRepo.findByStatus('idle'); const runningAgents = await agentRepo.findByStatus('running'); expect(idleAgents.length).toBe(1); expect(idleAgents[0].name).toBe('idle-agent'); expect(runningAgents.length).toBe(1); expect(runningAgents[0].name).toBe('running-agent'); }); it('should filter by waiting_for_input status', async () => { const agent = await agentRepo.create({ name: 'waiting-agent', worktreeId: 'wt-1', }); await agentRepo.update(agent.id, { status: 'waiting_for_input' }); const waitingAgents = await agentRepo.findByStatus('waiting_for_input'); expect(waitingAgents.length).toBe(1); expect(waitingAgents[0].status).toBe('waiting_for_input'); }); }); describe('update', () => { it('should change status and updatedAt', async () => { const created = await agentRepo.create({ name: 'status-test', worktreeId: 'wt-1', }); await new Promise((resolve) => setTimeout(resolve, 10)); const updated = await agentRepo.update(created.id, { status: 'running' }); expect(updated.status).toBe('running'); expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual( created.updatedAt.getTime() ); }); it('should change sessionId and updatedAt', async () => { const created = await agentRepo.create({ name: 'session-test', worktreeId: 'wt-1', }); expect(created.sessionId).toBeNull(); await new Promise((resolve) => setTimeout(resolve, 10)); const updated = await agentRepo.update(created.id, { sessionId: 'new-session-id' }); expect(updated.sessionId).toBe('new-session-id'); expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual( created.updatedAt.getTime() ); }); it('should throw for non-existent agent', async () => { await expect( agentRepo.update('non-existent-id', { status: 'running' }) ).rejects.toThrow('Agent not found'); }); }); describe('delete', () => { it('should delete an existing agent', async () => { const created = await agentRepo.create({ name: 'to-delete', worktreeId: 'wt-1', }); await agentRepo.delete(created.id); const found = await agentRepo.findById(created.id); expect(found).toBeNull(); }); it('should throw for non-existent agent', async () => { await expect(agentRepo.delete('non-existent-id')).rejects.toThrow( 'Agent not found' ); }); }); }); describe('DrizzleAgentRepository.findWaitingWithContext()', () => { let agentRepo: DrizzleAgentRepository; let taskRepo: DrizzleTaskRepository; let phaseRepo: DrizzlePhaseRepository; let initiativeRepo: DrizzleInitiativeRepository; beforeEach(() => { const db = createTestDatabase(); agentRepo = new DrizzleAgentRepository(db); taskRepo = new DrizzleTaskRepository(db); phaseRepo = new DrizzlePhaseRepository(db); initiativeRepo = new DrizzleInitiativeRepository(db); }); it('returns empty array when no waiting agents exist', async () => { const result = await agentRepo.findWaitingWithContext(); expect(result).toEqual([]); }); it('only returns agents with status waiting_for_input', async () => { await agentRepo.create({ name: 'running-agent', worktreeId: 'wt1', status: 'running' }); await agentRepo.create({ name: 'waiting-agent', worktreeId: 'wt2', status: 'waiting_for_input' }); const result = await agentRepo.findWaitingWithContext(); expect(result).toHaveLength(1); expect(result[0].name).toBe('waiting-agent'); }); it('populates taskName, phaseName, initiativeName, taskDescription when FK associations exist', async () => { const initiative = await initiativeRepo.create({ name: 'My Initiative' }); const phase = await phaseRepo.create({ initiativeId: initiative.id, name: 'Phase 1' }); const task = await taskRepo.create({ phaseId: phase.id, name: 'Implement feature', description: 'Write the feature code', }); await agentRepo.create({ name: 'ctx-agent', worktreeId: 'wt3', status: 'waiting_for_input', taskId: task.id, initiativeId: initiative.id, }); const result = await agentRepo.findWaitingWithContext(); expect(result).toHaveLength(1); expect(result[0].taskName).toBe('Implement feature'); expect(result[0].phaseName).toBe('Phase 1'); expect(result[0].initiativeName).toBe('My Initiative'); expect(result[0].taskDescription).toBe('Write the feature code'); }); it('returns null for context fields when agent has no taskId or initiativeId', async () => { await agentRepo.create({ name: 'bare-agent', worktreeId: 'wt4', status: 'waiting_for_input' }); const result = await agentRepo.findWaitingWithContext(); expect(result).toHaveLength(1); expect(result[0].taskName).toBeNull(); expect(result[0].phaseName).toBeNull(); expect(result[0].initiativeName).toBeNull(); expect(result[0].taskDescription).toBeNull(); }); it('returns null phaseName when task has no phaseId', async () => { const initiative = await initiativeRepo.create({ name: 'Orphan Init' }); const task = await taskRepo.create({ phaseId: null, name: 'Orphan Task', description: null, }); await agentRepo.create({ name: 'orphan-agent', worktreeId: 'wt5', status: 'waiting_for_input', taskId: task.id, initiativeId: initiative.id, }); const result = await agentRepo.findWaitingWithContext(); expect(result).toHaveLength(1); expect(result[0].phaseName).toBeNull(); expect(result[0].taskName).toBe('Orphan Task'); expect(result[0].initiativeName).toBe('Orphan Init'); }); });