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>): 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 { 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, name: string, args: Record, ) { 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 /.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'); }); });