fix(wiki): give the preamble its own field instead of overloading heading=="" (#103)
Round 3 review: verified structurally that the preamble is no longer in the sections slice or the idx map, so it cannot collide with a degenerate `## ` heading. Flip-tested by reverting to the heading=="" model and confirming TestMerge_DegenerateEmptyHeadingDoesNotCollideWithPreamble goes red. Adversarial pass (multiple degenerate headings, frontmatter-only page, unicode around the ## boundary, 3x self-merge idempotency with preamble+degenerate) all held. CI green on 09aeaa9 (run 7820).
Co-authored-by: Mathias <mthbqv@gmail.com>
177 lines
7.8 KiB
Go
177 lines
7.8 KiB
Go
// ingestion/internal/wiki/merge_test.go
|
|
package wiki
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestMerge_BulletSectionsUnion(t *testing.T) {
|
|
a := Page{Path: "wiki/concepts/foo.md", Content: "---\ntitle: Foo\n---\n\n## Related Concepts\n\n- [[bar|Bar]]\n"}
|
|
b := Page{Path: "wiki/concepts/foo.md", Content: "---\ntitle: Foo\n---\n\n## Related Concepts\n\n- [[bar|Bar]]\n- [[baz|Baz]]\n"}
|
|
|
|
got := Merge(a, b)
|
|
assert.Contains(t, got.Content, "[[bar|Bar]]")
|
|
assert.Contains(t, got.Content, "[[baz|Baz]]")
|
|
assert.Equal(t, 1, strings.Count(got.Content, "[[bar|Bar]]"))
|
|
}
|
|
|
|
func TestMerge_AppendSections(t *testing.T) {
|
|
a := Page{Path: "wiki/concepts/foo.md", Content: "---\ntitle: Foo\n---\n\n## Evolving Notes\n\nFirst note.\n"}
|
|
b := Page{Path: "wiki/concepts/foo.md", Content: "---\ntitle: Foo\n---\n\n## Evolving Notes\n\nSecond note.\n"}
|
|
|
|
got := Merge(a, b)
|
|
assert.Contains(t, got.Content, "First note.")
|
|
assert.Contains(t, got.Content, "Second note.")
|
|
}
|
|
|
|
func TestMerge_KeepFirstForOtherSections(t *testing.T) {
|
|
a := Page{Path: "wiki/concepts/foo.md", Content: "---\ntitle: Foo\n---\n\n## Definition\n\nFirst definition.\n"}
|
|
b := Page{Path: "wiki/concepts/foo.md", Content: "---\ntitle: Foo\n---\n\n## Definition\n\nSecond definition.\n"}
|
|
|
|
got := Merge(a, b)
|
|
assert.Contains(t, got.Content, "First definition.")
|
|
assert.NotContains(t, got.Content, "Second definition.")
|
|
}
|
|
|
|
func TestMerge_NewSectionFromB(t *testing.T) {
|
|
a := Page{Path: "wiki/concepts/foo.md", Content: "---\ntitle: Foo\n---\n\n## Definition\n\nA thing.\n"}
|
|
b := Page{Path: "wiki/concepts/foo.md", Content: "---\ntitle: Foo\n---\n\n## Why It Matters\n\nBecause reasons.\n"}
|
|
|
|
got := Merge(a, b)
|
|
assert.Contains(t, got.Content, "A thing.")
|
|
assert.Contains(t, got.Content, "Because reasons.")
|
|
}
|
|
|
|
func TestMerge_KeepsFrontmatterFromA(t *testing.T) {
|
|
a := Page{Path: "p.md", Content: "---\ntitle: A\nlast_updated: 2026-01-01\n---\n\n## Definition\n\nA.\n"}
|
|
b := Page{Path: "p.md", Content: "---\ntitle: B\nlast_updated: 2026-06-01\n---\n\n## Definition\n\nB.\n"}
|
|
|
|
got := Merge(a, b)
|
|
assert.Contains(t, got.Content, "title: A")
|
|
assert.NotContains(t, got.Content, "title: B")
|
|
}
|
|
|
|
// --- hyperguild#103 review: a page with content outside any `## ` section ---
|
|
// must never lose that content on merge. parseSections used to only capture
|
|
// text between `## ` headings — anything before the first heading (a common,
|
|
// ordinary shape for a hand-written note: an intro paragraph before the first
|
|
// section) or, worse, an entire page with no `## ` headings at all (a legacy
|
|
// note, a stub, any hand-edit that doesn't follow the section convention)
|
|
// vanished silently. Merge would then fill the page entirely from the fresh
|
|
// extraction — the opposite of "never silently rewrite what a human put
|
|
// there," the guarantee this whole file exists to provide.
|
|
|
|
func TestMerge_PreservesOnDiskContentWithNoSectionHeadings(t *testing.T) {
|
|
a := Page{Path: "wiki/x.md", Content: "---\ntitle: X\n---\n\nJust some free text a human wrote. No section headings at all.\n"}
|
|
b := Page{Path: "wiki/x.md", Content: "---\ntitle: X\n---\n\n## Definition\n\nFresh extraction's take.\n"}
|
|
|
|
got := Merge(a, b)
|
|
assert.Contains(t, got.Content, "Just some free text a human wrote",
|
|
"on-disk content with no ## headings must survive the merge, not be replaced wholesale by the fresh extraction")
|
|
}
|
|
|
|
func TestMerge_DegenerateEmptyHeadingDoesNotCollideWithPreamble(t *testing.T) {
|
|
// Second review pass on #103: representing the preamble as a section with
|
|
// heading=="" collides with a literal `## ` (marker, no title text) —
|
|
// strings.TrimPrefix("## ", "## ") is also "". `a` has only a preamble
|
|
// (no degenerate heading of its own); `b` introduces a genuinely new
|
|
// degenerate `## ` section. Because both key to "", Merge's idx thinks
|
|
// b's section "already exists" in a (it's really just a's unrelated
|
|
// preamble) and never adds it — b's section vanishes from the output
|
|
// entirely, not merged, not kept, not appended.
|
|
a := Page{Path: "p.md", Content: "---\ntitle: X\n---\n\nAn intro paragraph.\n\n## Definition\n\nA thing.\n"}
|
|
b := Page{Path: "p.md", Content: "---\ntitle: X\n---\n\n## \n\nA genuinely new degenerate section's body.\n\n## Definition\n\nA thing.\n"}
|
|
|
|
got := Merge(a, b)
|
|
assert.Contains(t, got.Content, "An intro paragraph.", "a's own preamble must survive")
|
|
assert.Contains(t, got.Content, "A genuinely new degenerate section's body.",
|
|
"b's degenerate section is new content, not a collision with a's unrelated preamble — it must not be silently dropped")
|
|
}
|
|
|
|
func TestMerge_PreservesPreambleBeforeFirstHeading(t *testing.T) {
|
|
a := Page{Path: "wiki/x.md", Content: "---\ntitle: X\n---\n\nAn intro paragraph before the first heading.\n\n## Definition\n\nA thing.\n"}
|
|
b := Page{Path: "wiki/x.md", Content: "---\ntitle: X\n---\n\n## Definition\n\nA thing.\n"}
|
|
|
|
got := Merge(a, b)
|
|
assert.Contains(t, got.Content, "An intro paragraph before the first heading",
|
|
"text before the first ## heading is ordinary page content, not something the parser is entitled to drop")
|
|
}
|
|
|
|
// --- hyperguild#103 review: dedup must be whitespace-insensitive -----------
|
|
// Page content is produced by an LLM per run, so byte-identical re-extraction
|
|
// of "the same" logical line is not guaranteed — different bullet spacing or
|
|
// trailing whitespace on an otherwise-identical line must still dedup, or
|
|
// idempotency only holds when the extractor happens to reproduce whitespace
|
|
// exactly, which is not a real guarantee.
|
|
|
|
func TestMerge_AppendSectionDedupsAcrossWhitespaceVariants(t *testing.T) {
|
|
a := Page{Path: "p.md", Content: "---\ntitle: X\n---\n\n## Open Questions\n\n- Does this converge?\n"}
|
|
b := Page{Path: "p.md", Content: "---\ntitle: X\n---\n\n## Open Questions\n\n- Does this converge? \n"}
|
|
|
|
got := Merge(a, b)
|
|
assert.Equal(t, 1, strings.Count(got.Content, "Does this converge?"),
|
|
"same logical line with different whitespace must dedup, not duplicate")
|
|
}
|
|
|
|
func TestMerge_BulletSectionDedupsAcrossWhitespaceVariants(t *testing.T) {
|
|
a := Page{Path: "p.md", Content: "---\ntitle: X\n---\n\n## Related Concepts\n\n- [[bar|Bar]]\n"}
|
|
b := Page{Path: "p.md", Content: "---\ntitle: X\n---\n\n## Related Concepts\n\n- [[bar|Bar]]\n"}
|
|
|
|
got := Merge(a, b)
|
|
assert.Equal(t, 1, strings.Count(got.Content, "[[bar|Bar]]"),
|
|
"same logical bullet with different whitespace must dedup, not duplicate")
|
|
}
|
|
|
|
// --- brain#21/B1: merging must be safe to run again -------------------------
|
|
|
|
func TestMerge_AppendSectionDoesNotDuplicateContentAlreadyPresent(t *testing.T) {
|
|
// Compile-on-ingest merges a freshly extracted page into the one already
|
|
// on disk. Bullet sections union, so they are naturally idempotent — but
|
|
// append sections concatenate, so re-ingesting an unchanged source would
|
|
// grow "Open Questions" on every run until the page is unreadable.
|
|
page := Page{Path: "wiki/sources/x.md", Content: `---
|
|
title: X
|
|
---
|
|
|
|
## Open Questions
|
|
|
|
- Does this converge?
|
|
`}
|
|
|
|
once := Merge(page, page)
|
|
twice := Merge(once, page)
|
|
|
|
assert.Equal(t, 1, strings.Count(once.Content, "Does this converge?"),
|
|
"merging a page with itself must not duplicate an append section")
|
|
assert.Equal(t, once.Content, twice.Content,
|
|
"merge must be idempotent — a third pass may not keep growing the page")
|
|
}
|
|
|
|
func TestMerge_AppendSectionStillAccumulatesGenuinelyNewContent(t *testing.T) {
|
|
// The dedup must not turn append sections into keep-a-only sections:
|
|
// new material is exactly what compile-on-ingest exists to add.
|
|
existing := Page{Path: "wiki/sources/x.md", Content: `---
|
|
title: X
|
|
---
|
|
|
|
## Open Questions
|
|
|
|
- Does this converge?
|
|
`}
|
|
fresh := Page{Path: "wiki/sources/x.md", Content: `---
|
|
title: X
|
|
---
|
|
|
|
## Open Questions
|
|
|
|
- What happens under load?
|
|
`}
|
|
|
|
got := Merge(existing, fresh).Content
|
|
assert.Contains(t, got, "Does this converge?", "existing question must survive")
|
|
assert.Contains(t, got, "What happens under load?", "new question must be added")
|
|
}
|