/*
Unit tests for extract covering normalizeWhitespace; splitMarkdownSections; extractParagraphs. It also guards the documented public contract and regression-sensitive edge cases.
- Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only.
- RFC-1311: Compass header sweep — declares the extract suite's contract for test-file scaffold coverage.
*/
import { describe, expect, it } from "vitest";
import {
extractEmail,
extractListFacts,
extractLists,
extractParagraphs,
extractPostalAddress,
mergePeople,
normalizeWhitespace,
splitMarkdownSections,
splitSentences,
} from "../extract.ts";
describe("normalizeWhitespace", () => {
it("collapses multiple spaces", () => {
expect(normalizeWhitespace("a b\n c")).toBe("a b c");
});
});
describe("splitMarkdownSections", () => {
it("splits by h2 headings", () => {
const md = "## Section 1\n\nBody 1\n\n## Section 2\n\nBody 2";
const sections = splitMarkdownSections(md);
expect(sections).toHaveLength(2);
expect(sections[0]).toEqual({ heading: "Section 1", body: "Body 1" });
expect(sections[1]).toEqual({ heading: "Section 2", body: "Body 2" });
});
it("splits by h3 when level=3", () => {
const md = "### Sub 1\n\nBody 1\n\n### Sub 2\n\nBody 2";
const sections = splitMarkdownSections(md, 3);
expect(sections).toHaveLength(2);
expect(sections[0].heading).toBe("Sub 1");
});
it("returns empty for empty input", () => {
expect(splitMarkdownSections("")).toEqual([]);
});
});
describe("extractParagraphs", () => {
it("splits on blank lines", () => {
expect(extractParagraphs("Para 1\n\nPara 2\n\nPara 3")).toEqual(["Para 1", "Para 2", "Para 3"]);
});
});
describe("splitSentences", () => {
it("splits on period + space + capital", () => {
expect(splitSentences("Hello world. Next sentence.")).toEqual([
"Hello world.",
"Next sentence.",
]);
});
it("handles German abbreviations", () => {
const result = splitSentences("Das ist z.B. ein Test. Und noch einer.", "de");
expect(result).toHaveLength(2);
expect(result[0]).toContain("z.B.");
});
it("handles English abbreviations", () => {
const result = splitSentences("See e.g. the docs. For more info.", "en");
expect(result).toHaveLength(2);
});
it("skips numbered list markers", () => {
const result = splitSentences("1. First item 2. Second item", "en");
expect(result).toHaveLength(1);
});
it("returns empty for empty input", () => {
expect(splitSentences("")).toEqual([]);
});
});
describe("extractListFacts", () => {
it("extracts bullet list items", () => {
expect(extractListFacts("- Item 1\n- Item 2")).toEqual(["Item 1", "Item 2"]);
});
it("extracts numbered list items", () => {
expect(extractListFacts("1. First\n2. Second")).toEqual(["First", "Second"]);
});
});
describe("extractLists", () => {
it("extracts a single bullet list with dash marker", () => {
const result = extractLists("- Item 1\n- Item 2\n- Item 3");
expect(result).toEqual([{ marker: "-", items: ["Item 1", "Item 2", "Item 3"] }]);
});
it("extracts a single bullet list with asterisk marker", () => {
const result = extractLists("* Item 1\n* Item 2");
expect(result).toEqual([{ marker: "*", items: ["Item 1", "Item 2"] }]);
});
it("extracts a single bullet list with plus marker", () => {
const result = extractLists("+ Item 1\n+ Item 2");
expect(result).toEqual([{ marker: "+", items: ["Item 1", "Item 2"] }]);
});
it("extracts an ordered list with digit marker", () => {
const result = extractLists("1. First\n2. Second\n3. Third");
expect(result).toEqual([{ marker: "digit", items: ["First", "Second", "Third"] }]);
});
it("groups consecutive list lines into a single list", () => {
const md = "- Alpha\n- Beta\n- Gamma";
const result = extractLists(md);
expect(result).toHaveLength(1);
expect(result[0].items).toHaveLength(3);
});
it("terminates list on blank line", () => {
const md = "- Item 1\n- Item 2\n\nProse text";
const result = extractLists(md);
expect(result).toHaveLength(1);
expect(result[0].items).toEqual(["Item 1", "Item 2"]);
});
it("terminates list on non-list line", () => {
const md = "- Item 1\n- Item 2\nSome prose line";
const result = extractLists(md);
expect(result).toHaveLength(1);
expect(result[0].items).toEqual(["Item 1", "Item 2"]);
});
it("separates lists broken by prose", () => {
const md = "- List 1 item\n\nSome prose\n\n- List 2 item";
const result = extractLists(md);
expect(result).toHaveLength(2);
expect(result[0].items).toEqual(["List 1 item"]);
expect(result[1].items).toEqual(["List 2 item"]);
});
it("skips lines inside fenced code blocks", () => {
const md = "- Real item\n\n```text\n- Not a list item\n- Also not\n```\n\n- Another real item";
const result = extractLists(md);
expect(result).toHaveLength(2);
expect(result[0].items).toEqual(["Real item"]);
expect(result[1].items).toEqual(["Another real item"]);
});
it("skips indented (nested) list lines — they do not start a new list", () => {
const md = "- Top level\n - Nested item\n - Another nested\n- Top level 2";
const result = extractLists(md);
expect(result).toHaveLength(1);
expect(result[0].items).toEqual(["Top level", "Top level 2"]);
});
it("returns empty array for empty input", () => {
expect(extractLists("")).toEqual([]);
});
it("returns empty array for prose with no lists", () => {
expect(extractLists("Just some prose. No lists here.")).toEqual([]);
});
it("separates lists with different marker types", () => {
const md = "- Dash item\n* Star item";
const result = extractLists(md);
expect(result).toHaveLength(2);
expect(result[0].marker).toBe("-");
expect(result[1].marker).toBe("*");
});
});
describe("extractEmail", () => {
it("extracts email from text", () => {
expect(extractEmail("Contact us at info@example.com today")).toBe("info@example.com");
});
it("returns undefined when no email", () => {
expect(extractEmail("No email here")).toBeUndefined();
});
});
describe("extractPostalAddress", () => {
it("extracts address with ZIP", () => {
const result = extractPostalAddress("Test Str. 1, 12345 Berlin");
expect(result).toEqual({
streetAddress: "Test Str. 1",
postalCode: "12345",
addressLocality: "Berlin",
addressCountry: "DE",
});
});
it("returns undefined when no match", () => {
expect(extractPostalAddress("No address here")).toBeUndefined();
});
});
describe("mergePeople", () => {
it("merges and deduplicates by name", () => {
const result = mergePeople(
[{ name: "Jane", role: "Founder" }],
[{ name: "Jane", description: "Bio" }, { name: "John" }],
);
expect(result).toHaveLength(2);
const jane = result.find((p) => p.name === "Jane");
expect(jane?.role).toBe("Founder");
expect(jane?.description).toBe("Bio");
});
it("handles undefined groups", () => {
expect(mergePeople(undefined, [{ name: "A" }])).toEqual([{ name: "A" }]);
});
});