Files
factory/lib/projects-meta-mcp/tests/tools/tasks-cli.test.ts
vitya ca669d96e1 feat(lib): bundle MCP servers from .common/lib into factory/lib/
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>
2026-06-11 13:17:29 +03:00

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({});
});
});