Files
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

104 lines
3.2 KiB
Go

// ingestion/internal/frontmatter/scan_test.go
package frontmatter
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func writeTree(t *testing.T, files map[string]string) string {
t.Helper()
root := t.TempDir()
for name, content := range files {
path := filepath.Join(root, name)
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
}
return root
}
const (
brokenDoc = "---\ntitle: Broken: title here\nwing: homelab\n---\n\nbody\n"
validDoc = "---\ntitle: 'Fine: quoted'\n---\n\nbody\n"
)
func TestScan_ReportsOnlyBrokenFiles(t *testing.T) {
root := writeTree(t, map[string]string{
"wiki/a/broken.md": brokenDoc,
"wiki/a/valid.md": validDoc,
"wiki/b/plain.md": "# no frontmatter\n\nbody: with colon\n",
"wiki/a/notes.txt": brokenDoc, // not markdown — must be ignored
})
found, err := Scan(root, false)
require.NoError(t, err)
require.Len(t, found, 1)
assert.Equal(t, "wiki/a/broken.md", found[0].Path)
assert.False(t, found[0].Repaired)
assert.Error(t, found[0].Err)
// Dry run must not touch the file.
got, err := os.ReadFile(filepath.Join(root, "wiki/a/broken.md"))
require.NoError(t, err)
assert.Equal(t, brokenDoc, string(got))
}
func TestScan_FixWritesRepairedFilesAndLeavesTheRestAlone(t *testing.T) {
root := writeTree(t, map[string]string{
"wiki/a/broken.md": brokenDoc,
"wiki/a/valid.md": validDoc,
})
found, err := Scan(root, true)
require.NoError(t, err)
require.Len(t, found, 1)
assert.True(t, found[0].Repaired)
assert.NoError(t, found[0].Err)
repaired, err := os.ReadFile(filepath.Join(root, "wiki/a/broken.md"))
require.NoError(t, err)
require.NoError(t, Lint(repaired), "the repaired file must parse")
assert.Contains(t, string(repaired), "title: 'Broken: title here'")
assert.Contains(t, string(repaired), "wing: homelab", "untouched keys must survive")
untouched, err := os.ReadFile(filepath.Join(root, "wiki/a/valid.md"))
require.NoError(t, err)
assert.Equal(t, validDoc, string(untouched))
// Second pass finds nothing — the repair is idempotent and complete.
again, err := Scan(root, true)
require.NoError(t, err)
assert.Empty(t, again)
}
func TestScan_ReportsUnfixableWithoutWriting(t *testing.T) {
unfixable := "---\ntitle: fine\n\tbad: tab-indented\n - stray\n---\n\nbody\n"
root := writeTree(t, map[string]string{"wiki/x.md": unfixable})
found, err := Scan(root, true)
require.NoError(t, err)
require.Len(t, found, 1)
assert.False(t, found[0].Repaired)
assert.Error(t, found[0].Err)
got, err := os.ReadFile(filepath.Join(root, "wiki/x.md"))
require.NoError(t, err)
assert.Equal(t, unfixable, string(got), "an unfixable file must be left exactly as it was")
}
func TestScan_ReportsUndecodableFilesInsteadOfFailingTheRun(t *testing.T) {
root := writeTree(t, map[string]string{"wiki/ok.md": validDoc})
require.NoError(t, os.WriteFile(filepath.Join(root, "wiki/bad-bytes.md"),
[]byte{'-', '-', '-', '\n', 0xff, 0xfe, '\n', '-', '-', '-', '\n'}, 0o644))
found, err := Scan(root, false)
require.NoError(t, err, "one unreadable file must not abort the whole scan")
require.Len(t, found, 1)
assert.Equal(t, "wiki/bad-bytes.md", found[0].Path)
}