/**
* Store helper-level unit tests (pure logic, no model/runtime dependency).
*/
import { describe, test, expect } from "vitest";
import {
homedir,
resolve,
getDefaultDbPath,
_resetProductionModeForTesting,
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;
// Reset production mode in case another test file set it (bun runs all
// files in a single process, so module state leaks between files).
_resetProductionModeForTesting();
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("日本語");
});
});