import fs from 'fs'; import os from 'os'; import path from 'path'; import { registerTools } from '../src/tools'; type RegisteredTool = { name: string; parameters?: { safeParse: (value: unknown) => { success: boolean } }; execute: (args: Record) => Promise | unknown; }; function collectTools() { const tools = new Map(); registerTools({ addTool(config: RegisteredTool) { tools.set(config.name, config); }, } as never); return tools; } function parseJsonToolResult(result: unknown) { const text = (result as { content: Array<{ text: string }> }).content[0]?.text; return JSON.parse(text); } describe('agent-teams-mcp tools', () => { const tools = collectTools(); const expectedToolNames = [ 'kanban_add_reviewer', 'kanban_clear', 'kanban_get', 'kanban_list_reviewers', 'kanban_remove_reviewer', 'kanban_set_column', 'message_send', 'process_list', 'process_register', 'process_stop', 'process_unregister', 'review_approve', 'review_request', 'review_request_changes', 'task_add_comment', 'task_attach_comment_file', 'task_attach_file', 'task_briefing', 'task_complete', 'task_create', 'task_get', 'task_link', 'task_list', 'task_set_clarification', 'task_set_owner', 'task_set_status', 'task_start', 'task_unlink', ] as const; function getTool(name: string) { const tool = tools.get(name); expect(tool).toBeDefined(); return tool!; } function makeClaudeDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'agent-teams-mcp-')); } it('registers the full expected MCP tool surface', () => { expect([...tools.keys()].sort()).toEqual([...expectedToolNames]); }); it('covers task lifecycle, attachments, relationships, kanban, and review flows', async () => { const claudeDir = makeClaudeDir(); const teamName = 'alpha'; const attachmentPath = path.join(claudeDir, 'note.txt'); fs.writeFileSync(attachmentPath, 'ship it'); const dependencyTask = parseJsonToolResult( await getTool('task_create').execute({ claudeDir, teamName, subject: 'Dependency', }) ); const createdTask = parseJsonToolResult( await getTool('task_create').execute({ claudeDir, teamName, subject: 'Review MCP adapter', owner: 'alice', }) ); const listedTasks = parseJsonToolResult( await getTool('task_list').execute({ claudeDir, teamName, }) ); expect(listedTasks).toHaveLength(2); const linked = parseJsonToolResult( await getTool('task_link').execute({ claudeDir, teamName, taskId: createdTask.id, targetId: dependencyTask.id, relationship: 'blocked-by', }) ); expect(linked.blockedBy).toContain(dependencyTask.id); const unlinked = parseJsonToolResult( await getTool('task_unlink').execute({ claudeDir, teamName, taskId: createdTask.id, targetId: dependencyTask.id, relationship: 'blocked-by', }) ); expect(unlinked.blockedBy ?? []).not.toContain(dependencyTask.id); const owned = parseJsonToolResult( await getTool('task_set_owner').execute({ claudeDir, teamName, taskId: createdTask.id, owner: 'alice', }) ); expect(owned.owner).toBe('alice'); const commented = parseJsonToolResult( await getTool('task_add_comment').execute({ claudeDir, teamName, taskId: createdTask.id, text: 'Need one more check', from: 'lead', }) ); const commentId = commented.commentId; expect(commentId).toBeTruthy(); const attachment = parseJsonToolResult( await getTool('task_attach_comment_file').execute({ claudeDir, teamName, taskId: createdTask.id, commentId, filePath: attachmentPath, mode: 'copy', }) ); expect(attachment.filename).toBe('note.txt'); const taskAttachment = parseJsonToolResult( await getTool('task_attach_file').execute({ claudeDir, teamName, taskId: createdTask.id, filePath: attachmentPath, mode: 'copy', }) ); expect(taskAttachment.filename).toBe('note.txt'); await getTool('task_set_clarification').execute({ claudeDir, teamName, taskId: createdTask.id, value: 'user', }); const loadedTask = parseJsonToolResult( await getTool('task_get').execute({ claudeDir, teamName, taskId: createdTask.id, }) ); expect(loadedTask.needsClarification).toBe('user'); expect(loadedTask.comments).toHaveLength(1); expect(loadedTask.comments[0].attachments).toHaveLength(1); expect(loadedTask.attachments).toHaveLength(1); const started = parseJsonToolResult( await getTool('task_start').execute({ claudeDir, teamName, taskId: createdTask.id, actor: 'alice', }) ); expect(started.status).toBe('in_progress'); await getTool('task_set_status').execute({ claudeDir, teamName, taskId: createdTask.id, status: 'completed', }); parseJsonToolResult( await getTool('kanban_add_reviewer').execute({ claudeDir, teamName, reviewer: 'alice', }) ); const reviewers = parseJsonToolResult( await getTool('kanban_list_reviewers').execute({ claudeDir, teamName, }) ); expect(reviewers).toEqual(['alice']); const reviewRequested = parseJsonToolResult( await getTool('review_request').execute({ claudeDir, teamName, taskId: createdTask.id, from: 'lead', reviewer: 'alice', leadSessionId: 'session-review-1', }) ); expect(reviewRequested.reviewState).toBe('review'); const reviewerInboxPath = path.join(claudeDir, 'teams', teamName, 'inboxes', 'alice.json'); const reviewerInbox = JSON.parse(fs.readFileSync(reviewerInboxPath, 'utf8')); expect(reviewerInbox.at(-1).leadSessionId).toBe('session-review-1'); const approved = parseJsonToolResult( await getTool('review_approve').execute({ claudeDir, teamName, taskId: createdTask.id, from: 'lead', note: 'Looks good', notifyOwner: true, leadSessionId: 'session-review-1', }) ); expect(approved.reviewState).toBe('approved'); const ownerInboxPath = path.join(claudeDir, 'teams', teamName, 'inboxes', 'alice.json'); const ownerInbox = JSON.parse(fs.readFileSync(ownerInboxPath, 'utf8')); expect(ownerInbox.at(-1).leadSessionId).toBe('session-review-1'); const kanbanState = parseJsonToolResult( await getTool('kanban_get').execute({ claudeDir, teamName, }) ); expect(kanbanState.tasks[createdTask.id].column).toBe('approved'); const briefing = await getTool('task_briefing').execute({ claudeDir, teamName, memberName: 'alice', }); expect((briefing as { content: Array<{ text: string }> }).content[0]?.text).toContain( 'Review MCP adapter' ); }); it('covers review_request_changes and full process lifecycle tools', async () => { const claudeDir = makeClaudeDir(); const teamName = 'beta'; const createdTask = parseJsonToolResult( await getTool('task_create').execute({ claudeDir, teamName, subject: 'Needs revision', owner: 'bob', }) ); await getTool('task_complete').execute({ claudeDir, teamName, taskId: createdTask.id, actor: 'bob', }); await getTool('review_request').execute({ claudeDir, teamName, taskId: createdTask.id, from: 'lead', reviewer: 'alice', leadSessionId: 'session-review-2', }); const changesRequested = parseJsonToolResult( await getTool('review_request_changes').execute({ claudeDir, teamName, taskId: createdTask.id, from: 'alice', comment: 'Please revise this section.', leadSessionId: 'session-review-2', }) ); expect(changesRequested.status).toBe('in_progress'); expect(changesRequested.reviewState).toBe('none'); const ownerInboxPath = path.join(claudeDir, 'teams', teamName, 'inboxes', 'bob.json'); const ownerInbox = JSON.parse(fs.readFileSync(ownerInboxPath, 'utf8')); expect(ownerInbox.at(-1).leadSessionId).toBe('session-review-2'); const kanbanCleared = parseJsonToolResult( await getTool('kanban_clear').execute({ claudeDir, teamName, taskId: createdTask.id, }) ); expect(kanbanCleared.tasks[createdTask.id]).toBeUndefined(); const pid = process.pid; const registered = parseJsonToolResult( await getTool('process_register').execute({ claudeDir, teamName, pid, label: 'vite', command: 'pnpm dev', from: 'lead', port: 3000, }) ); expect(registered.pid).toBe(pid); expect(registered.label).toBe('vite'); const listed = parseJsonToolResult( await getTool('process_list').execute({ claudeDir, teamName, }) ); expect(listed).toHaveLength(1); expect(listed[0].pid).toBe(pid); const stopped = parseJsonToolResult( await getTool('process_stop').execute({ claudeDir, teamName, pid, }) ); expect(stopped.pid).toBe(pid); expect(typeof stopped.stoppedAt).toBe('string'); const unregistered = parseJsonToolResult( await getTool('process_unregister').execute({ claudeDir, teamName, pid, }) ); expect(unregistered).toEqual([]); }); it('persists full message metadata through message_send', async () => { const claudeDir = makeClaudeDir(); const teamName = 'gamma'; const sent = parseJsonToolResult( await getTool('message_send').execute({ claudeDir, teamName, to: 'alice', text: 'Check this', from: 'lead', summary: 'Metadata test', source: 'system_notification', leadSessionId: 'session-42', attachments: [{ id: 'att-1', filename: 'note.txt', mimeType: 'text/plain', size: 4 }], }) ); expect(sent.deliveredToInbox).toBe(true); const inboxPath = path.join(claudeDir, 'teams', teamName, 'inboxes', 'alice.json'); const rows = JSON.parse(fs.readFileSync(inboxPath, 'utf8')); expect(rows[0].source).toBe('system_notification'); expect(rows[0].leadSessionId).toBe('session-42'); expect(rows[0].attachments[0].filename).toBe('note.txt'); }); it('exposes zod schemas that reject obviously invalid payloads', () => { expect( getTool('task_create').parameters?.safeParse({ teamName: 'demo', claudeDir: '/tmp/demo', }).success ).toBe(false); expect( getTool('process_register').parameters?.safeParse({ teamName: 'demo', pid: 0, label: '', }).success ).toBe(false); }); });