311 lines
10 KiB
TypeScript
311 lines
10 KiB
TypeScript
/**
|
|
* formatter.test.ts - Unit tests verifying context is shown in all output formats
|
|
*
|
|
* Run with: bun test formatter.test.ts
|
|
*/
|
|
|
|
import { describe, test, expect } from "vitest";
|
|
import {
|
|
// Search result formatters
|
|
searchResultsToJson,
|
|
searchResultsToCsv,
|
|
searchResultsToFiles,
|
|
searchResultsToMarkdown,
|
|
searchResultsToXml,
|
|
searchResultsToMcpCsv,
|
|
formatSearchResults,
|
|
// Document (multi-get) formatters
|
|
documentsToJson,
|
|
documentsToCsv,
|
|
documentsToFiles,
|
|
documentsToMarkdown,
|
|
documentsToXml,
|
|
formatDocuments,
|
|
// Single document formatters
|
|
documentToJson,
|
|
documentToMarkdown,
|
|
documentToXml,
|
|
formatDocument,
|
|
type MultiGetFile,
|
|
} from "../src/cli/formatter.js";
|
|
import type { SearchResult, DocumentResult } from "../src/store.js";
|
|
|
|
// =============================================================================
|
|
// Test Fixtures
|
|
// =============================================================================
|
|
|
|
const TEST_CONTEXT = "Internal engineering keynotes from company summit events";
|
|
|
|
function makeSearchResult(overrides: Partial<SearchResult> = {}): SearchResult {
|
|
return {
|
|
filepath: "qmd://archive/summit/keynote.md",
|
|
displayPath: "qmd://archive/summit/keynote.md",
|
|
title: "Summit Keynote",
|
|
context: TEST_CONTEXT,
|
|
hash: "dc5590abcdef",
|
|
docid: "dc5590",
|
|
collectionName: "archive",
|
|
modifiedAt: "2024-01-01T00:00:00Z",
|
|
bodyLength: 100,
|
|
body: "---\ntitle: Summit Keynote\n---\n\nThis is the keynote content.",
|
|
score: 0.84,
|
|
source: "fts",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeDocumentResult(overrides: Partial<DocumentResult> = {}): DocumentResult {
|
|
return {
|
|
filepath: "qmd://archive/summit/keynote.md",
|
|
displayPath: "qmd://archive/summit/keynote.md",
|
|
title: "Summit Keynote",
|
|
context: TEST_CONTEXT,
|
|
hash: "dc5590abcdef",
|
|
docid: "dc5590",
|
|
collectionName: "archive",
|
|
modifiedAt: "2024-01-01T00:00:00Z",
|
|
bodyLength: 100,
|
|
body: "---\ntitle: Summit Keynote\n---\n\nThis is the keynote content.",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeMultiGetFile(overrides: Partial<MultiGetFile & { skipped: false }> = {}): MultiGetFile {
|
|
return {
|
|
filepath: "qmd://archive/summit/keynote.md",
|
|
displayPath: "qmd://archive/summit/keynote.md",
|
|
title: "Summit Keynote",
|
|
context: TEST_CONTEXT,
|
|
body: "---\ntitle: Summit Keynote\n---\n\nThis is the keynote content.",
|
|
skipped: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// =============================================================================
|
|
// Search Results: Context in Every Format
|
|
// =============================================================================
|
|
|
|
describe("search results include context in all formats", () => {
|
|
const results = [makeSearchResult()];
|
|
|
|
test("JSON format includes context", () => {
|
|
const output = searchResultsToJson(results, { query: "keynote" });
|
|
const parsed = JSON.parse(output);
|
|
expect(parsed[0].context).toBe(TEST_CONTEXT);
|
|
});
|
|
|
|
test("JSON format includes line", () => {
|
|
const output = searchResultsToJson(results, { query: "keynote" });
|
|
const parsed = JSON.parse(output);
|
|
expect(parsed[0].line).toBeTypeOf("number");
|
|
expect(parsed[0].line).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("JSON format includes line with --full", () => {
|
|
const output = searchResultsToJson(results, { query: "keynote", full: true });
|
|
const parsed = JSON.parse(output);
|
|
expect(parsed[0].line).toBeTypeOf("number");
|
|
expect(parsed[0].line).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("CSV format includes context", () => {
|
|
const output = searchResultsToCsv(results, { query: "keynote" });
|
|
// Header should have context column
|
|
const lines = output.split("\n");
|
|
expect(lines[0]).toContain("context");
|
|
// Data row should contain the context text
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("files format includes context", () => {
|
|
const output = searchResultsToFiles(results);
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("Markdown format includes context", () => {
|
|
const output = searchResultsToMarkdown(results, { query: "keynote" });
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("XML format includes context", () => {
|
|
const output = searchResultsToXml(results, { query: "keynote" });
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("MCP CSV format includes context", () => {
|
|
const mcpResults = [{
|
|
docid: "dc5590",
|
|
file: "qmd://archive/summit/keynote.md",
|
|
title: "Summit Keynote",
|
|
score: 0.84,
|
|
context: TEST_CONTEXT,
|
|
snippet: "This is the keynote content.",
|
|
}];
|
|
const output = searchResultsToMcpCsv(mcpResults);
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("formatSearchResults (JSON) includes context", () => {
|
|
const output = formatSearchResults(results, "json", { query: "keynote" });
|
|
const parsed = JSON.parse(output);
|
|
expect(parsed[0].context).toBe(TEST_CONTEXT);
|
|
});
|
|
|
|
test("formatSearchResults (CSV) includes context", () => {
|
|
const output = formatSearchResults(results, "csv", { query: "keynote" });
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("formatSearchResults (files) includes context", () => {
|
|
const output = formatSearchResults(results, "files");
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("formatSearchResults (md) includes context", () => {
|
|
const output = formatSearchResults(results, "md", { query: "keynote" });
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("formatSearchResults (xml) includes context", () => {
|
|
const output = formatSearchResults(results, "xml", { query: "keynote" });
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Search Results: No Context When Absent
|
|
// =============================================================================
|
|
|
|
describe("search results omit context when null", () => {
|
|
const results = [makeSearchResult({ context: null })];
|
|
|
|
test("JSON format omits context field when null", () => {
|
|
const output = searchResultsToJson(results, { query: "keynote" });
|
|
const parsed = JSON.parse(output);
|
|
expect(parsed[0].context).toBeUndefined();
|
|
});
|
|
|
|
test("files format does not include trailing context when null", () => {
|
|
const output = searchResultsToFiles(results);
|
|
// Should just be docid,score,path - no trailing comma/context
|
|
expect(output).not.toContain(",\"");
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Multi-Get Documents: Context in Every Format
|
|
// =============================================================================
|
|
|
|
describe("multi-get documents include context in all formats", () => {
|
|
const docs = [makeMultiGetFile()];
|
|
|
|
test("JSON format includes context", () => {
|
|
const output = documentsToJson(docs);
|
|
const parsed = JSON.parse(output);
|
|
expect(parsed[0].context).toBe(TEST_CONTEXT);
|
|
});
|
|
|
|
test("CSV format includes context", () => {
|
|
const output = documentsToCsv(docs);
|
|
const lines = output.split("\n");
|
|
expect(lines[0]).toContain("context");
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("files format includes context", () => {
|
|
const output = documentsToFiles(docs);
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("Markdown format includes context", () => {
|
|
const output = documentsToMarkdown(docs);
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("XML format includes context", () => {
|
|
const output = documentsToXml(docs);
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("formatDocuments (JSON) includes context", () => {
|
|
const output = formatDocuments(docs, "json");
|
|
const parsed = JSON.parse(output);
|
|
expect(parsed[0].context).toBe(TEST_CONTEXT);
|
|
});
|
|
|
|
test("formatDocuments (md) includes context", () => {
|
|
const output = formatDocuments(docs, "md");
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("formatDocuments (xml) includes context", () => {
|
|
const output = formatDocuments(docs, "xml");
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Single Document: Context in Every Format
|
|
// =============================================================================
|
|
|
|
describe("single document includes context in all formats", () => {
|
|
const doc = makeDocumentResult();
|
|
|
|
test("JSON format includes context", () => {
|
|
const output = documentToJson(doc);
|
|
const parsed = JSON.parse(output);
|
|
expect(parsed.context).toBe(TEST_CONTEXT);
|
|
});
|
|
|
|
test("Markdown format includes context", () => {
|
|
const output = documentToMarkdown(doc);
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("XML format includes context", () => {
|
|
const output = documentToXml(doc);
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("formatDocument (JSON) includes context", () => {
|
|
const output = formatDocument(doc, "json");
|
|
const parsed = JSON.parse(output);
|
|
expect(parsed.context).toBe(TEST_CONTEXT);
|
|
});
|
|
|
|
test("formatDocument (md) includes context", () => {
|
|
const output = formatDocument(doc, "md");
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
|
|
test("formatDocument (xml) includes context", () => {
|
|
const output = formatDocument(doc, "xml");
|
|
expect(output).toContain(TEST_CONTEXT);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// Single Document: No Context When Absent
|
|
// =============================================================================
|
|
|
|
describe("single document omits context when null", () => {
|
|
const doc = makeDocumentResult({ context: null });
|
|
|
|
test("JSON format omits context field when null", () => {
|
|
const output = documentToJson(doc);
|
|
const parsed = JSON.parse(output);
|
|
expect(parsed.context).toBeUndefined();
|
|
});
|
|
|
|
test("Markdown format does not show Context line when null", () => {
|
|
const output = documentToMarkdown(doc);
|
|
expect(output).not.toContain("Context:");
|
|
});
|
|
|
|
test("XML format does not show context element when null", () => {
|
|
const output = documentToXml(doc);
|
|
expect(output).not.toContain("<context>");
|
|
});
|
|
});
|