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>
218 lines
6.0 KiB
TypeScript
218 lines
6.0 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 { makeKnowledgeTools } from '../../src/tools/knowledge.js';
|
|
import { writeCache, type CacheFile } from '../../src/lib/cache.js';
|
|
import type { Backend, CommitFileArgs, FileWithSha } from '../../src/lib/backend.js';
|
|
|
|
let wikiRoot: string;
|
|
let projectCwd: string;
|
|
let cacheFile: string;
|
|
|
|
beforeEach(async () => {
|
|
wikiRoot = await mkdtemp(join(tmpdir(), 'kw-shared-'));
|
|
projectCwd = await mkdtemp(join(tmpdir(), 'kw-cwd-'));
|
|
cacheFile = join(await mkdtemp(join(tmpdir(), 'kw-cache-')), 'cache.json');
|
|
});
|
|
|
|
function makeBackend(files: Record<string, Record<string, string | null | Error>>): Backend {
|
|
return {
|
|
async listUserRepos() {
|
|
return [];
|
|
},
|
|
async getRawFile(_u, repo, path) {
|
|
const v = files[repo]?.[path];
|
|
if (v === undefined) return null;
|
|
if (v instanceof Error) throw v;
|
|
return v;
|
|
},
|
|
async getFileWithSha(): Promise<FileWithSha | null> {
|
|
return null;
|
|
},
|
|
async commitFile(_a: CommitFileArgs) {
|
|
throw new Error('not used');
|
|
},
|
|
};
|
|
}
|
|
|
|
async function seedCache(projects: CacheFile['projects']) {
|
|
await writeCache(cacheFile, {
|
|
schemaVersion: 3,
|
|
synced_at: '2026-04-30T00:00:00Z',
|
|
synced_from: 'https://g',
|
|
machine: 'm',
|
|
projects,
|
|
errors: [],
|
|
});
|
|
}
|
|
|
|
function call(
|
|
tools: ReturnType<typeof makeKnowledgeTools>,
|
|
name: string,
|
|
args: Record<string, unknown>,
|
|
) {
|
|
const t = tools.find((x) => x.name === name);
|
|
if (!t) throw new Error(`tool ${name} missing`);
|
|
return t.handler(args);
|
|
}
|
|
|
|
describe('knowledge.get_from', () => {
|
|
it('returns full page body for a known qualified project + slug', async () => {
|
|
await seedCache([
|
|
{
|
|
name: 'victor/foo',
|
|
default_branch: 'main',
|
|
fetched_at: '2026-04-30T00:00:00Z',
|
|
active_tasks: [],
|
|
all_tasks_count: 0,
|
|
raw: '',
|
|
wiki_index: '',
|
|
},
|
|
]);
|
|
const body = '---\ntitle: Foo\n---\n\nfoo body content';
|
|
const backend = makeBackend({
|
|
foo: { '.wiki/concepts/foo.md': body },
|
|
});
|
|
const tools = makeKnowledgeTools({
|
|
wikiRoot,
|
|
projectCwd,
|
|
cacheFile,
|
|
backend,
|
|
giteaUser: 'u',
|
|
});
|
|
const r = await call(tools, 'knowledge.get_from', {
|
|
project: 'victor/foo',
|
|
slug: 'concepts/foo',
|
|
});
|
|
expect(r.isError).toBeFalsy();
|
|
expect(r.content[0].text).toBe(body);
|
|
});
|
|
|
|
it('rejects bare project name with hint', async () => {
|
|
await seedCache([]);
|
|
const backend = makeBackend({});
|
|
const tools = makeKnowledgeTools({
|
|
wikiRoot,
|
|
projectCwd,
|
|
cacheFile,
|
|
backend,
|
|
giteaUser: 'u',
|
|
});
|
|
const r = await call(tools, 'knowledge.get_from', {
|
|
project: 'foo',
|
|
slug: 'concepts/x',
|
|
});
|
|
expect(r.isError).toBe(true);
|
|
expect(r.content[0].text).toContain('must be qualified');
|
|
});
|
|
|
|
it('returns error when qualified project not in cache', async () => {
|
|
await seedCache([]);
|
|
const backend = makeBackend({});
|
|
const tools = makeKnowledgeTools({
|
|
wikiRoot,
|
|
projectCwd,
|
|
cacheFile,
|
|
backend,
|
|
giteaUser: 'u',
|
|
});
|
|
const r = await call(tools, 'knowledge.get_from', {
|
|
project: 'ghost/repo',
|
|
slug: 'concepts/x',
|
|
});
|
|
expect(r.isError).toBe(true);
|
|
expect(r.content[0].text).toContain('project not in cache');
|
|
});
|
|
|
|
it('returns error when page does not exist', async () => {
|
|
await seedCache([
|
|
{
|
|
name: 'victor/foo',
|
|
default_branch: 'main',
|
|
fetched_at: '2026-04-30T00:00:00Z',
|
|
active_tasks: [],
|
|
all_tasks_count: 0,
|
|
raw: '',
|
|
},
|
|
]);
|
|
const backend = makeBackend({ foo: {} });
|
|
const tools = makeKnowledgeTools({
|
|
wikiRoot,
|
|
projectCwd,
|
|
cacheFile,
|
|
backend,
|
|
giteaUser: 'u',
|
|
});
|
|
const r = await call(tools, 'knowledge.get_from', {
|
|
project: 'victor/foo',
|
|
slug: 'concepts/missing',
|
|
});
|
|
expect(r.isError).toBe(true);
|
|
expect(r.content[0].text).toContain('page not found');
|
|
});
|
|
|
|
it('resolves agenda alias to projects-wiki', async () => {
|
|
await seedCache([
|
|
{
|
|
name: 'agenda',
|
|
default_branch: 'main',
|
|
fetched_at: '2026-04-30T00:00:00Z',
|
|
active_tasks: [],
|
|
all_tasks_count: 0,
|
|
raw: '',
|
|
},
|
|
]);
|
|
const body = '---\ntitle: Agenda\n---\n\nagenda content';
|
|
// Shared wiki (projects-wiki) stores content at repo root, NO .wiki/ prefix.
|
|
// Asymmetric with project wikis where pages live under <repo>/.wiki/.
|
|
const backend = makeBackend({
|
|
'projects-wiki': { 'concepts/test.md': body },
|
|
});
|
|
const tools = makeKnowledgeTools({
|
|
wikiRoot,
|
|
projectCwd,
|
|
cacheFile,
|
|
backend,
|
|
giteaUser: 'u',
|
|
projectsWikiRepo: 'projects-wiki',
|
|
});
|
|
const r = await call(tools, 'knowledge.get_from', {
|
|
project: 'agenda',
|
|
slug: 'concepts/test',
|
|
});
|
|
expect(r.isError).toBeFalsy();
|
|
expect(r.content[0].text).toBe(body);
|
|
});
|
|
|
|
it('rejects legacy "_meta" literal with hint (renamed to "agenda" in 2.0)', async () => {
|
|
await seedCache([]);
|
|
const backend = makeBackend({});
|
|
const tools = makeKnowledgeTools({
|
|
wikiRoot,
|
|
projectCwd,
|
|
cacheFile,
|
|
backend,
|
|
giteaUser: 'u',
|
|
projectsWikiRepo: 'projects-wiki',
|
|
});
|
|
const r = await call(tools, 'knowledge.get_from', {
|
|
project: '_meta',
|
|
slug: 'concepts/test',
|
|
});
|
|
expect(r.isError).toBe(true);
|
|
expect(r.content[0].text).toContain('must be qualified');
|
|
});
|
|
|
|
it('returns error when backend not configured', async () => {
|
|
await seedCache([]);
|
|
const tools = makeKnowledgeTools({ wikiRoot, projectCwd, cacheFile });
|
|
const r = await call(tools, 'knowledge.get_from', {
|
|
project: 'victor/foo',
|
|
slug: 'concepts/x',
|
|
});
|
|
expect(r.isError).toBe(true);
|
|
expect(r.content[0].text).toContain('disabled');
|
|
});
|
|
});
|