TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import { describe, it, expect } from "vitest";2import { parseQuery, tokenize, addFilter, removeFilter, stripFilters, highlightSegments, makeSnippet, stripMarkdown, prefixTsQuery, parseDateToken, escapeLike } from "@/lib/search/query";34const NOW = new Date("2026-09-11T15:30:00Z");56describe("tokenize", () => {7 it("splits on whitespace and keeps quoted phrases together", () => {8 expect(tokenize('hello world "new york" model:"gpt 5"')).toEqual(["hello", "world", '"new york"', 'model:"gpt 5"']);9 });10 it("tolerates an unterminated quote", () => {11 expect(tokenize('foo "bar baz')).toEqual(["foo", '"bar baz']);12 });13});1415describe("parseQuery", () => {16 it("returns free text only when there are no filters", () => {17 const q = parseQuery("rate limits gemini", { now: NOW });18 expect(q.text).toBe("rate limits gemini");19 expect(q.words).toEqual(["rate", "limits", "gemini"]);20 expect(q.hasFilters).toBe(false);21 expect(q.tokens).toEqual([]);22 });2324 it("extracts every filter kind", () => {25 const q = parseQuery('budget model:claude provider:Anthropic project:research folder:Clients after:2026-08-01 before:2026-09-01 role:user is:pinned is:shared "exact phrase"', { now: NOW });26 expect(q.words).toEqual(["budget"]);27 expect(q.phrases).toEqual(["exact phrase"]);28 expect(q.text).toBe('budget "exact phrase"');29 expect(q.terms).toEqual(["exact phrase", "budget"]);30 expect(q.filters.model).toEqual(["claude"]);31 expect(q.filters.provider).toEqual(["anthropic"]);32 expect(q.filters.project).toEqual(["research"]);33 expect(q.filters.folder).toEqual(["Clients"]);34 expect(q.filters.after?.toISOString()).toBe("2026-08-01T00:00:00.000Z");35 expect(q.filters.before?.toISOString()).toBe("2026-09-02T00:00:00.000Z");36 expect(q.filters.role).toBe("user");37 expect(q.filters.is).toEqual(["pinned", "shared"]);38 expect(q.hasFilters).toBe(true);39 expect(q.tokens.map((t) => t.key)).toEqual(["model", "provider", "project", "folder", "after", "before", "role", "is", "is"]);40 });4142 it("supports quoted filter values and case-insensitive keys", () => {43 const q = parseQuery('Project:"Q3 Research" MODEL:"gpt 5"', { now: NOW });44 expect(q.filters.project).toEqual(["Q3 Research"]);45 expect(q.filters.model).toEqual(["gpt 5"]);46 expect(q.text).toBe("");47 });4849 it("keeps unknown keys and invalid filter values as free text", () => {50 const q = parseQuery("meeting at 10:30 https://example.com role:pirate after:someday", { now: NOW });51 expect(q.filters.role).toBeNull();52 expect(q.filters.after).toBeNull();53 expect(q.words).toEqual(["meeting", "at", "10:30", "https://example.com", "role:pirate", "after:someday"]);54 });5556 it("maps role aliases", () => {57 expect(parseQuery("role:ai").filters.role).toBe("assistant");58 expect(parseQuery("role:me").filters.role).toBe("user");59 });6061 it("dedupes words and lower-cases them", () => {62 const q = parseQuery("Claude claude CLAUDE");63 expect(q.words).toEqual(["claude"]);64 });6566 it("parses relative dates", () => {67 expect(parseQuery("after:7d", { now: NOW }).filters.after?.toISOString()).toBe("2026-09-04T00:00:00.000Z");68 expect(parseQuery("after:today", { now: NOW }).filters.after?.toISOString()).toBe("2026-09-11T00:00:00.000Z");69 expect(parseQuery("before:yesterday", { now: NOW }).filters.before?.toISOString()).toBe("2026-09-11T00:00:00.000Z");70 expect(parseQuery("after:2026-08", { now: NOW }).filters.after?.toISOString()).toBe("2026-08-01T00:00:00.000Z");71 expect(parseQuery("before:2026-08", { now: NOW }).filters.before?.toISOString()).toBe("2026-09-01T00:00:00.000Z");72 expect(parseDateToken("2026-13-01", NOW, "start")).toBeNull();73 });74});7576describe("filter helpers", () => {77 it("appends filters and replaces single-valued ones", () => {78 expect(addFilter("hello", "model", "claude")).toBe("hello model:claude");79 expect(addFilter("hello model:claude", "model", "claude")).toBe("hello model:claude");80 expect(addFilter("hello model:claude", "model", "gpt")).toBe("hello model:claude model:gpt");81 expect(addFilter("hello after:2026-01-01", "after", "7d")).toBe("hello after:7d");82 expect(addFilter("x", "project", "Q3 Research")).toBe('x project:"Q3 Research"');83 });8485 it("removes a token and strips all filters", () => {86 const q = parseQuery("hello model:claude is:pinned");87 expect(removeFilter(q.raw, q.tokens[0])).toBe("hello is:pinned");88 expect(stripFilters("hello model:claude is:pinned world")).toBe("hello world");89 });90});9192describe("highlighting", () => {93 it("marks case-insensitive, non-overlapping hits, longest term first", () => {94 const segs = highlightSegments("The Claude model and claude-sonnet", ["claude", "claude-sonnet"]);95 expect(segs).toEqual([96 { text: "The ", hit: false },97 { text: "Claude", hit: true },98 { text: " model and ", hit: false },99 { text: "claude-sonnet", hit: true },100 ]);101 });102 it("escapes regex metacharacters and handles empty input", () => {103 expect(highlightSegments("a+b (c)", ["a+b", "(c)"])).toEqual([104 { text: "a+b", hit: true },105 { text: " ", hit: false },106 { text: "(c)", hit: true },107 ]);108 expect(highlightSegments("", ["x"])).toEqual([]);109 expect(highlightSegments("plain", [])).toEqual([{ text: "plain", hit: false }]);110 });111});112113describe("snippets", () => {114 it("strips markdown", () => {115 expect(stripMarkdown("# Title\n\n- **bold** and _em_ with `code` and [link](https://x.y)\n```ts\nlet a = 1\n```")).toBe("Title bold and em with code and link let a = 1");116 });117 it("returns short text untouched and centres long text on the first hit", () => {118 expect(makeSnippet("short text", ["text"])).toBe("short text");119 const long = `${"lorem ipsum ".repeat(30)}NEEDLE here ${"dolor sit ".repeat(30)}`;120 const s = makeSnippet(long, ["needle"], 120);121 expect(s.startsWith("…")).toBe(true);122 expect(s.endsWith("…")).toBe(true);123 expect(s.toLowerCase()).toContain("needle");124 expect(s.length).toBeLessThanOrEqual(160);125 });126 it("falls back to the beginning when nothing matches", () => {127 const s = makeSnippet("x".repeat(500), ["zzz"], 100);128 expect(s).toBe("x".repeat(100) + "…");129 });130});131132describe("sql helpers", () => {133 it("builds a prefix tsquery with quoted lexemes", () => {134 expect(prefixTsQuery(["rate", "lim"])).toBe("'rate':* & 'lim':*");135 expect(prefixTsQuery(["a", "it's"])).toBe("'its':*");136 expect(prefixTsQuery(["a"])).toBeNull();137 });138 it("escapes LIKE wildcards", () => {139 expect(escapeLike("100%_done\\")).toBe("100\\%\\_done\\\\");140 });141});142