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>
198 lines
6.5 KiB
TypeScript
198 lines
6.5 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { mkdtemp } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { writeCache, type CacheFile } from '../../src/lib/cache.js';
|
|
import { makeTasksTools } from '../../src/tools/tasks.js';
|
|
import type { Backend, FileWithSha } from '../../src/lib/backend.js';
|
|
|
|
function recordingBackend(initial: Record<string, FileWithSha> = {}) {
|
|
const files = { ...initial };
|
|
const backend: Backend = {
|
|
async listUserRepos() { return []; },
|
|
async getRawFile(_u, repo, path) { return files[`${repo}:${path}`]?.content ?? null; },
|
|
async getFileWithSha(_u, repo, path) { return files[`${repo}:${path}`] ?? null; },
|
|
async commitFile() { return { fileSha: 'x', commitSha: 'y' }; },
|
|
};
|
|
return { backend, files };
|
|
}
|
|
|
|
const ALPHA_BOARD = `# Task Board
|
|
_Updated: 2026-06-09_
|
|
|
|
## 🔵 [blocked-cheap] — blocked cheap task
|
|
**Status:** blocked
|
|
**Where I stopped:** dep pending
|
|
**Next action:** wait
|
|
**Blocker:** dep-a, dep-b
|
|
**Branch:** master
|
|
**Weight:** cheap-ok
|
|
|
|
---
|
|
|
|
## 🔵 [blocked-claude] — blocked claude task
|
|
**Status:** blocked
|
|
**Where I stopped:** dep pending
|
|
**Next action:** wait
|
|
**Blocker:** dep-c
|
|
**Branch:** master
|
|
**Weight:** needs-claude
|
|
|
|
---
|
|
|
|
## 🔵 [blocked-human] — blocked human task (should be skipped)
|
|
**Status:** blocked
|
|
**Where I stopped:** dep pending
|
|
**Next action:** wait
|
|
**Blocker:** dep-d
|
|
**Branch:** master
|
|
**Weight:** needs-human
|
|
|
|
---
|
|
|
|
## 🔵 [blocked-noweight] — blocked no-weight task (should be skipped)
|
|
**Status:** blocked
|
|
**Where I stopped:** dep pending
|
|
**Next action:** wait
|
|
**Blocker:** dep-e
|
|
**Branch:** master
|
|
|
|
---
|
|
|
|
## ⚪ [ready-task] — not blocked, should not appear
|
|
**Status:** ready
|
|
**Next action:** go
|
|
**Branch:** master
|
|
|
|
---
|
|
`;
|
|
|
|
const BETA_BOARD = `# Task Board
|
|
_Updated: 2026-06-09_
|
|
|
|
## 🔵 [blocked-beta] — blocked beta task
|
|
**Status:** blocked
|
|
**Where I stopped:** waiting
|
|
**Next action:** wait
|
|
**Blocker:** dep-x
|
|
**Branch:** master
|
|
**Weight:** needs-claude
|
|
|
|
---
|
|
`;
|
|
|
|
const fixture: CacheFile = {
|
|
schemaVersion: 3,
|
|
synced_at: '2026-06-09T12:00:00.000Z',
|
|
synced_from: 'https://g',
|
|
machine: 'm',
|
|
projects: [
|
|
{
|
|
name: 'OpeItcLoc03/alpha',
|
|
default_branch: 'main',
|
|
fetched_at: '2026-06-09T12:00:00.000Z',
|
|
active_tasks: [
|
|
{ slug: 'blocked-cheap', status: 'blocked', next: 'wait' },
|
|
{ slug: 'blocked-claude', status: 'blocked', next: 'wait' },
|
|
{ slug: 'blocked-human', status: 'blocked', next: 'wait' },
|
|
{ slug: 'blocked-noweight', status: 'blocked', next: 'wait' },
|
|
],
|
|
all_tasks_count: 5,
|
|
raw: '',
|
|
},
|
|
{
|
|
name: 'OpeItcLoc03/beta',
|
|
default_branch: 'main',
|
|
fetched_at: '2026-06-09T12:00:00.000Z',
|
|
active_tasks: [
|
|
{ slug: 'blocked-beta', status: 'blocked', next: 'wait' },
|
|
],
|
|
all_tasks_count: 1,
|
|
raw: '',
|
|
},
|
|
],
|
|
errors: [],
|
|
};
|
|
|
|
let cacheFile: string;
|
|
beforeEach(async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), 'listblocked-'));
|
|
cacheFile = join(dir, 'tasks.json');
|
|
await writeCache(cacheFile, fixture);
|
|
});
|
|
|
|
function makeTools(backend: Backend) {
|
|
return makeTasksTools({ cacheFile, backend, giteaUser: 'u', agendaTasksRepo: 'projects-tasks' });
|
|
}
|
|
function call(tools: ReturnType<typeof makeTasksTools>, name: string, args: Record<string, unknown>) {
|
|
const t = tools.find((x) => x.name === name)!;
|
|
return t.handler(args);
|
|
}
|
|
|
|
describe('tasks.list_blocked', () => {
|
|
it('returns blocked tasks with weight needs-claude or cheap-ok, skips needs-human and no-weight', async () => {
|
|
const { backend } = recordingBackend({
|
|
'alpha:.tasks/STATUS.md': { content: ALPHA_BOARD, sha: 's' },
|
|
'beta:.tasks/STATUS.md': { content: BETA_BOARD, sha: 's' },
|
|
});
|
|
const r = await call(makeTools(backend), 'tasks.list_blocked', {});
|
|
const parsed = JSON.parse(r.content[0].text);
|
|
expect(parsed.ok).toBe(true);
|
|
const slugs = parsed.tasks.map((t: { slug: string }) => t.slug).sort();
|
|
expect(slugs).toEqual(['blocked-beta', 'blocked-cheap', 'blocked-claude']);
|
|
});
|
|
|
|
it('includes blocker and weight fields', async () => {
|
|
const { backend } = recordingBackend({
|
|
'alpha:.tasks/STATUS.md': { content: ALPHA_BOARD, sha: 's' },
|
|
'beta:.tasks/STATUS.md': { content: BETA_BOARD, sha: 's' },
|
|
});
|
|
const r = await call(makeTools(backend), 'tasks.list_blocked', {});
|
|
const parsed = JSON.parse(r.content[0].text);
|
|
const cheap = parsed.tasks.find((t: { slug: string }) => t.slug === 'blocked-cheap');
|
|
expect(cheap.blocker).toBe('dep-a, dep-b');
|
|
expect(cheap.weight).toBe('cheap-ok');
|
|
expect(cheap.project).toBe('OpeItcLoc03/alpha');
|
|
const claude = parsed.tasks.find((t: { slug: string }) => t.slug === 'blocked-claude');
|
|
expect(claude.blocker).toBe('dep-c');
|
|
expect(claude.weight).toBe('needs-claude');
|
|
});
|
|
|
|
it('respects project_allowlist — only returns tasks from listed projects', async () => {
|
|
const { backend } = recordingBackend({
|
|
'alpha:.tasks/STATUS.md': { content: ALPHA_BOARD, sha: 's' },
|
|
'beta:.tasks/STATUS.md': { content: BETA_BOARD, sha: 's' },
|
|
});
|
|
const r = await call(makeTools(backend), 'tasks.list_blocked', {
|
|
project_allowlist: ['OpeItcLoc03/beta'],
|
|
});
|
|
const parsed = JSON.parse(r.content[0].text);
|
|
expect(parsed.ok).toBe(true);
|
|
expect(parsed.tasks.every((t: { project: string }) => t.project === 'OpeItcLoc03/beta')).toBe(true);
|
|
expect(parsed.tasks.map((t: { slug: string }) => t.slug)).toEqual(['blocked-beta']);
|
|
});
|
|
|
|
it('returns empty tasks when cache is missing', async () => {
|
|
const { backend } = recordingBackend({});
|
|
const tools = makeTasksTools({ cacheFile: '/nonexistent/tasks.json', backend, giteaUser: 'u', agendaTasksRepo: 'projects-tasks' });
|
|
const t = tools.find((x) => x.name === 'tasks.list_blocked')!;
|
|
const r = await t.handler({});
|
|
const parsed = JSON.parse(r.content[0].text);
|
|
expect(parsed.tasks).toEqual([]);
|
|
expect(parsed.cache_missing).toBe(true);
|
|
});
|
|
|
|
it('skips project when STATUS.md not found in backend (graceful degradation)', async () => {
|
|
const { backend } = recordingBackend({
|
|
'beta:.tasks/STATUS.md': { content: BETA_BOARD, sha: 's' },
|
|
// alpha STATUS.md is missing
|
|
});
|
|
const r = await call(makeTools(backend), 'tasks.list_blocked', {});
|
|
const parsed = JSON.parse(r.content[0].text);
|
|
expect(parsed.ok).toBe(true);
|
|
const slugs = parsed.tasks.map((t: { slug: string }) => t.slug);
|
|
expect(slugs).toContain('blocked-beta');
|
|
expect(slugs).not.toContain('blocked-cheap');
|
|
});
|
|
});
|