diff --git a/src/lib/supabase-provider.ts b/src/lib/supabase-provider.ts index 25736eb..3b1631f 100644 --- a/src/lib/supabase-provider.ts +++ b/src/lib/supabase-provider.ts @@ -6,7 +6,7 @@ export class SupabaseProvider implements StorageProvider { private client: any; private wikiId: string; - private constructor(client: any, wikiId: string) { + constructor(client: any, wikiId: string) { this.client = client; this.wikiId = wikiId; } diff --git a/test/filesystem-provider.test.ts b/test/filesystem-provider.test.ts new file mode 100644 index 0000000..c801e13 --- /dev/null +++ b/test/filesystem-provider.test.ts @@ -0,0 +1,69 @@ +import { describe, it, expect, beforeEach, afterEach } from "bun:test"; +import { mkdtemp, rm } from "fs/promises"; +import { join } from "path"; +import { tmpdir } from "os"; +import { WikiManager } from "../src/lib/wiki.ts"; +import type { StorageProvider } from "../src/types.ts"; + +let testDir: string; +let provider: StorageProvider; + +beforeEach(async () => { + testDir = await mkdtemp(join(tmpdir(), "llmwiki-fs-")); + provider = new WikiManager(testDir); +}); + +afterEach(async () => { + await rm(testDir, { recursive: true, force: true }); +}); + +describe("FilesystemProvider", () => { + it("writePage + readPage round-trips content", async () => { + await provider.writePage("test.md", "hello world"); + const content = await provider.readPage("test.md"); + expect(content).toBe("hello world"); + }); + + it("readPage returns null for missing page", async () => { + const content = await provider.readPage("nonexistent.md"); + expect(content).toBeNull(); + }); + + it("pageExists returns false for missing page", async () => { + expect(await provider.pageExists("nope.md")).toBe(false); + }); + + it("pageExists returns true after write", async () => { + await provider.writePage("exists.md", "content"); + expect(await provider.pageExists("exists.md")).toBe(true); + }); + + it("appendPage returns false for missing page", async () => { + const ok = await provider.appendPage("missing.md", "more"); + expect(ok).toBe(false); + }); + + it("appendPage appends to existing page", async () => { + await provider.writePage("page.md", "first\n"); + const ok = await provider.appendPage("page.md", "second"); + expect(ok).toBe(true); + const content = await provider.readPage("page.md"); + expect(content).toBe("first\nsecond"); + }); + + it("listPages returns written markdown files", async () => { + await provider.writePage("a.md", "a"); + await provider.writePage("sub/b.md", "b"); + const pages = await provider.listPages(); + expect(pages).toContain("a.md"); + expect(pages).toContain("sub/b.md"); + }); + + it("listPages with dir scopes to subdirectory", async () => { + await provider.writePage("root.md", "r"); + await provider.writePage("sub/child.md", "c"); + const pages = await provider.listPages("sub"); + expect(pages).toContain("sub/child.md"); + expect(pages).not.toContain("root.md"); + }); +}); diff --git a/test/git-provider.test.ts b/test/git-provider.test.ts new file mode 100644 index 0000000..4a8f6f3 --- /dev/null +++ b/test/git-provider.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, beforeEach, afterEach } from "bun:test"; +import { mkdtemp, rm } from "fs/promises"; +import { join } from "path"; +import { tmpdir } from "os"; +import { execFile } from "child_process"; +import { promisify } from "util"; +import { GitProvider } from "../src/lib/git-provider.ts"; +import * as git from "../src/lib/git.ts"; +import type { StorageProvider } from "../src/types.ts"; + +const exec = promisify(execFile); + +let gitDir: string; +let gitProvider: StorageProvider; + +beforeEach(async () => { + gitDir = await mkdtemp(join(tmpdir(), "llmwiki-git-")); + await git.init(gitDir); + // Configure git user for CI environments + await exec("git", ["config", "user.name", "Test"], { cwd: gitDir }); + await exec("git", ["config", "user.email", "test@test.com"], { cwd: gitDir }); + gitProvider = new GitProvider(gitDir); +}); + +afterEach(async () => { + await rm(gitDir, { recursive: true, force: true }); +}); + +describe("GitProvider", () => { + it("writePage stores content and auto-commits", async () => { + await gitProvider.writePage("test.md", "hello"); + const content = await gitProvider.readPage("test.md"); + expect(content).toBe("hello"); + const log = await git.log(gitDir, 1); + expect(log.ok).toBe(true); + expect(log.output).toContain("update test.md"); + }); + + it("appendPage auto-commits on success", async () => { + await gitProvider.writePage("page.md", "first\n"); + await gitProvider.appendPage("page.md", "second"); + const log = await git.log(gitDir, 2); + expect(log.ok).toBe(true); + expect(log.output).toContain("append to page.md"); + }); + + it("appendPage does not commit on missing page", async () => { + const ok = await gitProvider.appendPage("missing.md", "nope"); + expect(ok).toBe(false); + const log = await git.log(gitDir, 1); + expect(log.output).not.toContain("append to missing.md"); + }); + + it("readPage returns null for missing page", async () => { + const content = await gitProvider.readPage("nope.md"); + expect(content).toBeNull(); + }); + + it("listPages works like filesystem", async () => { + await gitProvider.writePage("a.md", "a"); + await gitProvider.writePage("sub/b.md", "b"); + const pages = await gitProvider.listPages(); + expect(pages).toContain("a.md"); + expect(pages).toContain("sub/b.md"); + }); +}); diff --git a/test/storage.test.ts b/test/storage.test.ts index bb18e53..db91f9b 100644 --- a/test/storage.test.ts +++ b/test/storage.test.ts @@ -4,7 +4,6 @@ import { join } from "path"; import { tmpdir } from "os"; import { createProvider } from "../src/lib/storage.ts"; import { GitProvider } from "../src/lib/git-provider.ts"; -import * as git from "../src/lib/git.ts"; import type { StorageProvider, WikiConfig } from "../src/types.ts"; function makeConfig(backend: WikiConfig["backend"] = "filesystem"): WikiConfig { @@ -18,11 +17,9 @@ function makeConfig(backend: WikiConfig["backend"] = "filesystem"): WikiConfig { } let testDir: string; -let provider: StorageProvider; beforeEach(async () => { testDir = await mkdtemp(join(tmpdir(), "llmwiki-storage-")); - provider = await createProvider(makeConfig("filesystem"), testDir); }); afterEach(async () => { @@ -30,7 +27,8 @@ afterEach(async () => { }); describe("createProvider", () => { - it("creates a filesystem provider", () => { + it("creates a filesystem provider", async () => { + const provider = await createProvider(makeConfig("filesystem"), testDir); expect(provider).toBeDefined(); expect(provider.readPage).toBeInstanceOf(Function); expect(provider.writePage).toBeInstanceOf(Function); @@ -56,106 +54,3 @@ describe("createProvider", () => { ).rejects.toThrow('Unknown storage backend: "unknown"'); }); }); - -describe("StorageProvider contract (filesystem)", () => { - it("writePage + readPage round-trips content", async () => { - await provider.writePage("test.md", "hello world"); - const content = await provider.readPage("test.md"); - expect(content).toBe("hello world"); - }); - - it("readPage returns null for missing page", async () => { - const content = await provider.readPage("nonexistent.md"); - expect(content).toBeNull(); - }); - - it("pageExists returns false for missing page", async () => { - expect(await provider.pageExists("nope.md")).toBe(false); - }); - - it("pageExists returns true after write", async () => { - await provider.writePage("exists.md", "content"); - expect(await provider.pageExists("exists.md")).toBe(true); - }); - - it("appendPage returns false for missing page", async () => { - const ok = await provider.appendPage("missing.md", "more"); - expect(ok).toBe(false); - }); - - it("appendPage appends to existing page", async () => { - await provider.writePage("page.md", "first\n"); - const ok = await provider.appendPage("page.md", "second"); - expect(ok).toBe(true); - const content = await provider.readPage("page.md"); - expect(content).toBe("first\nsecond"); - }); - - it("listPages returns written markdown files", async () => { - await provider.writePage("a.md", "a"); - await provider.writePage("sub/b.md", "b"); - const pages = await provider.listPages(); - expect(pages).toContain("a.md"); - expect(pages).toContain("sub/b.md"); - }); - - it("listPages with dir scopes to subdirectory", async () => { - await provider.writePage("root.md", "r"); - await provider.writePage("sub/child.md", "c"); - const pages = await provider.listPages("sub"); - expect(pages).toContain("sub/child.md"); - expect(pages).not.toContain("root.md"); - }); -}); - -describe("GitProvider", () => { - let gitDir: string; - let gitProvider: StorageProvider; - - beforeEach(async () => { - gitDir = await mkdtemp(join(tmpdir(), "llmwiki-git-")); - await git.init(gitDir); - gitProvider = await createProvider(makeConfig("git"), gitDir); - }); - - afterEach(async () => { - await rm(gitDir, { recursive: true, force: true }); - }); - - it("writePage stores content and auto-commits", async () => { - await gitProvider.writePage("test.md", "hello"); - const content = await gitProvider.readPage("test.md"); - expect(content).toBe("hello"); - const log = await git.log(gitDir, 1); - expect(log.ok).toBe(true); - expect(log.output).toContain("update test.md"); - }); - - it("appendPage auto-commits on success", async () => { - await gitProvider.writePage("page.md", "first\n"); - await gitProvider.appendPage("page.md", "second"); - const log = await git.log(gitDir, 2); - expect(log.ok).toBe(true); - expect(log.output).toContain("append to page.md"); - }); - - it("appendPage does not commit on missing page", async () => { - const ok = await gitProvider.appendPage("missing.md", "nope"); - expect(ok).toBe(false); - const log = await git.log(gitDir, 1); - expect(log.output).not.toContain("append to missing.md"); - }); - - it("readPage returns null for missing page", async () => { - const content = await gitProvider.readPage("nope.md"); - expect(content).toBeNull(); - }); - - it("listPages works like filesystem", async () => { - await gitProvider.writePage("a.md", "a"); - await gitProvider.writePage("sub/b.md", "b"); - const pages = await gitProvider.listPages(); - expect(pages).toContain("a.md"); - expect(pages).toContain("sub/b.md"); - }); -}); diff --git a/test/supabase-provider.test.ts b/test/supabase-provider.test.ts new file mode 100644 index 0000000..f4c0e00 --- /dev/null +++ b/test/supabase-provider.test.ts @@ -0,0 +1,126 @@ +import { describe, it, expect, beforeEach } from "bun:test"; +import { SupabaseProvider } from "../src/lib/supabase-provider.ts"; + +// In-memory store simulating Supabase table +let store: Map; + +function mockClient() { + function makeEqChain(wikiId: string, isCount: boolean) { + return { + eq(col2: string, val2: string) { + const key = `${wikiId}:${val2}`; + if (isCount) { + const exists = store.has(key); + return { count: exists ? 1 : 0, error: null }; + } + return { + maybeSingle() { + const row = store.get(key); + return { data: row ? { content: row.content } : null, error: null }; + }, + }; + }, + like(_col: string, _pattern: string) { + return { + order() { + return { data: [], error: null }; + }, + }; + }, + order(_col: string) { + const results: { path: string }[] = []; + for (const row of store.values()) { + if (row.wiki_id === wikiId) { + results.push({ path: row.path }); + } + } + return { data: results, error: null }; + }, + }; + } + + return { + from(_table: string) { + return { + select(fields: string, opts?: { count?: string; head?: boolean }) { + const isCount = opts?.count === "exact"; + return { + eq(col: string, val: string) { + return makeEqChain(val, isCount); + }, + }; + }, + upsert(row: any, _opts?: any) { + const key = `${row.wiki_id}:${row.path}`; + store.set(key, { wiki_id: row.wiki_id, path: row.path, content: row.content }); + return { error: null }; + }, + }; + }, + }; +} + +let provider: SupabaseProvider; + +beforeEach(() => { + store = new Map(); + provider = new SupabaseProvider(mockClient(), "test-wiki"); +}); + +describe("SupabaseProvider", () => { + it("writePage + readPage round-trips content", async () => { + await provider.writePage("wiki/test.md", "hello world"); + const content = await provider.readPage("wiki/test.md"); + expect(content).toBe("hello world"); + }); + + it("readPage returns null for missing page", async () => { + const content = await provider.readPage("nonexistent.md"); + expect(content).toBeNull(); + }); + + it("writePage overwrites existing content", async () => { + await provider.writePage("wiki/page.md", "v1"); + await provider.writePage("wiki/page.md", "v2"); + const content = await provider.readPage("wiki/page.md"); + expect(content).toBe("v2"); + }); + + it("appendPage appends to existing page", async () => { + await provider.writePage("wiki/page.md", "first\n"); + const ok = await provider.appendPage("wiki/page.md", "second"); + expect(ok).toBe(true); + const content = await provider.readPage("wiki/page.md"); + expect(content).toBe("first\nsecond"); + }); + + it("appendPage returns false for missing page", async () => { + const ok = await provider.appendPage("missing.md", "nope"); + expect(ok).toBe(false); + }); + + it("pageExists returns false for missing page", async () => { + expect(await provider.pageExists("nope.md")).toBe(false); + }); + + it("pageExists returns true after write", async () => { + await provider.writePage("wiki/exists.md", "content"); + expect(await provider.pageExists("wiki/exists.md")).toBe(true); + }); + + it("listPages returns stored pages", async () => { + await provider.writePage("wiki/a.md", "a"); + await provider.writePage("wiki/b.md", "b"); + const pages = await provider.listPages(); + expect(pages).toContain("wiki/a.md"); + expect(pages).toContain("wiki/b.md"); + }); + + it("listPages only returns .md files", async () => { + await provider.writePage("wiki/page.md", "content"); + await provider.writePage("wiki/image.png", "binary"); + const pages = await provider.listPages(); + expect(pages).toContain("wiki/page.md"); + expect(pages).not.toContain("wiki/image.png"); + }); +});