Files
llmwiki-cli/test/index-manager.test.ts
doum1004 428e3e516e Refactor wiki structure and improve command functionality
- Renamed sections in the wiki index from Entities/Sources/Concepts to a more structured format.
- Removed the log.md file and its associated tests to streamline the logging process.
- Updated the ai-agent-patterns.md to include JSON write examples and demo conventions.
- Modified commands tests to handle JSON input for writing and reading pages.
- Implemented delete functionality for pages and ensured proper index updates.
- Enhanced index management to support upserting entries and handling duplicates.
- Removed deprecated profile tests and log manager tests to clean up the codebase.
- Adjusted storage tests to reflect changes in file writing locations.
2026-04-30 23:31:06 -04:00

181 lines
6.7 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { mkdtemp, rm, writeFile } from "fs/promises";
import { join } from "path";
import { tmpdir } from "os";
import { IndexManager } from "../src/lib/index-manager.ts";
import { WikiManager } from "../src/lib/wiki.ts";
let testDir: string;
let indexPath: string;
let wiki: WikiManager;
let mgr: IndexManager;
const INITIAL_INDEX = `# Index
## Sources
## Entities
## Concepts
## Synthesis
`;
beforeEach(async () => {
testDir = await mkdtemp(join(tmpdir(), "llmwiki-idx-"));
indexPath = join(testDir, "index.md");
await writeFile(indexPath, INITIAL_INDEX, "utf-8");
wiki = new WikiManager(testDir);
mgr = new IndexManager(wiki, "index.md");
});
afterEach(async () => {
await rm(testDir, { recursive: true, force: true });
});
describe("IndexManager", () => {
it("read returns index content", async () => {
const content = await mgr.read();
expect(content).toContain("## Sources");
});
it("addEntry inserts under correct section from path", async () => {
await mgr.addEntry("sources/paper.md", "A paper summary");
const content = await mgr.read();
expect(content).toContain("- [[sources/paper.md]] — A paper summary");
// Should be under Sources section
const sourcesIdx = content.indexOf("## Sources");
const entryIdx = content.indexOf("[[sources/paper.md]]");
const entitiesIdx = content.indexOf("## Entities");
expect(entryIdx).toBeGreaterThan(sourcesIdx);
expect(entryIdx).toBeLessThan(entitiesIdx);
});
it("addEntry puts entities under Entities section", async () => {
await mgr.addEntry("entities/google.md", "Google LLC");
const content = await mgr.read();
const entitiesIdx = content.indexOf("## Entities");
const entryIdx = content.indexOf("[[entities/google.md]]");
const conceptsIdx = content.indexOf("## Concepts");
expect(entryIdx).toBeGreaterThan(entitiesIdx);
expect(entryIdx).toBeLessThan(conceptsIdx);
});
it("addEntry defaults to Concepts for unknown paths", async () => {
await mgr.addEntry("misc/note.md", "A random note");
const content = await mgr.read();
const conceptsIdx = content.indexOf("## Concepts");
const entryIdx = content.indexOf("[[misc/note.md]]");
expect(entryIdx).toBeGreaterThan(conceptsIdx);
});
it("multiple entries in same section", async () => {
await mgr.addEntry("concepts/attention.md", "Attention mechanism");
await mgr.addEntry("concepts/transformers.md", "Transformer architecture");
const content = await mgr.read();
expect(content).toContain("[[concepts/attention.md]]");
expect(content).toContain("[[concepts/transformers.md]]");
});
it("removeEntry removes the correct line", async () => {
await mgr.addEntry("concepts/foo.md", "Foo concept");
await mgr.addEntry("concepts/bar.md", "Bar concept");
const removed = await mgr.removeEntry("concepts/foo.md");
expect(removed).toBe(true);
const content = await mgr.read();
expect(content).not.toContain("[[concepts/foo.md]]");
expect(content).toContain("[[concepts/bar.md]]");
});
it("removeEntry returns false for missing entry", async () => {
const removed = await mgr.removeEntry("nonexistent.md");
expect(removed).toBe(false);
});
it("hasEntry returns true for existing entry", async () => {
await mgr.addEntry("sources/test.md", "Test");
expect(await mgr.hasEntry("sources/test.md")).toBe(true);
});
it("hasEntry returns false for missing entry", async () => {
expect(await mgr.hasEntry("nope.md")).toBe(false);
});
it("creates section if it does not exist", async () => {
// Start with an empty file
await writeFile(indexPath, "# Index\n", "utf-8");
mgr = new IndexManager(wiki, "index.md");
await mgr.addEntry("sources/new.md", "New source");
const content = await mgr.read();
expect(content).toContain("## Sources");
expect(content).toContain("[[sources/new.md]]");
});
it("addEntry routes synthesis paths correctly", async () => {
await mgr.addEntry("synthesis/overview.md", "Overview doc");
const content = await mgr.read();
const synthesisIdx = content.indexOf("## Synthesis");
const entryIdx = content.indexOf("[[synthesis/overview.md]]");
expect(entryIdx).toBeGreaterThan(synthesisIdx);
});
it("addEntry handles duplicate paths", async () => {
await mgr.addEntry("concepts/dup.md", "First add");
await mgr.addEntry("concepts/dup.md", "Second add");
const content = await mgr.read();
const matches = content.match(/\[\[concepts\/dup\.md\]\]/g);
expect(matches).toHaveLength(2);
});
it("upsertEntry replaces existing line for same path", async () => {
await mgr.addEntry("concepts/dup.md", "Old summary");
await mgr.upsertEntry("concepts/dup.md", "New summary");
const content = await mgr.read();
expect(content).toContain("— New summary");
expect(content).not.toContain("— Old summary");
const matches = content.match(/\[\[concepts\/dup\.md\]\]/g);
expect(matches).toHaveLength(1);
});
it("upsertEntry inserts when path absent", async () => {
await mgr.upsertEntry("concepts/new-up.md", "Fresh");
const content = await mgr.read();
expect(content).toContain("[[concepts/new-up.md]]");
expect(content).toContain("— Fresh");
});
it("read returns empty string for missing file", async () => {
const missingMgr = new IndexManager(wiki, "nonexistent.md");
const content = await missingMgr.read();
expect(content).toBe("");
});
it("hasEntry is false after removeEntry", async () => {
await mgr.addEntry("concepts/temp.md", "Temporary");
expect(await mgr.hasEntry("concepts/temp.md")).toBe(true);
await mgr.removeEntry("concepts/temp.md");
expect(await mgr.hasEntry("concepts/temp.md")).toBe(false);
});
it("removeEntry preserves other entries in same section", async () => {
await mgr.addEntry("concepts/keep.md", "Keep this");
await mgr.addEntry("concepts/remove.md", "Remove this");
await mgr.addEntry("concepts/also-keep.md", "Also keep");
await mgr.removeEntry("concepts/remove.md");
const content = await mgr.read();
expect(content).toContain("[[concepts/keep.md]]");
expect(content).toContain("[[concepts/also-keep.md]]");
expect(content).not.toContain("[[concepts/remove.md]]");
});
it("categoryFromPath matches case-insensitively", async () => {
await mgr.addEntry("Sources/paper.md", "Paper");
const content = await mgr.read();
const sourcesIdx = content.indexOf("## Sources");
const entryIdx = content.indexOf("[[Sources/paper.md]]");
const entitiesIdx = content.indexOf("## Entities");
expect(entryIdx).toBeGreaterThan(sourcesIdx);
expect(entryIdx).toBeLessThan(entitiesIdx);
});
});