Files
hyperguild/ingestion/internal/frontmatter/frontmatter_test.go
T
mathiasandClaude Opus 5 7c9293f4ed feat(ingestion): lint and repair unparseable note frontmatter (#90)
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
2026-08-18 00:07:27 +02:00

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")
}