Files
factory/lib/projects-meta-mcp/tests/tools/knowledge-get-from.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

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');
});
});