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>
133 lines
5.1 KiB
TypeScript
133 lines
5.1 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
formatPage,
|
|
pagePath,
|
|
indexPath,
|
|
logPath,
|
|
rawPath,
|
|
insertIndexEntry,
|
|
appendLogEntry,
|
|
freshIndexMd,
|
|
freshLogMd,
|
|
} from '../../src/lib/wiki-writer.js';
|
|
|
|
describe('formatPage', () => {
|
|
it('produces frontmatter+body via gray-matter', () => {
|
|
const out = formatPage({
|
|
type: 'concepts',
|
|
slug: 'tasks-routing-rule',
|
|
body: 'Read local `.tasks/STATUS.md` for current project.',
|
|
frontmatter: { tags: ['routing'], domain: 'projects-meta' },
|
|
});
|
|
expect(out.startsWith('---\n')).toBe(true);
|
|
expect(out).toContain("title: tasks-routing-rule");
|
|
expect(out).toContain('type: concept');
|
|
expect(out).toContain('domain: projects-meta');
|
|
expect(out).toContain('Read local `.tasks/STATUS.md`');
|
|
});
|
|
|
|
it('auto-injects ingested_at / ingested_by / source_project', () => {
|
|
const out = formatPage({
|
|
type: 'sources',
|
|
slug: 's',
|
|
body: 'b',
|
|
ingestedAtIso: '2026-04-29T12:00:00Z',
|
|
ingestedBy: 'vitya@laptop',
|
|
sourceProject: 'modules-db',
|
|
});
|
|
expect(out).toContain("ingested_at: '2026-04-29T12:00:00Z'");
|
|
expect(out).toContain('ingested_by: vitya@laptop');
|
|
expect(out).toContain('source_project: modules-db');
|
|
});
|
|
|
|
it('respects user-supplied title and type override', () => {
|
|
const out = formatPage({
|
|
type: 'concepts',
|
|
slug: 's',
|
|
body: 'b',
|
|
frontmatter: { title: 'Hand title', type: 'design-decision' },
|
|
});
|
|
expect(out).toContain('title: Hand title');
|
|
expect(out).toContain('type: design-decision');
|
|
});
|
|
});
|
|
|
|
describe('pagePath', () => {
|
|
it('builds .wiki/<type>/<slug>.md for project wikis (default / isAgenda=false)', () => {
|
|
expect(pagePath('concepts', 'foo')).toBe('.wiki/concepts/foo.md');
|
|
expect(pagePath('raw', 'pdf-note')).toBe('.wiki/raw/pdf-note.md');
|
|
expect(pagePath('concepts', 'foo', false)).toBe('.wiki/concepts/foo.md');
|
|
});
|
|
|
|
it('builds <type>/<slug>.md for the shared wiki (isAgenda=true, no .wiki/ prefix)', () => {
|
|
expect(pagePath('concepts', 'foo', true)).toBe('concepts/foo.md');
|
|
expect(pagePath('sources', 'bar', true)).toBe('sources/bar.md');
|
|
expect(pagePath('raw', 'pdf', true)).toBe('raw/pdf.md');
|
|
});
|
|
});
|
|
|
|
describe('indexPath / logPath / rawPath', () => {
|
|
it('project wiki layout — .wiki/ prefix', () => {
|
|
expect(indexPath()).toBe('.wiki/index.md');
|
|
expect(indexPath(false)).toBe('.wiki/index.md');
|
|
expect(logPath()).toBe('.wiki/log.md');
|
|
expect(logPath(false)).toBe('.wiki/log.md');
|
|
expect(rawPath('foo')).toBe('.wiki/raw/foo.md');
|
|
expect(rawPath('foo', false)).toBe('.wiki/raw/foo.md');
|
|
});
|
|
|
|
it('shared wiki layout — repo root, no prefix', () => {
|
|
expect(indexPath(true)).toBe('index.md');
|
|
expect(logPath(true)).toBe('log.md');
|
|
expect(rawPath('foo', true)).toBe('raw/foo.md');
|
|
});
|
|
});
|
|
|
|
describe('insertIndexEntry', () => {
|
|
it('inserts entry into existing section, removes (none yet) placeholder', () => {
|
|
const before = freshIndexMd();
|
|
const after = insertIndexEntry(before, 'concepts', 'tasks-routing-rule', 'Tasks Routing Rule');
|
|
expect(after).toContain('- [tasks-routing-rule](concepts/tasks-routing-rule.md) — Tasks Routing Rule');
|
|
// Placeholder gone for Concepts section, but other sections still empty
|
|
const conceptsSection = after.slice(after.indexOf('## Concepts'), after.indexOf('## Packages'));
|
|
expect(conceptsSection).not.toContain('(none yet)');
|
|
const packagesSection = after.slice(after.indexOf('## Packages'), after.indexOf('## Sources'));
|
|
expect(packagesSection).toContain('(none yet)');
|
|
});
|
|
|
|
it('appends to non-empty section preserving prior entries', () => {
|
|
const before = `# Wiki Index\n\n## Concepts\n\n- [first](concepts/first.md) — first\n\n## Packages\n\n<!-- (none yet) -->\n`;
|
|
const after = insertIndexEntry(before, 'concepts', 'second', 'Second');
|
|
expect(after).toContain('- [first](concepts/first.md) — first');
|
|
expect(after).toContain('- [second](concepts/second.md) — Second');
|
|
});
|
|
|
|
it('skips if entry with the same slug already present', () => {
|
|
const before = `# Wiki Index\n\n## Concepts\n\n- [foo](concepts/foo.md) — Foo\n\n## Packages\n\n`;
|
|
const after = insertIndexEntry(before, 'concepts', 'foo', 'Foo Again');
|
|
expect(after).toBe(before);
|
|
});
|
|
|
|
it('appends section if missing', () => {
|
|
const before = `# Wiki Index\n\n## Overview\n\n- [overview.md](overview.md) — overview\n`;
|
|
const after = insertIndexEntry(before, 'sources', 's1', 'Source One');
|
|
expect(after).toContain('## Sources');
|
|
expect(after).toContain('- [s1](sources/s1.md) — Source One');
|
|
});
|
|
});
|
|
|
|
describe('appendLogEntry', () => {
|
|
it('appends to existing log', () => {
|
|
const before = freshLogMd();
|
|
const after = appendLogEntry(before, '2026-04-29', 'ingest', 'concepts/x');
|
|
expect(after).toContain('## [2026-04-29] ingest | concepts/x');
|
|
expect(after).toContain('# Wiki Log'); // header preserved
|
|
});
|
|
|
|
it('initializes when log is empty', () => {
|
|
const after = appendLogEntry('', '2026-04-29', 'init', 'bootstrap');
|
|
expect(after).toMatch(/^# Wiki Log/);
|
|
expect(after).toContain('## [2026-04-29] init | bootstrap');
|
|
});
|
|
});
|