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
104 lines
3.2 KiB
Go
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)
|
|
}
|