Copies source (no node_modules, dist, .tasks, .wiki, __pycache__) for: - projects-meta-mcp v2.25.0 (TypeScript/Node) - wiki-graph v0.3.1 (TypeScript/Node) - interns-mcp v0.3.3 (Python/FastMCP) .gitignore: exclude lib build artefacts (node_modules, dist, .venv, __pycache__, *.pyc) bootstrap.ps1: add MCP build step — npm install+build for TS servers, venv+pip for Python Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
374 lines
12 KiB
TypeScript
374 lines
12 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { runTasksCli } from '../../src/tasks-cli.js';
|
|
import type { ToolDef, ToolResult } from '../../src/tools/types.js';
|
|
|
|
function fakeTool(
|
|
name: string,
|
|
impl: (args: Record<string, unknown>) => ToolResult,
|
|
): ToolDef {
|
|
return {
|
|
name,
|
|
description: '',
|
|
inputSchema: { type: 'object' },
|
|
handler: async (args) => impl(args),
|
|
};
|
|
}
|
|
|
|
function jsonResult(value: unknown): ToolResult {
|
|
return { content: [{ type: 'text', text: JSON.stringify(value) }] };
|
|
}
|
|
|
|
describe('runTasksCli', () => {
|
|
it('routes claim-next to tasks.claim_next with parsed flags and prints the handler JSON', async () => {
|
|
let received: Record<string, unknown> | undefined;
|
|
const tools = [
|
|
fakeTool('tasks.claim_next', (args) => {
|
|
received = args;
|
|
return jsonResult({ ok: true, claimed: true, slug: 'foo' });
|
|
}),
|
|
];
|
|
const out: string[] = [];
|
|
|
|
const code = await runTasksCli(
|
|
[
|
|
'claim-next',
|
|
'--claimer-identity',
|
|
'host:claude-opus:123',
|
|
'--runtime',
|
|
'claude-opus',
|
|
'--requirements',
|
|
'needs-db,needs-internet',
|
|
'--confirm',
|
|
],
|
|
{ tools, log: (s) => out.push(s) },
|
|
);
|
|
|
|
expect(code).toBe(0);
|
|
expect(received).toEqual({
|
|
claimer_identity: 'host:claude-opus:123',
|
|
confirm: true,
|
|
filter: { runtime: 'claude-opus', requirements: ['needs-db', 'needs-internet'] },
|
|
});
|
|
expect(JSON.parse(out.join('\n'))).toEqual({ ok: true, claimed: true, slug: 'foo' });
|
|
});
|
|
|
|
it('routes append-decision-trail to tasks.append_decision_trail, splitting the chain', async () => {
|
|
let received: Record<string, unknown> | undefined;
|
|
const tools = [
|
|
fakeTool('tasks.append_decision_trail', (args) => {
|
|
received = args;
|
|
return jsonResult({ committed: true, trail_ref: 'x' });
|
|
}),
|
|
];
|
|
const code = await runTasksCli(
|
|
[
|
|
'append-decision-trail',
|
|
'--target-project',
|
|
'OpeItcLoc03/alpha',
|
|
'--slug',
|
|
'my-task',
|
|
'--question',
|
|
'cache key includes locale?',
|
|
'--decided-by',
|
|
'arbiter',
|
|
'--blast-radius',
|
|
'reversible',
|
|
'--ruling',
|
|
'include locale',
|
|
'--rationale',
|
|
'avoids bleed',
|
|
'--escalation-chain',
|
|
'brief,read-repo',
|
|
],
|
|
{ tools, log: () => {} },
|
|
);
|
|
expect(code).toBe(0);
|
|
expect(received).toEqual({
|
|
target_project: 'OpeItcLoc03/alpha',
|
|
slug: 'my-task',
|
|
question: 'cache key includes locale?',
|
|
decided_by: 'arbiter',
|
|
blast_radius: 'reversible',
|
|
ruling: 'include locale',
|
|
rationale: 'avoids bleed',
|
|
escalation_chain: ['brief', 'read-repo'],
|
|
});
|
|
});
|
|
|
|
it('omits ruling on a halt trail entry', async () => {
|
|
let received: Record<string, unknown> | undefined;
|
|
const tools = [
|
|
fakeTool('tasks.append_decision_trail', (args) => {
|
|
received = args;
|
|
return jsonResult({ committed: true });
|
|
}),
|
|
];
|
|
await runTasksCli(
|
|
[
|
|
'append-decision-trail',
|
|
'--target-project',
|
|
'OpeItcLoc03/alpha',
|
|
'--slug',
|
|
'my-task',
|
|
'--question',
|
|
'priorities?',
|
|
'--decided-by',
|
|
'human-required',
|
|
'--escalation-chain',
|
|
'brief,arbiter-fork',
|
|
],
|
|
{ tools, log: () => {} },
|
|
);
|
|
expect(received).not.toHaveProperty('ruling');
|
|
expect(received!.decided_by).toBe('human-required');
|
|
});
|
|
|
|
it('routes park-question to tasks.park_question with question + optional claim_token', async () => {
|
|
let received: Record<string, unknown> | undefined;
|
|
const tools = [
|
|
fakeTool('tasks.park_question', (args) => {
|
|
received = args;
|
|
return jsonResult({ parked: true });
|
|
}),
|
|
];
|
|
const code = await runTasksCli(
|
|
[
|
|
'park-question',
|
|
'--target-project',
|
|
'OpeItcLoc03/alpha',
|
|
'--slug',
|
|
'my-task',
|
|
'--question',
|
|
'drop the legacy column?',
|
|
'--claim-token',
|
|
'tok-123',
|
|
],
|
|
{ tools, log: () => {} },
|
|
);
|
|
expect(code).toBe(0);
|
|
expect(received).toEqual({
|
|
target_project: 'OpeItcLoc03/alpha',
|
|
slug: 'my-task',
|
|
question: 'drop the legacy column?',
|
|
claim_token: 'tok-123',
|
|
});
|
|
});
|
|
|
|
it('routes get-status to tasks.get_status', async () => {
|
|
let received: Record<string, unknown> | undefined;
|
|
const tools = [
|
|
fakeTool('tasks.get_status', (args) => {
|
|
received = args;
|
|
return jsonResult({ found: true, status: 'blocked' });
|
|
}),
|
|
];
|
|
const code = await runTasksCli(
|
|
['get-status', '--target-project', 'OpeItcLoc03/alpha', '--slug', 'my-task'],
|
|
{ tools, log: () => {} },
|
|
);
|
|
expect(code).toBe(0);
|
|
expect(received).toEqual({ target_project: 'OpeItcLoc03/alpha', slug: 'my-task' });
|
|
});
|
|
|
|
it('routes heartbeat to tasks.heartbeat with slug + claim_token (+ optional target_project)', async () => {
|
|
let received: Record<string, unknown> | undefined;
|
|
const tools = [
|
|
fakeTool('tasks.heartbeat', (args) => {
|
|
received = args;
|
|
return jsonResult({ ok: true });
|
|
}),
|
|
];
|
|
const code = await runTasksCli(
|
|
['heartbeat', '--slug', 'foo', '--claim-token', 'tok-1', '--target-project', 'victor/books'],
|
|
{ tools, log: () => {} },
|
|
);
|
|
expect(code).toBe(0);
|
|
expect(received).toEqual({ slug: 'foo', claim_token: 'tok-1', target_project: 'victor/books' });
|
|
});
|
|
|
|
it('routes close to tasks.close with target_project + slug + note + confirm', async () => {
|
|
let received: Record<string, unknown> | undefined;
|
|
const tools = [
|
|
fakeTool('tasks.close', (args) => {
|
|
received = args;
|
|
return jsonResult({ committed: true });
|
|
}),
|
|
];
|
|
const code = await runTasksCli(
|
|
['close', '--target-project', 'victor/books', '--slug', 'foo', '--note', 'done via poller', '--confirm'],
|
|
{ tools, log: () => {} },
|
|
);
|
|
expect(code).toBe(0);
|
|
expect(received).toEqual({
|
|
target_project: 'victor/books',
|
|
slug: 'foo',
|
|
note: 'done via poller',
|
|
confirm: true,
|
|
});
|
|
});
|
|
|
|
it('routes update to tasks.update with target_project + slug + status + where_stopped', async () => {
|
|
let received: Record<string, unknown> | undefined;
|
|
const tools = [
|
|
fakeTool('tasks.update', (args) => {
|
|
received = args;
|
|
return jsonResult({ committed: true });
|
|
}),
|
|
];
|
|
const code = await runTasksCli(
|
|
[
|
|
'update',
|
|
'--target-project',
|
|
'victor/books',
|
|
'--slug',
|
|
'foo',
|
|
'--status',
|
|
'blocked',
|
|
'--where-stopped',
|
|
'spawn exited 1: boom',
|
|
'--confirm',
|
|
],
|
|
{ tools, log: () => {} },
|
|
);
|
|
expect(code).toBe(0);
|
|
expect(received).toEqual({
|
|
target_project: 'victor/books',
|
|
slug: 'foo',
|
|
status: 'blocked',
|
|
where_stopped: 'spawn exited 1: boom',
|
|
confirm: true,
|
|
});
|
|
});
|
|
|
|
it('passes an explicit empty --blocker= through as blocker:"" (clear the blocker)', async () => {
|
|
// The ops watchdog resets a transient-blocked task with `--status=ready
|
|
// --blocker=`; the empty value must reach the handler so updateTaskFields
|
|
// strips the stale **Blocker:** line, not be dropped as a falsy flag.
|
|
let received: Record<string, unknown> | undefined;
|
|
const tools = [
|
|
fakeTool('tasks.update', (args) => {
|
|
received = args;
|
|
return jsonResult({ committed: true });
|
|
}),
|
|
];
|
|
const code = await runTasksCli(
|
|
['update', '--target-project=victor/books', '--slug=foo', '--status=ready', '--blocker=', '--confirm'],
|
|
{ tools, log: () => {} },
|
|
);
|
|
expect(code).toBe(0);
|
|
expect(received).toEqual({
|
|
target_project: 'victor/books',
|
|
slug: 'foo',
|
|
status: 'ready',
|
|
blocker: '',
|
|
confirm: true,
|
|
});
|
|
});
|
|
|
|
it('omits blocker entirely when --blocker is absent', async () => {
|
|
let received: Record<string, unknown> | undefined;
|
|
const tools = [
|
|
fakeTool('tasks.update', (args) => {
|
|
received = args;
|
|
return jsonResult({ committed: true });
|
|
}),
|
|
];
|
|
const code = await runTasksCli(
|
|
['update', '--target-project=victor/books', '--slug=foo', '--status=ready', '--confirm'],
|
|
{ tools, log: () => {} },
|
|
);
|
|
expect(code).toBe(0);
|
|
expect(received).not.toHaveProperty('blocker');
|
|
});
|
|
|
|
it('returns exit code 1 when the handler result isError', async () => {
|
|
const tools = [
|
|
fakeTool('tasks.claim_next', () => ({
|
|
content: [{ type: 'text', text: 'cache missing — run sync first' }],
|
|
isError: true,
|
|
})),
|
|
];
|
|
const out: string[] = [];
|
|
const code = await runTasksCli(['claim-next', '--claimer-identity', 'h:r:1'], {
|
|
tools,
|
|
log: (s) => out.push(s),
|
|
});
|
|
expect(code).toBe(1);
|
|
expect(out.join('\n')).toContain('cache missing');
|
|
});
|
|
|
|
it('returns exit code 2 and usage on an unknown command', async () => {
|
|
const err: string[] = [];
|
|
const code = await runTasksCli(['frobnicate'], { tools: [], errLog: (s) => err.push(s) });
|
|
expect(code).toBe(2);
|
|
expect(err.join('\n')).toContain('unknown command: frobnicate');
|
|
});
|
|
|
|
it('a throwing handler (e.g. zod parse on a missing required flag) → JSON + non-zero, not an uncaught throw', async () => {
|
|
const tools = [
|
|
fakeTool('tasks.claim_next', () => {
|
|
throw new Error('claimer_identity: Required');
|
|
}),
|
|
];
|
|
const out: string[] = [];
|
|
const code = await runTasksCli(['claim-next'], { tools, log: (s) => out.push(s) });
|
|
expect(code).not.toBe(0);
|
|
const parsed = JSON.parse(out.join('\n'));
|
|
expect(parsed.ok).toBe(false);
|
|
expect(parsed.error).toContain('claimer_identity');
|
|
});
|
|
|
|
it('--flag=value form: a value beginning with -- survives (blocked-path where_stopped)', async () => {
|
|
let received: Record<string, unknown> | undefined;
|
|
const tools = [
|
|
fakeTool('tasks.update', (args) => {
|
|
received = args;
|
|
return jsonResult({ committed: true });
|
|
}),
|
|
];
|
|
const code = await runTasksCli(
|
|
['update', '--target-project=victor/books', '--slug=foo', '--status=blocked', '--where-stopped=--boom: bad flag', '--confirm'],
|
|
{ tools, log: () => {} },
|
|
);
|
|
expect(code).toBe(0);
|
|
expect(received).toEqual({
|
|
target_project: 'victor/books',
|
|
slug: 'foo',
|
|
status: 'blocked',
|
|
where_stopped: '--boom: bad flag',
|
|
confirm: true,
|
|
});
|
|
});
|
|
|
|
it('routes list-blocked to tasks.list_blocked with optional project_allowlist', async () => {
|
|
let received: Record<string, unknown> | undefined;
|
|
const tools = [
|
|
fakeTool('tasks.list_blocked', (args) => {
|
|
received = args;
|
|
return jsonResult({ ok: true, tasks: [] });
|
|
}),
|
|
];
|
|
const out: string[] = [];
|
|
const code = await runTasksCli(
|
|
['list-blocked', '--projects', 'OpeItcLoc03/alpha,OpeItcLoc03/beta'],
|
|
{ tools, log: (s) => out.push(s) },
|
|
);
|
|
expect(code).toBe(0);
|
|
expect(received).toEqual({ project_allowlist: ['OpeItcLoc03/alpha', 'OpeItcLoc03/beta'] });
|
|
expect(JSON.parse(out.join('\n'))).toEqual({ ok: true, tasks: [] });
|
|
});
|
|
|
|
it('routes list-blocked without --projects (no allowlist)', async () => {
|
|
let received: Record<string, unknown> | undefined;
|
|
const tools = [
|
|
fakeTool('tasks.list_blocked', (args) => {
|
|
received = args;
|
|
return jsonResult({ ok: true, tasks: [] });
|
|
}),
|
|
];
|
|
const code = await runTasksCli(['list-blocked'], { tools, log: () => {} });
|
|
expect(code).toBe(0);
|
|
expect(received).toEqual({});
|
|
});
|
|
});
|