- Renamed the 'check' script to 'check:workspace' for clarity and updated the main 'check' script to include linting. - Modified CI workflow to monitor changes in the '.github/workflows/**' and 'pnpm-workspace.yaml' files, ensuring better integration and tracking. - Added new tests to validate task lifecycle and review processes, enhancing overall test coverage and reliability.
411 lines
10 KiB
TypeScript
411 lines
10 KiB
TypeScript
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<string, unknown>) => Promise<unknown> | unknown;
|
|
};
|
|
|
|
function collectTools() {
|
|
const tools = new Map<string, RegisteredTool>();
|
|
|
|
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',
|
|
})
|
|
);
|
|
|
|
expect(reviewRequested.reviewState).toBe('review');
|
|
|
|
const approved = parseJsonToolResult(
|
|
await getTool('review_approve').execute({
|
|
claudeDir,
|
|
teamName,
|
|
taskId: createdTask.id,
|
|
from: 'lead',
|
|
note: 'Looks good',
|
|
notifyOwner: true,
|
|
})
|
|
);
|
|
expect(approved.reviewState).toBe('approved');
|
|
|
|
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',
|
|
});
|
|
|
|
const changesRequested = parseJsonToolResult(
|
|
await getTool('review_request_changes').execute({
|
|
claudeDir,
|
|
teamName,
|
|
taskId: createdTask.id,
|
|
from: 'alice',
|
|
comment: 'Please revise this section.',
|
|
})
|
|
);
|
|
|
|
expect(changesRequested.status).toBe('in_progress');
|
|
expect(changesRequested.reviewState).toBe('none');
|
|
|
|
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);
|
|
});
|
|
});
|