leethobbit_obsidian-strong-.../tests/sections.test.ts
Dan 20c0b49a77 Review fix pass: data-loss criticals, codec hardening, leak cleanup
Full senior-review sweep (all findings fixed, 530 tests green):

- Prep board: sync cached body after own section writes (typed scenes
  could vanish from the UI and then be erased from disk)
- Dashboard: pip toggle is now a read-modify-write against the note's
  current Fronts section, re-located by name (stale cache silently
  deleted fronts added on Foundation)
- CampaignStore: notify on body-only changes to managed notes (root
  cause under both criticals); self-write marks now outlive done() so
  the cache echo still reads as a self-write (in-flight counted)
- sections.ts: fence-aware parsing (fenced ## lines no longer corrupt
  managed writes); body-split tolerates BOM and empty frontmatter
- Frontmatter codecs: unknown-key passthrough on write (newer-version
  fields survive older-plugin writes), id-bearing secret rows survive
  empty text (tombstone resurrection), ledger/session-zero patches read
  current state inside processFrontMatter instead of metadataCache
- Monster builder: attack-count round-trip no longer corrupts damage at
  CR 1/8, 9, 16, 17; CR fractions render as 1/8-1/2; preview title live
- Wizard double-fire guard; loadIfDeferred in openView; starter/demo
  builders refuse re-runs; safer filenames (#^[], reserved names)
- Per-render listener scopes (RenderScopes) for header/rail, prep, run
  scenes/secrets, tables; popout-safe timers; toast/undo handle cleanup
- Log-row rollback on failed writes; 0x table weights round-trip;
  duplicate doom lines preserved; obsidian dep pinned to 1.13.1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:58:35 -04:00

179 lines
7.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { buildScaffold, healSections, parseSections, removeSection, replaceSection, sectionContent } from "../src/lib/sections";
const SCAFFOLD = "## Campaign pitch\n\n## Six truths\n\n## Fronts\n\n## House rules\n";
describe("parseSections", () => {
it("finds every H2 section in order", () => {
const sections = parseSections(SCAFFOLD);
expect(sections.map((s) => s.heading)).toEqual(["Campaign pitch", "Six truths", "Fronts", "House rules"]);
});
it("ignores H3+ headings", () => {
const body = "## Fronts\n\n### The Veiled Hand\ngoal line\n\n## House rules\n";
const sections = parseSections(body);
expect(sections.map((s) => s.heading)).toEqual(["Fronts", "House rules"]);
expect(sections[0].content).toContain("### The Veiled Hand");
});
it("captures content exactly between headings", () => {
const body = "## Pitch\nA doomed city.\n\n## Truths\n- one\n";
const sections = parseSections(body);
expect(sections[0].content).toBe("A doomed city.\n\n");
expect(sections[1].content).toBe("- one\n");
});
it("returns nothing for a body with no managed headings", () => {
expect(parseSections("just some prose")).toEqual([]);
});
it("ignores a heading-like line inside a backtick fence", () => {
const body = "## Fronts\n```\n## Fake\n```\ngoal line\n\n## House rules\n";
const sections = parseSections(body);
expect(sections.map((s) => s.heading)).toEqual(["Fronts", "House rules"]);
});
it("ignores a heading-like line inside a tilde fence", () => {
const body = "## Fronts\n~~~\n## Fake\n~~~\ngoal line\n\n## House rules\n";
const sections = parseSections(body);
expect(sections.map((s) => s.heading)).toEqual(["Fronts", "House rules"]);
});
it("treats an unclosed fence as running to end of body", () => {
const body = "## Fronts\n```\n## Fake\nstill fenced\n";
const sections = parseSections(body);
expect(sections.map((s) => s.heading)).toEqual(["Fronts"]);
expect(sections[0].content).toBe("```\n## Fake\nstill fenced\n");
});
it("still finds a real heading after a properly closed fence", () => {
const body = "## Fronts\n```\ncode\n```\n\n## House rules\nno flanking\n";
const sections = parseSections(body);
expect(sections.map((s) => s.heading)).toEqual(["Fronts", "House rules"]);
});
it("treats an indented (<=3 space) fence opener as a fence", () => {
const body = "## Fronts\n ```\n## Fake\n ```\n\n## House rules\n";
const sections = parseSections(body);
expect(sections.map((s) => s.heading)).toEqual(["Fronts", "House rules"]);
});
it("does not treat a 4-space-indented ``` as a fence opener", () => {
const body = "## Fronts\n ```\n## Real\ntext\n";
const sections = parseSections(body);
expect(sections.map((s) => s.heading)).toEqual(["Fronts", "Real"]);
});
});
describe("replaceSection", () => {
it("replaces one section's content, leaving everything else byte-for-byte identical", () => {
const body = "## Pitch\nold pitch\n\n## Truths\n- one\n- two\n";
const updated = replaceSection(body, "Pitch", "new pitch");
expect(updated).toBe("## Pitch\nnew pitch\n\n## Truths\n- one\n- two\n");
});
it("matches heading names case-insensitively, preserving the original heading casing", () => {
const body = "## campaign pitch\nold\n";
const updated = replaceSection(body, "Campaign Pitch", "new");
expect(updated).toBe("## campaign pitch\nnew\n");
});
it("tolerates reordered/user headings and only touches the target", () => {
const body = "## House rules\nno flanking\n\n## Pitch\nold\n";
const updated = replaceSection(body, "Pitch", "new");
expect(updated).toBe("## House rules\nno flanking\n\n## Pitch\nnew\n");
});
it("appends the section at the end when the heading is missing", () => {
const body = "## Pitch\nold\n";
const updated = replaceSection(body, "Truths", "- one");
expect(updated).toBe("## Pitch\nold\n\n## Truths\n- one\n");
});
it("creates the heading in an empty body", () => {
expect(replaceSection("", "Pitch", "new")).toBe("## Pitch\nnew\n");
});
it("treats a fenced fake heading as section content, not a boundary — replacing spans past it to the real next section", () => {
// The fence belongs to Pitch's content, so replacing Pitch replaces it
// too; the win is that the span reaches the REAL next heading instead
// of stopping at (or corrupting) the fenced `## Fake` line.
const body = "## Pitch\nold\n```\n## Fake\n```\n\n## Truths\n- one\n";
const updated = replaceSection(body, "Pitch", "new");
expect(updated).toBe("## Pitch\nnew\n\n## Truths\n- one\n");
});
it("appends a new section rather than editing inside a fence when the target heading only exists fenced", () => {
const body = "## Pitch\nold\n```\n## Fake\n```\n";
const updated = replaceSection(body, "Fake", "x");
expect(updated).toBe("## Pitch\nold\n```\n## Fake\n```\n\n## Fake\nx\n");
});
it("leaves a fence inside an untouched section's content byte-for-byte when replacing a different section", () => {
const body = "## Pitch\nold\n\n## Truths\n```\n## Fake\ncode\n```\nkept\n";
const updated = replaceSection(body, "Pitch", "new");
expect(updated).toBe("## Pitch\nnew\n\n## Truths\n```\n## Fake\ncode\n```\nkept\n");
});
});
describe("healSections", () => {
it("appends only the managed headings that are missing, preserving existing content", () => {
const body = "## Pitch\nold pitch\n";
const healed = healSections(body, ["Pitch", "Truths", "Fronts"]);
expect(healed).toBe("## Pitch\nold pitch\n\n## Truths\n\n## Fronts\n");
});
it("is a no-op when every required heading is already present", () => {
const body = "## Pitch\nold\n\n## Truths\n- one\n";
expect(healSections(body, ["Pitch", "Truths"])).toBe(body);
});
});
describe("buildScaffold", () => {
it("produces one empty section per heading", () => {
const scaffold = buildScaffold(["Pitch", "Truths"]);
expect(parseSections(scaffold).map((s) => s.heading)).toEqual(["Pitch", "Truths"]);
});
});
describe("sectionContent", () => {
it("returns one section's trimmed content", () => {
const body = "## Strong start\nDrop them in the fight.\n\n## Scenes\n- one\n";
expect(sectionContent(body, "Strong start")).toBe("Drop them in the fight.");
});
it("matches case-insensitively", () => {
expect(sectionContent("## strong start\ntext\n", "Strong Start")).toBe("text");
});
it("returns an empty string when the heading is missing", () => {
expect(sectionContent("## Other\ntext\n", "Strong start")).toBe("");
});
it("includes a fenced fake heading as part of the section's content", () => {
const body = "## Strong start\nintro\n```\n## Fake\ncode\n```\noutro\n";
expect(sectionContent(body, "Strong start")).toBe("intro\n```\n## Fake\ncode\n```\noutro");
});
});
describe("removeSection", () => {
it("removes the heading line and content, leaving neighbors byte-identical", () => {
const body = "## Aspects\n- steamy\n- deep\n\n## History\nOld stone.\n";
expect(removeSection(body, "Aspects")).toBe("## History\nOld stone.\n");
});
it("matches the heading case-insensitively", () => {
const body = "intro\n\n## ASPECTS\n- one\n\n## Next\nkeep\n";
expect(removeSection(body, "aspects")).toBe("intro\n\n## Next\nkeep\n");
});
it("removes a last section without touching prior content", () => {
const body = "## Keep\ntext\n\n## Aspects\n- one\n";
expect(removeSection(body, "Aspects")).toBe("## Keep\ntext\n\n");
});
it("no-ops when the heading is absent", () => {
const body = "## Keep\ntext\n";
expect(removeSection(body, "Aspects")).toBe(body);
});
});