feat: enrich listWaitingAgents with task/phase/initiative context via DB joins

Replaces the in-memory filter (agentManager.list() + filter) with a direct
repository query that LEFT JOINs tasks, phases, and initiatives to return
taskName, phaseName, initiativeName, and taskDescription alongside agent fields.

- Adds AgentWithContext interface and findWaitingWithContext() to AgentRepository port
- Implements findWaitingWithContext() in DrizzleAgentRepository using getTableColumns
- Wires agentRepository into TRPCContext, CreateContextOptions, and TrpcAdapterOptions
- Adds requireAgentRepository() helper following existing pattern
- Updates listWaitingAgents to use repository query instead of agentManager
- Adds 5 unit tests for findWaitingWithContext() covering all FK join edge cases
- Updates existing AgentRepository mocks to satisfy updated interface

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas May
2026-03-06 23:29:49 +01:00
parent 79a0bd0a74
commit 7e6921f01e
12 changed files with 151 additions and 10 deletions

View File

@@ -277,3 +277,91 @@ describe('DrizzleAgentRepository', () => {
});
});
});
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');
});
});