import { readFileSync } from "node:fs"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; import type { DocPage } from "../src/lib/docs"; import { parseFrontmatter } from "../src/lib/frontmatter"; import { DOC_SLUGS } from "../src/lib/nav"; import { buildSearchRecords, createSearchSnippet, findSearchMatchRanges, markdownToSearchText, normalizeSearchText, searchRecords, } from "../src/lib/search"; import { getSearchShortcutLabel } from "../src/lib/shortcut"; const docs: DocPage[] = [ { slug: "introduction", lang: "zh", title: "产品介绍", description: "", body: [ "PenguinHarness 是一个开源的 AI Agent Harness。", "", "## 产品组成", "", "Web App 提供多 Session 对话和 Trace 观测。", "", "### 模型网关", "", "统一接入在线与本地模型。", ].join("\n"), }, { slug: "server-api", lang: "en", title: "Server API", description: "", body: ["## Predict", "", "Call `client.predict` to stream an agent response."].join("\n"), }, { slug: "configuration", lang: "en", title: "Configuration", description: "Environment variables and configuration files.", body: ["## Vault", "", "Store secrets outside the project."].join("\n"), }, ]; const contentDir = join(__dirname, "..", "content"); const contentFiles = ["zh", "en"].flatMap((lang) => DOC_SLUGS.map((slug) => `${slug}.${lang}.md`)); describe("docs search", () => { it("normalizes width, case, and whitespace", () => { expect(normalizeSearchText(" PENGUIN Harness ")).toBe("penguin harness"); }); it("maps normalized matches back to their original display text", () => { const text = "使用 Client.Predict 调用本地模型"; const ranges = findSearchMatchRanges(text, "client.predict 模型"); expect(ranges.map(({ start, end }) => text.slice(start, end))).toEqual([ "Client.Predict", "模型", ]); }); it("uses locale-independent case folding for English documentation", () => { expect(normalizeSearchText("INTERFACES INPUT")).toBe("interfaces input"); }); it("maps composed queries to decomposed source graphemes", () => { const text = "Cafe\u0301 configuration"; const ranges = findSearchMatchRanges(text, "café"); expect(ranges.map(({ start, end }) => text.slice(start, end))).toEqual(["Cafe\u0301"]); }); it("merges overlapping highlight terms", () => { expect(findSearchMatchRanges("PenguinHarness", "penguin penguinharness")).toEqual([ { start: 0, end: 14 }, ]); }); it("keeps link labels and inline code while removing Markdown syntax", () => { expect(markdownToSearchText("Use [`client.predict`](/api) with **streaming**.")).toBe( "Use client.predict with streaming.", ); }); it("preserves identifiers and angle-bracket placeholders inside code", () => { const markdown = [ "Use `event_msg`, `PENGUIN_HOME`, and `agent_state/skills//`.", "", "```ts", "listTools(): Promise;", 'const type = "partial_tool_call_output";', "```", ].join("\n"); expect(markdownToSearchText(markdown)).toBe( 'Use event_msg, PENGUIN_HOME, and agent_state/skills//. listTools(): Promise; const type = "partial_tool_call_output";', ); }); it("removes paired formatting without changing literal identifier punctuation", () => { expect( markdownToSearchText( "**Bold** __strong__ _italic_ ~~old~~ context_engine foo__bar__baz partial_* model.* ~/.penguin ", ), ).toBe( "Bold strong italic old context_engine foo__bar__baz partial_* model.* ~/.penguin https://example.com", ); }); it("flattens Markdown tables without changing literal pipes in prose", () => { expect( markdownToSearchText( ["Keep A|B searchable.", "", "| Name | Value |", "| --- | --- |", "| alpha | beta |"].join( "\n", ), ), ).toBe("Keep A|B searchable. Name Value alpha beta"); }); it("finds documented identifiers by their exact spelling", () => { const codeDocs: DocPage[] = [ { slug: "interfaces", lang: "en", title: "Interfaces", description: "", body: [ "## Environment", "", "Returns `Promise` and emits `partial_tool_call_output`.", ].join("\n"), }, ]; const records = buildSearchRecords(codeDocs); expect(searchRecords(records, "Promise")).toHaveLength(1); expect(searchRecords(records, "partial_tool_call_output")).toHaveLength(1); }); it("finds identifier headings from the real OmniMessage documentation", () => { for (const lang of ["en", "zh"] as const) { const { meta, body } = parseFrontmatter( readFileSync(join(contentDir, `omni-message.${lang}.md`), "utf8"), ); const records = buildSearchRecords([ { slug: "omni-message", lang, title: meta.title ?? "OmniMessage", description: meta.description ?? "", body, }, ]); for (const heading of ["session_meta", "model_msg", "event_msg", "stop_reason"]) { expect( searchRecords(records, heading, 50).some((result) => result.record.heading.startsWith(heading), ), `${heading} should resolve to its real ${lang} heading`, ).toBe(true); } } }); it("preserves visible underscore identifiers across every documentation file", () => { for (const file of contentFiles) { const { body } = parseFrontmatter(readFileSync(join(contentDir, file), "utf8")); const visibleMarkdown = body .replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1") .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1"); const identifiers = new Set( visibleMarkdown.match(/[\p{L}\p{N}]+(?:_[\p{L}\p{N}*]+)+/gu) ?? [], ); const searchText = markdownToSearchText(body); for (const identifier of identifiers) { expect(searchText, `${file} should preserve ${identifier}`).toContain(identifier); } } }); it("maps normalized snippet offsets back to grapheme-safe source boundaries", () => { const text = `${"e\u0301".repeat(100)} TARGET value`; const snippet = createSearchSnippet(text, "target"); const visibleSnippet = snippet.replace(/^…/, ""); expect(snippet).toContain("TARGET"); expect(visibleSnippet).not.toMatch(/^\u0301/u); expect(findSearchMatchRanges(snippet, "target")).not.toHaveLength(0); }); it("builds page and heading records with deep-link anchors", () => { const records = buildSearchRecords(docs); expect(records).toEqual( expect.arrayContaining([ expect.objectContaining({ slug: "introduction", anchor: "", pageTitle: "产品介绍", }), expect.objectContaining({ slug: "introduction", anchor: "产品组成", heading: "产品组成", }), expect.objectContaining({ slug: "introduction", anchor: "模型网关", heading: "模型网关", }), ]), ); }); it("finds Chinese section content and ranks the matching section first", () => { const results = searchRecords(buildSearchRecords(docs), "本地模型"); expect(results[0]?.record).toMatchObject({ slug: "introduction", anchor: "模型网关", }); }); it("finds punctuation-bearing API names inside inline code", () => { const results = searchRecords(buildSearchRecords(docs), "client.predict"); expect(results[0]?.record).toMatchObject({ slug: "server-api", anchor: "predict", }); }); it("returns only the page record for a page-title-only query", () => { const results = searchRecords(buildSearchRecords(docs), "产品介绍"); expect(results).toHaveLength(1); expect(results[0]?.record.anchor).toBe(""); }); it("keeps a searchable page result when a document starts with a heading", () => { const results = searchRecords(buildSearchRecords(docs), "Configuration"); expect(results).toHaveLength(1); expect(results[0]).toMatchObject({ record: { slug: "configuration", anchor: "", }, }); }); it("returns no results for a blank or unmatched query", () => { const records = buildSearchRecords(docs); expect(searchRecords(records, " ")).toEqual([]); expect(searchRecords(records, "does-not-exist")).toEqual([]); }); }); describe("docs search shortcut", () => { it("uses the Command key label on Apple platforms", () => { expect(getSearchShortcutLabel("MacIntel")).toBe("⌘ K"); expect(getSearchShortcutLabel("iPad")).toBe("⌘ K"); }); it("uses the Control key label on non-Apple platforms", () => { expect(getSearchShortcutLabel("Win32")).toBe("Ctrl K"); expect(getSearchShortcutLabel("Linux x86_64")).toBe("Ctrl K"); }); });