// 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") } // TestSplit_NormalizesCRLFBeforeFenceDetection is hyperguild#138: a note // whose fence lines use CRLF ("---\r\n") failed strings.HasPrefix(doc, // "---\n") on the precondition read in internal/mcp's independent // reimplementation -- encoding alone, no adversarial input, silently // defeated fence detection there before this consolidation. Split must // never repeat that. func TestSplit_NormalizesCRLFBeforeFenceDetection(t *testing.T) { doc := "---\r\ntitle: 'n'\r\nhalf_life: durable\r\n---\r\n\r\nbody\r\n" block, rest, ok := Split([]byte(doc)) require.True(t, ok) assert.Contains(t, string(block), "half_life: durable") assert.Contains(t, string(rest), "body") } // TestSplit_StripsALeadingUTF8BOM is the same mechanism one layer earlier: // a leading BOM (Notepad's default "UTF-8" save, PowerShell's pre-v6 // Out-File/Set-Content) defeats the same HasPrefix check before CRLF // normalization even applies. func TestSplit_StripsALeadingUTF8BOM(t *testing.T) { doc := "\xef\xbb\xbf---\ntitle: 'n'\n---\n\nbody\n" block, rest, ok := Split([]byte(doc)) require.True(t, ok) assert.Contains(t, string(block), "title: 'n'") assert.Contains(t, string(rest), "body") } // TestSplit_RequiresANewlineAfterTheClosingFence closes the looser match // internal/api's independent reimplementation carried (strings.Index(body, // "\n"+fence) with no trailing-newline requirement): a document ending in // "\n---extra" with no newline separating the fence from what follows it is // not a valid closing fence and must not be treated as one. func TestSplit_RequiresANewlineAfterTheClosingFence(t *testing.T) { doc := "---\ntitle: 'n'\n---extra-not-a-real-close" _, _, ok := Split([]byte(doc)) assert.False(t, ok, "a fence line not followed by a newline is not a valid closing fence") }