/** * Agent Router Tests * * Tests for getAgent (exitCode, taskName, initiativeName), * getAgentInputFiles, and getAgentPrompt procedures. */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; import { appRouter, createCallerFactory } from '../index.js'; import type { TRPCContext } from '../context.js'; import type { EventBus } from '../../events/types.js'; const createCaller = createCallerFactory(appRouter); function createMockEventBus(): EventBus { return { emit: vi.fn(), on: vi.fn(), off: vi.fn(), once: vi.fn(), }; } function createTestContext(overrides: Partial = {}): TRPCContext { return { eventBus: createMockEventBus(), serverStartedAt: new Date('2026-01-30T12:00:00Z'), processCount: 0, ...overrides, }; } /** Minimal AgentInfo fixture matching the full interface */ function makeAgentInfo(overrides: Record = {}) { return { id: 'agent-1', name: 'test-agent', taskId: null, initiativeId: null, sessionId: null, worktreeId: 'test-agent', status: 'stopped' as const, mode: 'execute' as const, provider: 'claude', accountId: null, createdAt: new Date('2026-01-01T00:00:00Z'), updatedAt: new Date('2026-01-01T00:00:00Z'), userDismissedAt: null, exitCode: null, prompt: null, ...overrides, }; } describe('getAgent', () => { it('returns exitCode: 1 when agent has exitCode 1', async () => { const mockManager = { get: vi.fn().mockResolvedValue(makeAgentInfo({ exitCode: 1 })), }; const ctx = createTestContext({ agentManager: mockManager as any }); const caller = createCaller(ctx); const result = await caller.getAgent({ id: 'agent-1' }); expect(result.exitCode).toBe(1); }); it('returns exitCode: null when agent has no exitCode', async () => { const mockManager = { get: vi.fn().mockResolvedValue(makeAgentInfo({ exitCode: null })), }; const ctx = createTestContext({ agentManager: mockManager as any }); const caller = createCaller(ctx); const result = await caller.getAgent({ id: 'agent-1' }); expect(result.exitCode).toBeNull(); }); it('returns taskName and initiativeName from repositories', async () => { const mockManager = { get: vi.fn().mockResolvedValue(makeAgentInfo({ taskId: 'task-1', initiativeId: 'init-1' })), }; const mockTaskRepo = { findById: vi.fn().mockResolvedValue({ id: 'task-1', name: 'My Task' }), }; const mockInitiativeRepo = { findById: vi.fn().mockResolvedValue({ id: 'init-1', name: 'My Initiative' }), }; const ctx = createTestContext({ agentManager: mockManager as any, taskRepository: mockTaskRepo as any, initiativeRepository: mockInitiativeRepo as any, }); const caller = createCaller(ctx); const result = await caller.getAgent({ id: 'agent-1' }); expect(result.taskName).toBe('My Task'); expect(result.initiativeName).toBe('My Initiative'); }); it('returns taskName: null and initiativeName: null when agent has no taskId or initiativeId', async () => { const mockManager = { get: vi.fn().mockResolvedValue(makeAgentInfo({ taskId: null, initiativeId: null })), }; const ctx = createTestContext({ agentManager: mockManager as any }); const caller = createCaller(ctx); const result = await caller.getAgent({ id: 'agent-1' }); expect(result.taskName).toBeNull(); expect(result.initiativeName).toBeNull(); }); }); describe('getAgentInputFiles', () => { let tmpDir: string; beforeEach(async () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-test-')); }); afterEach(async () => { await fs.rm(tmpDir, { recursive: true, force: true }); }); function makeAgentManagerWithWorktree(worktreeId = 'test-worktree', agentName = 'test-agent') { return { get: vi.fn().mockResolvedValue(makeAgentInfo({ worktreeId, name: agentName })), }; } it('returns worktree_missing when worktree dir does not exist', async () => { const nonExistentRoot = path.join(tmpDir, 'no-such-dir'); const mockManager = makeAgentManagerWithWorktree('test-worktree'); const ctx = createTestContext({ agentManager: mockManager as any, workspaceRoot: nonExistentRoot, }); const caller = createCaller(ctx); const result = await caller.getAgentInputFiles({ id: 'agent-1' }); expect(result).toEqual({ files: [], reason: 'worktree_missing' }); }); it('returns input_dir_missing when worktree exists but .cw/input does not', async () => { const worktreeId = 'test-worktree'; const worktreeRoot = path.join(tmpDir, 'agent-workdirs', worktreeId); await fs.mkdir(worktreeRoot, { recursive: true }); const mockManager = makeAgentManagerWithWorktree(worktreeId); const ctx = createTestContext({ agentManager: mockManager as any, workspaceRoot: tmpDir, }); const caller = createCaller(ctx); const result = await caller.getAgentInputFiles({ id: 'agent-1' }); expect(result).toEqual({ files: [], reason: 'input_dir_missing' }); }); it('returns sorted file list with correct name, content, sizeBytes', async () => { const worktreeId = 'test-worktree'; const inputDir = path.join(tmpDir, 'agent-workdirs', worktreeId, '.cw', 'input'); await fs.mkdir(inputDir, { recursive: true }); await fs.mkdir(path.join(inputDir, 'pages'), { recursive: true }); const manifestContent = '{"files": ["a"]}'; const fooContent = '# Foo\nHello world'; await fs.writeFile(path.join(inputDir, 'manifest.json'), manifestContent); await fs.writeFile(path.join(inputDir, 'pages', 'foo.md'), fooContent); const mockManager = makeAgentManagerWithWorktree(worktreeId); const ctx = createTestContext({ agentManager: mockManager as any, workspaceRoot: tmpDir, }); const caller = createCaller(ctx); const result = await caller.getAgentInputFiles({ id: 'agent-1' }); expect(result.reason).toBeUndefined(); expect(result.files).toHaveLength(2); // Sorted alphabetically: manifest.json before pages/foo.md expect(result.files[0].name).toBe('manifest.json'); expect(result.files[0].content).toBe(manifestContent); expect(result.files[0].sizeBytes).toBe(Buffer.byteLength(manifestContent)); expect(result.files[1].name).toBe(path.join('pages', 'foo.md')); expect(result.files[1].content).toBe(fooContent); expect(result.files[1].sizeBytes).toBe(Buffer.byteLength(fooContent)); }); it('skips binary files (containing null byte)', async () => { const worktreeId = 'test-worktree'; const inputDir = path.join(tmpDir, 'agent-workdirs', worktreeId, '.cw', 'input'); await fs.mkdir(inputDir, { recursive: true }); // Binary file with null byte const binaryData = Buffer.from([0x89, 0x50, 0x00, 0x4e, 0x47]); await fs.writeFile(path.join(inputDir, 'image.png'), binaryData); // Text file should still be returned await fs.writeFile(path.join(inputDir, 'text.txt'), 'hello'); const mockManager = makeAgentManagerWithWorktree(worktreeId); const ctx = createTestContext({ agentManager: mockManager as any, workspaceRoot: tmpDir, }); const caller = createCaller(ctx); const result = await caller.getAgentInputFiles({ id: 'agent-1' }); expect(result.files).toHaveLength(1); expect(result.files[0].name).toBe('text.txt'); }); it('truncates files larger than 500 KB and preserves original sizeBytes', async () => { const worktreeId = 'test-worktree'; const inputDir = path.join(tmpDir, 'agent-workdirs', worktreeId, '.cw', 'input'); await fs.mkdir(inputDir, { recursive: true }); const MAX_SIZE = 500 * 1024; const largeContent = Buffer.alloc(MAX_SIZE + 100 * 1024, 'a'); // 600 KB await fs.writeFile(path.join(inputDir, 'big.txt'), largeContent); const mockManager = makeAgentManagerWithWorktree(worktreeId); const ctx = createTestContext({ agentManager: mockManager as any, workspaceRoot: tmpDir, }); const caller = createCaller(ctx); const result = await caller.getAgentInputFiles({ id: 'agent-1' }); expect(result.files).toHaveLength(1); expect(result.files[0].sizeBytes).toBe(largeContent.length); expect(result.files[0].content).toContain('[truncated — file exceeds 500 KB]'); }); }); describe('getAgentPrompt', () => { let tmpDir: string; beforeEach(async () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'agent-prompt-test-')); }); afterEach(async () => { await fs.rm(tmpDir, { recursive: true, force: true }); }); it('returns prompt_not_written when PROMPT.md does not exist', async () => { const mockManager = { get: vi.fn().mockResolvedValue(makeAgentInfo({ name: 'test-agent' })), }; const ctx = createTestContext({ agentManager: mockManager as any, workspaceRoot: tmpDir, }); const caller = createCaller(ctx); const result = await caller.getAgentPrompt({ id: 'agent-1' }); expect(result).toEqual({ content: null, reason: 'prompt_not_written' }); }); it('returns prompt content when PROMPT.md exists', async () => { const agentName = 'test-agent'; const promptDir = path.join(tmpDir, '.cw', 'agent-logs', agentName); await fs.mkdir(promptDir, { recursive: true }); const promptContent = '# System\nHello'; await fs.writeFile(path.join(promptDir, 'PROMPT.md'), promptContent); const mockManager = { get: vi.fn().mockResolvedValue(makeAgentInfo({ name: agentName, prompt: null })), }; const ctx = createTestContext({ agentManager: mockManager as any, workspaceRoot: tmpDir, }); const caller = createCaller(ctx); const result = await caller.getAgentPrompt({ id: 'agent-1' }); expect(result).toEqual({ content: promptContent }); }); it('returns prompt from DB when agent.prompt is set (no file needed)', async () => { const dbPromptContent = '# DB Prompt\nThis is persisted in the database'; const mockManager = { get: vi.fn().mockResolvedValue(makeAgentInfo({ name: 'test-agent', prompt: dbPromptContent })), }; // workspaceRoot has no PROMPT.md — but DB value takes precedence const ctx = createTestContext({ agentManager: mockManager as any, workspaceRoot: tmpDir, }); const caller = createCaller(ctx); const result = await caller.getAgentPrompt({ id: 'agent-1' }); expect(result).toEqual({ content: dbPromptContent }); }); it('falls back to PROMPT.md when agent.prompt is null in DB', async () => { const agentName = 'test-agent'; const promptDir = path.join(tmpDir, '.cw', 'agent-logs', agentName); await fs.mkdir(promptDir, { recursive: true }); const fileContent = '# File Prompt\nThis is from the file (legacy)'; await fs.writeFile(path.join(promptDir, 'PROMPT.md'), fileContent); const mockManager = { get: vi.fn().mockResolvedValue(makeAgentInfo({ name: agentName, prompt: null })), }; const ctx = createTestContext({ agentManager: mockManager as any, workspaceRoot: tmpDir, }); const caller = createCaller(ctx); const result = await caller.getAgentPrompt({ id: 'agent-1' }); expect(result).toEqual({ content: fileContent }); }); });