Hand-authored and promoted notes skip pipeline.BuildPages, so nothing quotes their scalars. A title carrying an unescaped ": " yields a block yaml.Unmarshal rejects, and every consumer that reads frontmatter drops the note silently — 18 files repo-wide today, up from the 8 in #90. frontmatter.Lint reports it; frontmatter.Fix quotes only what quoting can rescue and refuses to return a document that still fails to parse, so an unfixable file comes back byte-identical rather than half-repaired. Scan walks a tree and reports an unreadable file as a finding instead of aborting, so one poisoned note cannot hide the rest. Sequence items are deliberately left alone: "- foo: bar" is valid YAML that silently becomes a map, which is a type corruption rather than a parse failure, and repairing it would rewrite notes that currently load. task lint:frontmatter # report task lint:frontmatter FIX=1 # repair in place Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BeAp5LTscnz2W7Ubt4eJ6m
159 lines
4.8 KiB
Go
159 lines
4.8 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",
|
|
},
|
|
{
|
|
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_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")
|
|
}
|