`title: "Foo" — bar` opens with a quote and is not quoted; the previous prefix check read it as already-quoted and left two notes broken. Probing the value as the right-hand side of a mapping answers the real question — does this load as written — and leaves anything that already parses alone, whatever it parses into. Repairs the last two quotable files in mathias/brain. Only wiki/index.md remains unparseable, for an unrelated reason: a truncated UTF-8 sequence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
189 lines
5.9 KiB
Go
189 lines
5.9 KiB
Go
// ingestion/internal/frontmatter/frontmatter_test.go
|
|
package frontmatter
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func TestLint(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
doc string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "no frontmatter block is not an error",
|
|
doc: "# Just a heading\n\nSome body text.\n",
|
|
},
|
|
{
|
|
name: "quoted title parses",
|
|
doc: "---\ntitle: 'Headless dispatch runs: Bash is denied'\n---\n\nbody\n",
|
|
},
|
|
{
|
|
name: "plain title parses",
|
|
doc: "---\ntitle: A simple title\nwing: homelab\n---\n\nbody\n",
|
|
},
|
|
{
|
|
name: "unquoted colon in title does not parse",
|
|
doc: "---\ntitle: Headless dispatch runs: Bash is denied — use gitea MCP\nwing: homelab\n---\n\nbody\n",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
// Known gap, deliberately not fixed here: this parses, but the
|
|
// item becomes map[string]any{"Headless dispatch": "denied"}
|
|
// rather than a string. It is a silent type corruption, not a
|
|
// parse failure, so repairing it would rewrite documents that
|
|
// currently load — out of scope for a lint about broken YAML.
|
|
name: "unquoted colon in a list item parses as a mapping, not an error",
|
|
doc: "---\ntitle: Fine\naliases:\n - Headless dispatch: denied\n---\n\nbody\n",
|
|
},
|
|
{
|
|
name: "unterminated frontmatter is not treated as frontmatter",
|
|
doc: "---\ntitle: no closing fence\n\nbody\n",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := Lint([]byte(tt.doc))
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFix(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
doc string
|
|
wantChanged bool
|
|
wantKey string
|
|
wantValue string
|
|
}{
|
|
{
|
|
name: "quotes an unquoted title containing a colon",
|
|
doc: "---\ntitle: Headless dispatch runs: Bash is denied — use gitea MCP\nwing: homelab\n---\n\nbody\n",
|
|
wantChanged: true,
|
|
wantKey: "title",
|
|
wantValue: "Headless dispatch runs: Bash is denied — use gitea MCP",
|
|
},
|
|
{
|
|
// Parses already (see the Lint table), so Fix must leave it alone
|
|
// rather than rewrite a document that loads.
|
|
name: "leaves a colon-bearing list item alone because it already parses",
|
|
doc: "---\ntitle: Fine\naliases:\n - Headless dispatch: denied\n---\n\nbody\n",
|
|
wantChanged: false,
|
|
wantKey: "title",
|
|
wantValue: "Fine",
|
|
},
|
|
{
|
|
name: "escapes an embedded single quote when quoting",
|
|
doc: "---\ntitle: Don't do this: it breaks\n---\n\nbody\n",
|
|
wantChanged: true,
|
|
wantKey: "title",
|
|
wantValue: "Don't do this: it breaks",
|
|
},
|
|
{
|
|
// The value opens with a double quote and then keeps going after
|
|
// the closing one, so YAML reads it as a quoted scalar followed by
|
|
// junk. Treating "starts with a quote" as "already quoted" leaves
|
|
// this broken forever.
|
|
name: "quotes a value that only looks quoted",
|
|
doc: "---\ntitle: \"Evaluation Driven Generation\" — a TDD-style scaffold\ntags: [a, b]\n---\n\nbody\n",
|
|
wantChanged: true,
|
|
wantKey: "title",
|
|
wantValue: "\"Evaluation Driven Generation\" — a TDD-style scaffold",
|
|
},
|
|
{
|
|
name: "leaves an already-valid document untouched",
|
|
doc: "---\ntitle: 'Already quoted: fine'\nwing: homelab\n---\n\nbody\n",
|
|
wantChanged: false,
|
|
wantKey: "title",
|
|
wantValue: "Already quoted: fine",
|
|
},
|
|
{
|
|
name: "leaves a document with no frontmatter untouched",
|
|
doc: "# Heading\n\nbody: with a colon\n",
|
|
wantChanged: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, changed, err := Fix([]byte(tt.doc))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.wantChanged, changed)
|
|
|
|
if !tt.wantChanged {
|
|
assert.Equal(t, tt.doc, string(got), "unchanged docs must be returned byte-identical")
|
|
}
|
|
|
|
// Whatever came back must parse — that is the whole point.
|
|
require.NoError(t, Lint(got), "Fix must not return a document that still fails to parse")
|
|
|
|
if tt.wantKey == "" {
|
|
return
|
|
}
|
|
block, ok := block(got)
|
|
require.True(t, ok)
|
|
var m map[string]any
|
|
require.NoError(t, yaml.Unmarshal(block, &m))
|
|
assert.Equal(t, tt.wantValue, m[tt.wantKey], "the value must survive quoting unchanged")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFix_LeavesStructuredValuesStructured(t *testing.T) {
|
|
// Only the broken scalar may be touched. A flow sequence on a neighbouring
|
|
// line still has to load as a sequence afterwards, not as a string.
|
|
doc := "---\ntitle: Broken: here\ntags: [prompting, llm]\ncount: 3\n---\n\nbody\n"
|
|
|
|
got, changed, err := Fix([]byte(doc))
|
|
require.NoError(t, err)
|
|
require.True(t, changed)
|
|
|
|
b, ok := block(got)
|
|
require.True(t, ok)
|
|
var m map[string]any
|
|
require.NoError(t, yaml.Unmarshal(b, &m))
|
|
|
|
assert.Equal(t, "Broken: here", m["title"])
|
|
assert.Equal(t, []any{"prompting", "llm"}, m["tags"])
|
|
assert.Equal(t, 3, m["count"])
|
|
}
|
|
|
|
func TestFix_PreservesBodyExactly(t *testing.T) {
|
|
body := "\nThe body has: colons, 'quotes', and\n---\na horizontal rule.\n"
|
|
doc := "---\ntitle: Broken: title\n---" + body
|
|
|
|
got, changed, err := Fix([]byte(doc))
|
|
require.NoError(t, err)
|
|
require.True(t, changed)
|
|
assert.True(t, len(got) > 0)
|
|
|
|
_, rest, ok := split([]byte(doc))
|
|
require.True(t, ok)
|
|
_, gotRest, ok := split(got)
|
|
require.True(t, ok)
|
|
assert.Equal(t, string(rest), string(gotRest), "the body must not be touched")
|
|
}
|
|
|
|
func TestFix_RefusesToReturnAnUnparseableResult(t *testing.T) {
|
|
// A frontmatter block that quoting cannot rescue: the structure itself is
|
|
// broken, not a scalar. Fix must return the original and an error rather
|
|
// than a half-repaired document.
|
|
doc := "---\ntitle: fine\n\tbad: tab-indented mapping\n - stray list item\n---\n\nbody\n"
|
|
|
|
got, changed, err := Fix([]byte(doc))
|
|
require.Error(t, err)
|
|
assert.False(t, changed)
|
|
assert.Equal(t, doc, string(got), "an unfixable document must come back byte-identical")
|
|
}
|