store.helpers.unit.test.ts 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. /**
  2. * Store helper-level unit tests (pure logic, no model/runtime dependency).
  3. */
  4. import { describe, test, expect } from "vitest";
  5. import {
  6. homedir,
  7. resolve,
  8. getDefaultDbPath,
  9. getPwd,
  10. getRealPath,
  11. isVirtualPath,
  12. parseVirtualPath,
  13. normalizeVirtualPath,
  14. normalizeDocid,
  15. isDocid,
  16. handelize,
  17. cleanupOrphanedVectors,
  18. sanitizeFTS5Term,
  19. } from "../src/store";
  20. // =============================================================================
  21. // Path Utilities
  22. // =============================================================================
  23. describe("Path Utilities", () => {
  24. test("homedir returns HOME environment variable", () => {
  25. expect(homedir()).toBe(process.env.HOME || "/tmp");
  26. });
  27. test("resolve handles absolute paths", () => {
  28. expect(resolve("/foo/bar")).toBe("/foo/bar");
  29. expect(resolve("/foo", "/bar")).toBe("/bar");
  30. });
  31. test("resolve handles relative paths", () => {
  32. const pwd = process.env.PWD || process.cwd();
  33. expect(resolve("foo")).toBe(`${pwd}/foo`);
  34. expect(resolve("foo", "bar")).toBe(`${pwd}/foo/bar`);
  35. });
  36. test("resolve normalizes . and ..", () => {
  37. expect(resolve("/foo/bar/./baz")).toBe("/foo/bar/baz");
  38. expect(resolve("/foo/bar/../baz")).toBe("/foo/baz");
  39. expect(resolve("/foo/bar/../../baz")).toBe("/baz");
  40. });
  41. test("getDefaultDbPath throws in test mode without INDEX_PATH", () => {
  42. const originalIndexPath = process.env.INDEX_PATH;
  43. delete process.env.INDEX_PATH;
  44. expect(() => getDefaultDbPath()).toThrow("Database path not set");
  45. if (originalIndexPath) {
  46. process.env.INDEX_PATH = originalIndexPath;
  47. }
  48. });
  49. test("getDefaultDbPath uses INDEX_PATH when set", () => {
  50. const originalIndexPath = process.env.INDEX_PATH;
  51. process.env.INDEX_PATH = "/tmp/test-index.sqlite";
  52. expect(getDefaultDbPath()).toBe("/tmp/test-index.sqlite");
  53. expect(getDefaultDbPath("custom")).toBe("/tmp/test-index.sqlite");
  54. if (originalIndexPath) {
  55. process.env.INDEX_PATH = originalIndexPath;
  56. } else {
  57. delete process.env.INDEX_PATH;
  58. }
  59. });
  60. test("getPwd returns current working directory", () => {
  61. const pwd = getPwd();
  62. expect(pwd).toBeTruthy();
  63. expect(typeof pwd).toBe("string");
  64. });
  65. test("getRealPath resolves symlinks", () => {
  66. const result = getRealPath("/tmp");
  67. expect(result).toBeTruthy();
  68. expect(result === "/tmp" || result === "/private/tmp").toBe(true);
  69. });
  70. });
  71. // =============================================================================
  72. // Handelize Tests
  73. // =============================================================================
  74. describe("cleanupOrphanedVectors", () => {
  75. test("returns 0 when vec table exists in schema but sqlite-vec is unavailable", () => {
  76. const prepare = (sql: string) => {
  77. if (sql.includes("sqlite_master") && sql.includes("vectors_vec")) {
  78. return { get: () => ({ name: "vectors_vec" }) };
  79. }
  80. if (sql.includes("SELECT 1 FROM vectors_vec LIMIT 0")) {
  81. return { get: () => { throw new Error("no such module: vec0"); } };
  82. }
  83. throw new Error(`Unexpected SQL in test: ${sql}`);
  84. };
  85. const db = {
  86. prepare,
  87. exec: () => {
  88. throw new Error("cleanup should not execute vector deletes when sqlite-vec is unavailable");
  89. },
  90. } as any;
  91. expect(cleanupOrphanedVectors(db)).toBe(0);
  92. });
  93. });
  94. // =============================================================================
  95. // Handelize Tests
  96. // =============================================================================
  97. describe("handelize", () => {
  98. test("converts to lowercase", () => {
  99. expect(handelize("README.md")).toBe("readme.md");
  100. expect(handelize("MyFile.MD")).toBe("myfile.md");
  101. });
  102. test("preserves folder structure", () => {
  103. expect(handelize("a/b/c/d.md")).toBe("a/b/c/d.md");
  104. expect(handelize("docs/api/README.md")).toBe("docs/api/readme.md");
  105. });
  106. test("replaces non-word characters with dash", () => {
  107. expect(handelize("hello world.md")).toBe("hello-world.md");
  108. expect(handelize("file (1).md")).toBe("file-1.md");
  109. expect(handelize("foo@bar#baz.md")).toBe("foo-bar-baz.md");
  110. });
  111. test("collapses multiple special chars into single dash", () => {
  112. expect(handelize("hello world.md")).toBe("hello-world.md");
  113. expect(handelize("foo---bar.md")).toBe("foo-bar.md");
  114. expect(handelize("a - b.md")).toBe("a-b.md");
  115. });
  116. test("removes leading and trailing dashes from segments", () => {
  117. expect(handelize("-hello-.md")).toBe("hello.md");
  118. expect(handelize("--test--.md")).toBe("test.md");
  119. expect(handelize("a/-b-/c.md")).toBe("a/b/c.md");
  120. });
  121. test("converts triple underscore to folder separator", () => {
  122. expect(handelize("foo___bar.md")).toBe("foo/bar.md");
  123. expect(handelize("notes___2025___january.md")).toBe("notes/2025/january.md");
  124. expect(handelize("a/b___c/d.md")).toBe("a/b/c/d.md");
  125. });
  126. test("handles complex real-world meeting notes", () => {
  127. const complexName = "Money Movement Licensing Review - 2025/11/19 10:25 EST - Notes by Gemini.md";
  128. const result = handelize(complexName);
  129. expect(result).toBe("money-movement-licensing-review-2025-11-19-10-25-est-notes-by-gemini.md");
  130. expect(result).not.toContain(" ");
  131. expect(result).not.toContain("/");
  132. expect(result).not.toContain(":");
  133. });
  134. test("handles unicode characters", () => {
  135. expect(handelize("日本語.md")).toBe("日本語.md");
  136. expect(handelize("Зоны и проекты.md")).toBe("зоны-и-проекты.md");
  137. expect(handelize("café-notes.md")).toBe("café-notes.md");
  138. expect(handelize("naïve.md")).toBe("naïve.md");
  139. expect(handelize("日本語-notes.md")).toBe("日本語-notes.md");
  140. });
  141. test("handles emoji filenames (issue #302)", () => {
  142. // Emoji-only filenames should convert to hex codepoints
  143. expect(handelize("🐘.md")).toBe("1f418.md");
  144. expect(handelize("🎉.md")).toBe("1f389.md");
  145. // Emoji mixed with text
  146. expect(handelize("notes 🐘.md")).toBe("notes-1f418.md");
  147. expect(handelize("🐘 elephant.md")).toBe("1f418-elephant.md");
  148. // Multiple emojis
  149. expect(handelize("🐘🎉.md")).toBe("1f418-1f389.md");
  150. // Emoji in directory names
  151. expect(handelize("🐘/notes.md")).toBe("1f418/notes.md");
  152. });
  153. test("handles dates and times in filenames", () => {
  154. expect(handelize("meeting-2025-01-15.md")).toBe("meeting-2025-01-15.md");
  155. expect(handelize("notes 2025/01/15.md")).toBe("notes-2025/01/15.md");
  156. expect(handelize("call_10:30_AM.md")).toBe("call-10-30-am.md");
  157. });
  158. test("handles special project naming patterns", () => {
  159. expect(handelize("PROJECT_ABC_v2.0.md")).toBe("project-abc-v2-0.md");
  160. expect(handelize("[WIP] Feature Request.md")).toBe("wip-feature-request.md");
  161. expect(handelize("(DRAFT) Proposal v1.md")).toBe("draft-proposal-v1.md");
  162. });
  163. test("handles symbol-only route filenames", () => {
  164. expect(handelize("routes/api/auth/$.ts")).toBe("routes/api/auth/$.ts");
  165. expect(handelize("app/routes/$id.tsx")).toBe("app/routes/$id.tsx");
  166. });
  167. test("filters out empty segments", () => {
  168. expect(handelize("a//b/c.md")).toBe("a/b/c.md");
  169. expect(handelize("/a/b/")).toBe("a/b");
  170. expect(handelize("///test///")).toBe("test");
  171. });
  172. test("throws error for invalid inputs", () => {
  173. expect(() => handelize("" )).toThrow("path cannot be empty");
  174. expect(() => handelize(" ")).toThrow("path cannot be empty");
  175. expect(() => handelize(".md")).toThrow("no valid filename content");
  176. expect(() => handelize("...")).toThrow("no valid filename content");
  177. expect(() => handelize("___")).toThrow("no valid filename content");
  178. });
  179. test("handles minimal valid inputs", () => {
  180. expect(handelize("a")).toBe("a");
  181. expect(handelize("1")).toBe("1");
  182. expect(handelize("a.md")).toBe("a.md");
  183. });
  184. test("normalizes virtual paths", () => {
  185. expect(normalizeVirtualPath("qmd://docs/readme.md")).toBe("qmd://docs/readme.md");
  186. expect(normalizeVirtualPath("docs/readme.md")).toBe("docs/readme.md");
  187. });
  188. test("detects virtual paths", () => {
  189. expect(isVirtualPath("qmd://docs/readme.md")).toBe(true);
  190. expect(isVirtualPath("/tmp/file.md")).toBe(false);
  191. });
  192. test("parses virtual paths", () => {
  193. expect(parseVirtualPath("qmd://docs/readme.md")).toEqual({
  194. collectionName: "docs",
  195. path: "readme.md",
  196. });
  197. });
  198. test("normalizes docids", () => {
  199. expect(normalizeDocid("123456")).toBe("123456");
  200. expect(normalizeDocid("#123456")).toBe("123456");
  201. });
  202. test("checks docid validity", () => {
  203. expect(isDocid("123456")).toBe(true);
  204. expect(isDocid("#123456")).toBe(true);
  205. expect(isDocid("bad-id")).toBe(false);
  206. expect(isDocid("12345")).toBe(false);
  207. });
  208. });
  209. // =============================================================================
  210. // sanitizeFTS5Term Tests
  211. // =============================================================================
  212. describe("sanitizeFTS5Term", () => {
  213. test("preserves underscores in snake_case identifiers", () => {
  214. expect(sanitizeFTS5Term("my_variable")).toBe("my_variable");
  215. expect(sanitizeFTS5Term("MAX_RETRIES")).toBe("max_retries");
  216. expect(sanitizeFTS5Term("__init__")).toBe("__init__");
  217. });
  218. test("preserves alphanumeric characters", () => {
  219. expect(sanitizeFTS5Term("hello123")).toBe("hello123");
  220. expect(sanitizeFTS5Term("test")).toBe("test");
  221. });
  222. test("preserves apostrophes for contractions", () => {
  223. expect(sanitizeFTS5Term("don't")).toBe("don't");
  224. expect(sanitizeFTS5Term("it's")).toBe("it's");
  225. });
  226. test("strips other punctuation", () => {
  227. expect(sanitizeFTS5Term("hello!")).toBe("hello");
  228. expect(sanitizeFTS5Term("test@value")).toBe("testvalue");
  229. expect(sanitizeFTS5Term("a.b")).toBe("ab");
  230. });
  231. test("lowercases output", () => {
  232. expect(sanitizeFTS5Term("Hello")).toBe("hello");
  233. expect(sanitizeFTS5Term("MY_VAR")).toBe("my_var");
  234. });
  235. test("handles unicode letters and numbers", () => {
  236. expect(sanitizeFTS5Term("café")).toBe("café");
  237. expect(sanitizeFTS5Term("日本語")).toBe("日本語");
  238. });
  239. });