hyperguild#128, filed while fixing the same bug class in the sibling internal/wiki package's parseFrontmatterBlock (hyperguild#126 review round 3). This package was the original parseFrontmatter/set()/render() shape that reimplementation was modeled on, and had the same gap: an indented YAML block-list item containing a colon (e.g. a title with a subtitle inside an aliases: list) got cut at the colon and treated as a fake top-level key, since strings.Cut doesn't know a line is a continuation just because TrimSpace(key) happens to be non-empty. Re-rendering then stripped the item's indentation, de-indenting it out of the list it belongs to. This package backs UpdateNote/writeHallNote, so it's on the live brain_update write path. Fixed the same way as the wiki package: check indentation before calling Cut at all -- a line starting with a space or tab is a continuation, full stop, regardless of what it contains. Second, related bug found via the fix's own test fixture, not speculatively: render() wrote "key: " (trailing space) for a key with an empty value (the same "aliases:" block-list header line) instead of bare "key:" -- a different byte sequence than the original, breaking this type's whole documented purpose (a byte-preserving line view, not a YAML re-marshal). No current caller sets an empty-value key via set(), so this hadn't shown up in a set()-driven path, but it fires on every ordinary parse-then-render round-trip of a note that already has one -- confirmed directly via this fix's own test output before writing a fix for it. Flip-tested both independently: disabling either fix turns its corresponding test red, the other stays green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
96 lines
3.6 KiB
Go
96 lines
3.6 KiB
Go
package api
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestParseFrontmatterSplitsHeaderAndBody(t *testing.T) {
|
|
src := "---\nwing: jepa-fx\nhall: facts\ncreated_at: 2026-01-01T00:00:00Z\n---\n# Title\n\nbody text\n"
|
|
fm, body := parseFrontmatter(src)
|
|
|
|
assert.Equal(t, "jepa-fx", fm.get("wing"))
|
|
assert.Equal(t, "facts", fm.get("hall"))
|
|
assert.Equal(t, "2026-01-01T00:00:00Z", fm.get("created_at"))
|
|
assert.Equal(t, "# Title\n\nbody text\n", body)
|
|
}
|
|
|
|
func TestParseFrontmatterNoHeader(t *testing.T) {
|
|
src := "# Just a body\n\nno frontmatter here\n"
|
|
fm, body := parseFrontmatter(src)
|
|
|
|
assert.Empty(t, fm.lines)
|
|
assert.Equal(t, src, body)
|
|
}
|
|
|
|
func TestFrontmatterSetOverridesExistingKey(t *testing.T) {
|
|
fm, _ := parseFrontmatter("---\nwing: a\nupdated_at: old\n---\nbody\n")
|
|
fm.set("updated_at", "new")
|
|
|
|
assert.Equal(t, "new", fm.get("updated_at"))
|
|
// No duplicate key.
|
|
assert.Equal(t, 1, strings.Count(fm.render(), "updated_at:"))
|
|
}
|
|
|
|
func TestFrontmatterSetAppendsNewKey(t *testing.T) {
|
|
fm, _ := parseFrontmatter("---\nwing: a\n---\nbody\n")
|
|
fm.set("supersedes", "abc123")
|
|
|
|
out := fm.render()
|
|
assert.Contains(t, out, "wing: a")
|
|
assert.Contains(t, out, "supersedes: abc123")
|
|
}
|
|
|
|
func TestFrontmatterRenderPreservesCustomFields(t *testing.T) {
|
|
src := "---\nwing: a\nhall: facts\ncustom_field: keep-me\ntags: [x, y]\n---\nbody\n"
|
|
fm, _ := parseFrontmatter(src)
|
|
fm.set("updated_at", "2026-06-22T00:00:00Z")
|
|
|
|
out := fm.render()
|
|
assert.Contains(t, out, "custom_field: keep-me")
|
|
assert.Contains(t, out, "tags: [x, y]")
|
|
assert.Contains(t, out, "updated_at: 2026-06-22T00:00:00Z")
|
|
}
|
|
|
|
func TestFrontmatterRenderRoundTrips(t *testing.T) {
|
|
src := "---\nwing: a\nhall: facts\n---\n"
|
|
fm, _ := parseFrontmatter(src)
|
|
assert.Equal(t, src, fm.render())
|
|
}
|
|
|
|
func TestParseFrontmatterIndentedListItemWithColonIsNotMisparsedAsKey(t *testing.T) {
|
|
// hyperguild#128: an indented YAML block-list item that happens to
|
|
// contain a colon (e.g. a title with a subtitle) is a continuation
|
|
// line, never a top-level key — strings.Cut doesn't know that just
|
|
// because TrimSpace(key) happens to be non-empty. Misparsing it as a
|
|
// key/value pair and re-rendering strips its indentation, de-indenting
|
|
// it out of the list it belongs to. Same bug class already found and
|
|
// fixed in the sibling internal/wiki package's parseFrontmatterBlock
|
|
// (hyperguild#126 review round 3) — this package was the original the
|
|
// reimplementation was modeled on and had the gap too.
|
|
src := "---\naliases:\n - 'Chapter 3: The Reckoning'\ntitle: 'X'\n---\nbody\n"
|
|
fm, _ := parseFrontmatter(src)
|
|
fm.set("updated_at", "2026-09-23T00:00:00Z")
|
|
|
|
out := fm.render()
|
|
assert.Contains(t, out, " - 'Chapter 3: The Reckoning'",
|
|
"the indented list item must survive verbatim, not get re-emitted as a de-indented fake key")
|
|
assert.NotContains(t, out, "\n- 'Chapter 3",
|
|
"must not be split into a fake key/value pair at column 0 (as opposed to surviving indented, asserted above)")
|
|
}
|
|
|
|
func TestFrontmatterRenderEmptyValueKeyStaysBareNoTrailingSpace(t *testing.T) {
|
|
// Companion to the indented-list-item fix: "aliases:" (a key with
|
|
// nothing after the colon, the YAML block-list header) must round-trip
|
|
// bare, not as "aliases: " with a trailing space — a different byte
|
|
// sequence than the original, breaking this type's whole reason for
|
|
// being a line-preserving view rather than a YAML re-marshal.
|
|
// render() returns only the frontmatter block (see
|
|
// TestFrontmatterRenderRoundTrips), no trailing body.
|
|
src := "---\naliases:\n - one\n - two\n---\n"
|
|
fm, _ := parseFrontmatter(src)
|
|
assert.Equal(t, src, fm.render())
|
|
}
|