SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
6.5 KB · 142 lines typescript
Raw Blame History
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