/** * Store helper-level unit tests (pure logic, no model/runtime dependency). */ import { describe, test, expect } from "vitest"; import { homedir, resolve, getDefaultDbPath, getPwd, getRealPath, isVirtualPath, parseVirtualPath, normalizeVirtualPath, normalizeDocid, isDocid, handelize, cleanupOrphanedVectors, sanitizeFTS5Term, } from "../src/store"; // ============================================================================= // Path Utilities // ============================================================================= describe("Path Utilities", () => { test("homedir returns HOME environment variable", () => { expect(homedir()).toBe(process.env.HOME || "/tmp"); }); test("resolve handles absolute paths", () => { expect(resolve("/foo/bar")).toBe("/foo/bar"); expect(resolve("/foo", "/bar")).toBe("/bar"); }); test("resolve handles relative paths", () => { const pwd = process.env.PWD || process.cwd(); expect(resolve("foo")).toBe(`${pwd}/foo`); expect(resolve("foo", "bar")).toBe(`${pwd}/foo/bar`); }); test("resolve normalizes . and ..", () => { expect(resolve("/foo/bar/./baz")).toBe("/foo/bar/baz"); expect(resolve("/foo/bar/../baz")).toBe("/foo/baz"); expect(resolve("/foo/bar/../../baz")).toBe("/baz"); }); test("getDefaultDbPath throws in test mode without INDEX_PATH", () => { const originalIndexPath = process.env.INDEX_PATH; delete process.env.INDEX_PATH; expect(() => getDefaultDbPath()).toThrow("Database path not set"); if (originalIndexPath) { process.env.INDEX_PATH = originalIndexPath; } }); test("getDefaultDbPath uses INDEX_PATH when set", () => { const originalIndexPath = process.env.INDEX_PATH; process.env.INDEX_PATH = "/tmp/test-index.sqlite"; expect(getDefaultDbPath()).toBe("/tmp/test-index.sqlite"); expect(getDefaultDbPath("custom")).toBe("/tmp/test-index.sqlite"); if (originalIndexPath) { process.env.INDEX_PATH = originalIndexPath; } else { delete process.env.INDEX_PATH; } }); test("getPwd returns current working directory", () => { const pwd = getPwd(); expect(pwd).toBeTruthy(); expect(typeof pwd).toBe("string"); }); test("getRealPath resolves symlinks", () => { const result = getRealPath("/tmp"); expect(result).toBeTruthy(); expect(result === "/tmp" || result === "/private/tmp").toBe(true); }); }); // ============================================================================= // Handelize Tests // ============================================================================= describe("cleanupOrphanedVectors", () => { test("returns 0 when vec table exists in schema but sqlite-vec is unavailable", () => { const prepare = (sql: string) => { if (sql.includes("sqlite_master") && sql.includes("vectors_vec")) { return { get: () => ({ name: "vectors_vec" }) }; } if (sql.includes("SELECT 1 FROM vectors_vec LIMIT 0")) { return { get: () => { throw new Error("no such module: vec0"); } }; } throw new Error(`Unexpected SQL in test: ${sql}`); }; const db = { prepare, exec: () => { throw new Error("cleanup should not execute vector deletes when sqlite-vec is unavailable"); }, } as any; expect(cleanupOrphanedVectors(db)).toBe(0); }); }); // ============================================================================= // Handelize Tests // ============================================================================= describe("handelize", () => { test("preserves original case", () => { expect(handelize("README.md")).toBe("README.md"); expect(handelize("MyFile.MD")).toBe("MyFile.MD"); }); test("preserves folder structure", () => { expect(handelize("a/b/c/d.md")).toBe("a/b/c/d.md"); expect(handelize("docs/api/README.md")).toBe("docs/api/README.md"); }); test("replaces non-word characters with dash", () => { expect(handelize("hello world.md")).toBe("hello-world.md"); expect(handelize("file (1).md")).toBe("file-1.md"); expect(handelize("foo@bar#baz.md")).toBe("foo-bar-baz.md"); }); test("collapses multiple special chars into single dash", () => { expect(handelize("hello world.md")).toBe("hello-world.md"); expect(handelize("foo---bar.md")).toBe("foo-bar.md"); expect(handelize("a - b.md")).toBe("a-b.md"); }); test("removes leading and trailing dashes from segments", () => { expect(handelize("-hello-.md")).toBe("hello.md"); expect(handelize("--test--.md")).toBe("test.md"); expect(handelize("a/-b-/c.md")).toBe("a/b/c.md"); }); test("converts triple underscore to folder separator", () => { expect(handelize("foo___bar.md")).toBe("foo/bar.md"); expect(handelize("notes___2025___january.md")).toBe("notes/2025/january.md"); expect(handelize("a/b___c/d.md")).toBe("a/b/c/d.md"); }); test("handles complex real-world meeting notes", () => { const complexName = "Money Movement Licensing Review - 2025/11/19 10:25 EST - Notes by Gemini.md"; const result = handelize(complexName); expect(result).toBe("Money-Movement-Licensing-Review-2025-11-19-10-25-EST-Notes-by-Gemini.md"); expect(result).not.toContain(" "); expect(result).not.toContain("/"); expect(result).not.toContain(":"); }); test("handles unicode characters", () => { expect(handelize("日本語.md")).toBe("日本語.md"); expect(handelize("Зоны и проекты.md")).toBe("Зоны-и-проекты.md"); expect(handelize("café-notes.md")).toBe("café-notes.md"); expect(handelize("naïve.md")).toBe("naïve.md"); expect(handelize("日本語-notes.md")).toBe("日本語-notes.md"); }); test("handles emoji filenames (issue #302)", () => { // Emoji-only filenames should convert to hex codepoints expect(handelize("🐘.md")).toBe("1f418.md"); expect(handelize("🎉.md")).toBe("1f389.md"); // Emoji mixed with text expect(handelize("notes 🐘.md")).toBe("notes-1f418.md"); expect(handelize("🐘 elephant.md")).toBe("1f418-elephant.md"); // Multiple emojis expect(handelize("🐘🎉.md")).toBe("1f418-1f389.md"); // Emoji in directory names expect(handelize("🐘/notes.md")).toBe("1f418/notes.md"); }); test("handles dates and times in filenames", () => { expect(handelize("meeting-2025-01-15.md")).toBe("meeting-2025-01-15.md"); expect(handelize("notes 2025/01/15.md")).toBe("notes-2025/01/15.md"); expect(handelize("call_10:30_AM.md")).toBe("call-10-30-AM.md"); }); test("handles special project naming patterns", () => { expect(handelize("PROJECT_ABC_v2.0.md")).toBe("PROJECT-ABC-v2.0.md"); expect(handelize("[WIP] Feature Request.md")).toBe("WIP-Feature-Request.md"); expect(handelize("(DRAFT) Proposal v1.md")).toBe("DRAFT-Proposal-v1.md"); }); test("handles symbol-only route filenames", () => { expect(handelize("routes/api/auth/$.ts")).toBe("routes/api/auth/$.ts"); expect(handelize("app/routes/$id.tsx")).toBe("app/routes/$id.tsx"); }); test("filters out empty segments", () => { expect(handelize("a//b/c.md")).toBe("a/b/c.md"); expect(handelize("/a/b/")).toBe("a/b"); expect(handelize("///test///")).toBe("test"); }); test("throws error for invalid inputs", () => { expect(() => handelize("" )).toThrow("path cannot be empty"); expect(() => handelize(" ")).toThrow("path cannot be empty"); expect(() => handelize(".md")).toThrow("no valid filename content"); expect(() => handelize("...")).toThrow("no valid filename content"); expect(() => handelize("___")).toThrow("no valid filename content"); }); test("handles minimal valid inputs", () => { expect(handelize("a")).toBe("a"); expect(handelize("1")).toBe("1"); expect(handelize("a.md")).toBe("a.md"); }); test("normalizes virtual paths", () => { expect(normalizeVirtualPath("qmd://docs/readme.md")).toBe("qmd://docs/readme.md"); expect(normalizeVirtualPath("docs/readme.md")).toBe("docs/readme.md"); }); test("detects virtual paths", () => { expect(isVirtualPath("qmd://docs/readme.md")).toBe(true); expect(isVirtualPath("/tmp/file.md")).toBe(false); }); test("parses virtual paths", () => { expect(parseVirtualPath("qmd://docs/readme.md")).toEqual({ collectionName: "docs", path: "readme.md", }); }); test("normalizes docids", () => { expect(normalizeDocid("123456")).toBe("123456"); expect(normalizeDocid("#123456")).toBe("123456"); }); test("checks docid validity", () => { expect(isDocid("123456")).toBe(true); expect(isDocid("#123456")).toBe(true); expect(isDocid("bad-id")).toBe(false); expect(isDocid("12345")).toBe(false); }); }); // ============================================================================= // sanitizeFTS5Term Tests // ============================================================================= describe("sanitizeFTS5Term", () => { test("preserves underscores in snake_case identifiers", () => { expect(sanitizeFTS5Term("my_variable")).toBe("my_variable"); expect(sanitizeFTS5Term("MAX_RETRIES")).toBe("max_retries"); expect(sanitizeFTS5Term("__init__")).toBe("__init__"); }); test("preserves alphanumeric characters", () => { expect(sanitizeFTS5Term("hello123")).toBe("hello123"); expect(sanitizeFTS5Term("test")).toBe("test"); }); test("preserves apostrophes for contractions", () => { expect(sanitizeFTS5Term("don't")).toBe("don't"); expect(sanitizeFTS5Term("it's")).toBe("it's"); }); test("strips other punctuation", () => { expect(sanitizeFTS5Term("hello!")).toBe("hello"); expect(sanitizeFTS5Term("test@value")).toBe("testvalue"); expect(sanitizeFTS5Term("a.b")).toBe("ab"); }); test("lowercases output", () => { expect(sanitizeFTS5Term("Hello")).toBe("hello"); expect(sanitizeFTS5Term("MY_VAR")).toBe("my_var"); }); test("handles unicode letters and numbers", () => { expect(sanitizeFTS5Term("café")).toBe("café"); expect(sanitizeFTS5Term("日本語")).toBe("日本語"); }); });