Files
llmwiki-cli/test/init.test.ts
doum1004 6a8b9a5612 Phase 1: Bootstrap + Init + Registry
Implement llmwiki-cli v0.1.0 — a CLI tool for LLM agents to build and
maintain personal knowledge bases via pure filesystem + git operations.

Phase 1 includes:
- wiki init: create wiki with directory structure, SCHEMA.md, git init
- wiki registry: list all registered wikis
- wiki use: switch active wiki
- Global registry at ~/.config/llmwiki/
- Wiki resolution chain (--wiki flag > cwd > walk up > default)
- 23 tests passing
- Published to npm as llmwiki-cli
2026-04-10 01:36:17 -04:00

352 lines
11 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { mkdtemp, rm, readdir, stat } from "fs/promises";
import { join } from "path";
import { tmpdir } from "os";
import * as yaml from "js-yaml";
import { loadConfig, saveConfig } from "../src/lib/config.ts";
import {
loadRegistry,
addToRegistry,
removeFromRegistry,
setDefault,
} from "../src/lib/registry.ts";
import { resolveWiki } from "../src/lib/resolver.ts";
import {
getDefaultConfig,
getDefaultSchema,
getDefaultIndex,
getDefaultLog,
} from "../src/lib/templates.ts";
import type { WikiConfig, RegistryEntry } from "../src/types.ts";
let testDir: string;
let configDir: string;
const origConfigDir = process.env.LLMWIKI_CONFIG_DIR;
beforeEach(async () => {
testDir = await mkdtemp(join(tmpdir(), "llmwiki-test-"));
configDir = await mkdtemp(join(tmpdir(), "llmwiki-config-"));
process.env.LLMWIKI_CONFIG_DIR = configDir;
});
afterEach(async () => {
await rm(testDir, { recursive: true, force: true });
await rm(configDir, { recursive: true, force: true });
if (origConfigDir) {
process.env.LLMWIKI_CONFIG_DIR = origConfigDir;
} else {
delete process.env.LLMWIKI_CONFIG_DIR;
}
});
// --- config ---
describe("config", () => {
it("loadConfig returns null for missing file", async () => {
const result = await loadConfig(testDir);
expect(result).toBeNull();
});
it("saveConfig writes valid YAML and loadConfig reads it back", async () => {
const config: WikiConfig = {
name: "test",
domain: "testing",
created: "2026-01-01T00:00:00.000Z",
paths: { raw: "raw", wiki: "wiki", schema: "SCHEMA.md" },
};
await saveConfig(testDir, config);
const loaded = await loadConfig(testDir);
expect(loaded).toEqual(config);
});
it("config file is valid YAML on disk", async () => {
const config = getDefaultConfig("mytest", "general");
await saveConfig(testDir, config);
const content = await Bun.file(join(testDir, ".llmwiki.yaml")).text();
const parsed = yaml.load(content) as WikiConfig;
expect(parsed.name).toBe("mytest");
expect(parsed.domain).toBe("general");
});
});
// --- registry ---
describe("registry", () => {
it("loadRegistry returns empty registry when file missing", async () => {
const registry = await loadRegistry();
expect(Object.keys(registry.wikis)).toHaveLength(0);
expect(registry.default).toBeNull();
});
it("addToRegistry adds a new entry", async () => {
const entry: RegistryEntry = {
path: "/tmp/wiki1",
name: "wiki1",
domain: "general",
created: "2026-01-01T00:00:00.000Z",
};
await addToRegistry("wiki1", entry);
const registry = await loadRegistry();
expect(registry.wikis["wiki1"]).toEqual(entry);
});
it("addToRegistry sets first wiki as default", async () => {
const entry: RegistryEntry = {
path: "/tmp/wiki1",
name: "wiki1",
domain: "general",
created: "2026-01-01T00:00:00.000Z",
};
await addToRegistry("wiki1", entry);
const registry = await loadRegistry();
expect(registry.default).toBe("wiki1");
});
it("addToRegistry updates existing entry with same id", async () => {
const entry1: RegistryEntry = {
path: "/tmp/wiki1",
name: "wiki1",
domain: "general",
created: "2026-01-01T00:00:00.000Z",
};
await addToRegistry("wiki1", entry1);
const entry2: RegistryEntry = {
...entry1,
path: "/tmp/wiki1-moved",
};
await addToRegistry("wiki1", entry2);
const registry = await loadRegistry();
expect(Object.keys(registry.wikis)).toHaveLength(1);
expect(registry.wikis["wiki1"]?.path).toBe("/tmp/wiki1-moved");
});
it("removeFromRegistry removes entry", async () => {
const entry: RegistryEntry = {
path: "/tmp/wiki1",
name: "wiki1",
domain: "general",
created: "2026-01-01T00:00:00.000Z",
};
await addToRegistry("wiki1", entry);
const removed = await removeFromRegistry("wiki1");
expect(removed).toBe(true);
const registry = await loadRegistry();
expect(Object.keys(registry.wikis)).toHaveLength(0);
});
it("removeFromRegistry returns false for unknown id", async () => {
const removed = await removeFromRegistry("nonexistent");
expect(removed).toBe(false);
});
it("removeFromRegistry updates default when removing default wiki", async () => {
const entry1: RegistryEntry = {
path: "/tmp/wiki1",
name: "wiki1",
domain: "general",
created: "2026-01-01T00:00:00.000Z",
};
const entry2: RegistryEntry = {
path: "/tmp/wiki2",
name: "wiki2",
domain: "science",
created: "2026-01-02T00:00:00.000Z",
};
await addToRegistry("wiki1", entry1);
await addToRegistry("wiki2", entry2);
await removeFromRegistry("wiki1");
const registry = await loadRegistry();
expect(registry.default).toBe("wiki2");
});
it("setDefault changes the default wiki", async () => {
const entry1: RegistryEntry = {
path: "/tmp/wiki1",
name: "wiki1",
domain: "general",
created: "2026-01-01T00:00:00.000Z",
};
const entry2: RegistryEntry = {
path: "/tmp/wiki2",
name: "wiki2",
domain: "science",
created: "2026-01-02T00:00:00.000Z",
};
await addToRegistry("wiki1", entry1);
await addToRegistry("wiki2", entry2);
await setDefault("wiki2");
const registry = await loadRegistry();
expect(registry.default).toBe("wiki2");
});
it("setDefault returns false for unknown id", async () => {
const result = await setDefault("nonexistent");
expect(result).toBe(false);
});
});
// --- resolver ---
describe("resolver", () => {
it("resolveWiki returns null when no wiki exists", async () => {
const result = await resolveWiki({});
expect(result).toBeNull();
});
it("resolveWiki finds wiki by --wiki flag", async () => {
const entry: RegistryEntry = {
path: testDir,
name: "flagtest",
domain: "general",
created: "2026-01-01T00:00:00.000Z",
};
const config = getDefaultConfig("flagtest", "general");
await saveConfig(testDir, config);
await addToRegistry("flagtest", entry);
const result = await resolveWiki({ wiki: "flagtest" });
expect(result).not.toBeNull();
expect(result!.id).toBe("flagtest");
expect(result!.root).toBe(testDir);
});
it("resolveWiki falls back to registry default", async () => {
const wikiDir = await mkdtemp(join(tmpdir(), "llmwiki-resolver-"));
try {
const config = getDefaultConfig("defaultwiki", "general");
await saveConfig(wikiDir, config);
const entry: RegistryEntry = {
path: wikiDir,
name: "defaultwiki",
domain: "general",
created: config.created,
};
await addToRegistry("defaultwiki", entry);
// Resolve from a directory that is NOT the wiki
const origCwd = process.cwd();
process.chdir(testDir);
try {
const result = await resolveWiki({});
expect(result).not.toBeNull();
expect(result!.id).toBe("defaultwiki");
} finally {
process.chdir(origCwd);
}
} finally {
await rm(wikiDir, { recursive: true, force: true });
}
});
it("--wiki flag returns null for unknown id", async () => {
const result = await resolveWiki({ wiki: "nonexistent" });
expect(result).toBeNull();
});
});
// --- templates ---
describe("templates", () => {
it("getDefaultConfig returns valid WikiConfig", () => {
const config = getDefaultConfig("test", "science");
expect(config.name).toBe("test");
expect(config.domain).toBe("science");
expect(config.created).toBeTruthy();
expect(config.paths.raw).toBe("raw");
expect(config.paths.wiki).toBe("wiki");
expect(config.paths.schema).toBe("SCHEMA.md");
});
it("getDefaultSchema includes wiki name and domain", () => {
const schema = getDefaultSchema("mywiki", "science");
expect(schema).toContain("mywiki");
expect(schema).toContain("science");
expect(schema).toContain("wiki init");
expect(schema).toContain("wiki search");
});
it("getDefaultIndex has section headers", () => {
const index = getDefaultIndex();
expect(index).toContain("## Sources");
expect(index).toContain("## Entities");
expect(index).toContain("## Concepts");
expect(index).toContain("## Synthesis");
});
it("getDefaultLog contains init entry", () => {
const log = getDefaultLog();
expect(log).toContain("init | Wiki initialized");
});
});
// --- init integration ---
describe("init command (integration)", () => {
it("creates all directories and files", async () => {
const wikiDir = join(testDir, "mywiki");
const proc = Bun.spawn(
["bun", "run", "bin/wiki.ts", "init", wikiDir, "--name", "mywiki", "--domain", "test"],
{ cwd: process.cwd(), stdout: "pipe", stderr: "pipe" },
);
await proc.exited;
// Check directories exist
const wikoDirStat = await stat(join(wikiDir, "wiki"));
expect(wikoDirStat.isDirectory()).toBe(true);
const rawDirStat = await stat(join(wikiDir, "raw"));
expect(rawDirStat.isDirectory()).toBe(true);
const assetsDirStat = await stat(join(wikiDir, "raw/assets"));
expect(assetsDirStat.isDirectory()).toBe(true);
for (const sub of ["entities", "concepts", "sources", "synthesis"]) {
const s = await stat(join(wikiDir, "wiki", sub));
expect(s.isDirectory()).toBe(true);
}
// Check files exist
expect(await Bun.file(join(wikiDir, ".llmwiki.yaml")).exists()).toBe(true);
expect(await Bun.file(join(wikiDir, "SCHEMA.md")).exists()).toBe(true);
expect(await Bun.file(join(wikiDir, "wiki/index.md")).exists()).toBe(true);
expect(await Bun.file(join(wikiDir, "wiki/log.md")).exists()).toBe(true);
// Check git
const gitStat = await stat(join(wikiDir, ".git"));
expect(gitStat.isDirectory()).toBe(true);
});
it("registers wiki in global registry", async () => {
const wikiDir = join(testDir, "regwiki");
const proc = Bun.spawn(
["bun", "run", "bin/wiki.ts", "init", wikiDir, "--name", "regwiki"],
{
cwd: process.cwd(),
stdout: "pipe",
stderr: "pipe",
env: { ...process.env, LLMWIKI_CONFIG_DIR: configDir },
},
);
await proc.exited;
const registry = await loadRegistry();
expect(registry.wikis["regwiki"]).toBeDefined();
expect(registry.default).toBe("regwiki");
});
it("uses directory basename as default name", async () => {
const wikiDir = join(testDir, "auto-named");
const proc = Bun.spawn(
["bun", "run", "bin/wiki.ts", "init", wikiDir],
{
cwd: process.cwd(),
stdout: "pipe",
stderr: "pipe",
env: { ...process.env, LLMWIKI_CONFIG_DIR: configDir },
},
);
await proc.exited;
const config = await loadConfig(wikiDir);
expect(config?.name).toBe("auto-named");
});
});