Files
veridian/tests/citations.test.ts
T

85 lines
4.0 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { collectCitationSources } from "../app/utils/citations";
import { createMarkdownParser, getNodeChildren, getNodeTag } from "../app/utils/markdown";
import type { MarkdownAstNode } from "../app/utils/markdown";
import { readSearchResponse } from "../server/utils/search";
const citations = (nodes: MarkdownAstNode[]): MarkdownAstNode[] => nodes.flatMap(node =>
getNodeTag(node) === "source-cite" ? [node] : citations(getNodeChildren(node)),
);
describe("citation parsing", () => {
test("keeps citation markers as text unless explicitly enabled", async () => {
const content = "Claim [[cite:1234]] and [[cite:s7,s9]].";
for (const options of [{}, { citations: false }]) {
for (const streaming of [false, true]) {
const tree = await createMarkdownParser(options)(content, { streaming });
expect(citations(tree.nodes)).toEqual([]);
expect(getNodeChildren(tree.nodes[0] ?? "")).toEqual([content]);
}
}
});
test("renders single and grouped references as dedicated nodes", async () => {
const tree = await createMarkdownParser({ citations: true })("Claim [[cite:s7]]. Another [[cite:s7,s9]].");
expect(citations(tree.nodes)).toEqual([
["source-cite", { ids: "s7" }],
["source-cite", { ids: "s7,s9" }],
]);
});
test("does not interpret code, escaped references, or link labels", async () => {
for (const content of [
"`[[cite:s7]]`",
"```text\n[[cite:s7]]\n```",
"\\[[cite:s7]]",
"[Claim [[cite:s7]]](https://example.com)",
]) {
const tree = await createMarkdownParser({ citations: true })(content);
expect(citations(tree.nodes)).toEqual([]);
}
});
test("streaming never auto-completes a citation", async () => {
const parse = createMarkdownParser({ citations: true });
const content = "Claim [[cite:s7,s9]]";
for (let length = 1; length < content.length; length++) {
expect(citations((await parse(content.slice(0, length), { streaming: true })).nodes)).toEqual([]);
}
expect(citations((await parse(content, { streaming: true })).nodes)).toHaveLength(1);
expect(citations((await parse(content)).nodes)).toHaveLength(1);
});
});
describe("citation sources", () => {
test("IDs survive storage, including duplicate URLs and multiple searches", () => {
const first = readSearchResponse({ results: [
{ title: "One", url: "https://example.com" },
{ title: "Two", url: "https://example.com" },
] });
const second = readSearchResponse({ results: [{ title: "Three", url: "https://example.com" }] });
expect(first.ok && second.ok).toBe(true);
if (!first.ok || !second.ok) {
return;
}
const parts = [first.data, second.data].map(output => ({
toolCall: { toolName: "search", output: { type: "json", value: JSON.stringify(output) } },
}));
const sources = collectCitationSources(parts);
expect(sources.size).toBe(3);
expect(sources.get(first.data.results[0]?.sourceId ?? "")?.title).toBe("One");
expect(collectCitationSources(JSON.parse(JSON.stringify(parts)))).toEqual(sources);
expect(sources.has("invented")).toBe(false);
expect(collectCitationSources([]).size).toBe(0);
});
test("ignores legacy results, non-search tools, and unsafe URLs", () => {
const output = { type: "json", value: JSON.stringify({ results: [
{ title: "Legacy", link: "https://example.com" },
{ sourceId: "bad", title: "Bad", link: "javascript:alert(1)" },
] }) };
expect(collectCitationSources([{ toolCall: { toolName: "search", output } }]).size).toBe(0);
expect(collectCitationSources([{ toolCall: { toolName: "python", output } }]).size).toBe(0);
});
});