package doc
import (
"strings"
"testing"
"sourcecraft.dev/bigbes/sr-ht-spec/core"
)
func TestParseFront(t *testing.T) {
cases := []struct {
name string
src string
wantTitle string
wantType string
wantStatus core.Status
wantID string
wantTags []string
wantProps int
wantBody string
}{
{
name: "full document",
src: "---\nid: SPEC-0007\ntitle: Proposal storage model\nstatus: draft\n" +
"type: concept\nsummary: \"Write-optimized storage\"\n" +
"tags:\n - databases\n - storage\n---\n\nA Log-Structured Merge tree.\n",
wantTitle: "Proposal storage model", wantType: "concept",
wantStatus: core.StatusDraft, wantID: "SPEC-0007",
wantTags: []string{"databases", "storage"}, wantProps: 6,
wantBody: "A Log-Structured Merge tree.\n",
},
{
// Pushed with --push-option=skip-validation, or written by hand.
name: "no frontmatter at all",
src: "Loading... [13 kB]\n\nProse begins here.\n",
wantBody: "Loading... [13 kB]\n\nProse begins here.\n",
},
{
// A `**Source:**` bold-label header is body text, not frontmatter,
// and must be left exactly alone.
name: "bold-label header is not frontmatter",
src: "**Source:** https://example.com/a\n\nArticle text.\n",
wantBody: "**Source:** https://example.com/a\n\nArticle text.\n",
},
{
name: "unterminated block stays body",
src: "---\ntitle: broken\n\nno closing fence\n",
wantBody: "---\ntitle: broken\n\nno closing fence\n",
},
{
name: "malformed yaml degrades to no properties",
src: "---\ntitle: [unclosed\n---\n\nBody.\n",
wantBody: "Body.\n",
},
{
// core rejects a duplicated key rather than taking the last one:
// two `id:` lines is exactly the typo that corrupts the registry.
// On the read path that degrades to an untitled document, not to a
// document with one of the two ids silently chosen.
name: "duplicate key degrades to no properties",
src: "---\nid: SPEC-0007\nid: SPEC-0008\ntitle: Two ids\n---\n\nBody.\n",
wantBody: "Body.\n",
},
{
// A thematic break must not be mistaken for an opening fence.
name: "leading thematic break is not frontmatter",
src: "---\n\nJust a rule.\n",
wantBody: "---\n\nJust a rule.\n",
},
{
name: "explicit yaml end marker closes the block",
src: "---\ntitle: Ends early\n...\nBody.\n",
wantTitle: "Ends early", wantProps: 1,
wantBody: "Body.\n",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
front, body := ParseFront([]byte(c.src))
if front.Title != c.wantTitle {
t.Errorf("Title = %q, want %q", front.Title, c.wantTitle)
}
if front.Type != c.wantType {
t.Errorf("Type = %q, want %q", front.Type, c.wantType)
}
if front.Status != c.wantStatus {
t.Errorf("Status = %q, want %q", front.Status, c.wantStatus)
}
if front.ID != c.wantID {
t.Errorf("ID = %q, want %q", front.ID, c.wantID)
}
if strings.Join(front.Tags, ",") != strings.Join(c.wantTags, ",") {
t.Errorf("Tags = %v, want %v", front.Tags, c.wantTags)
}
if len(front.Props) != c.wantProps {
t.Errorf("Props = %+v, want %d entries", front.Props, c.wantProps)
}
if string(body) != c.wantBody {
t.Errorf("body = %q, want %q", body, c.wantBody)
}
})
}
}
// The modelled fields must come from core and nowhere else, so that what the
// read plane believes a header says is what the push hook validated.
func TestParseFrontUsesCoreForModelledFields(t *testing.T) {
src := "---\nid: SPEC-0007\ntitle: T\nstatus: review\nsupersedes: SPEC-0003\n" +
"owners: [~bigbes]\nparent: \"[[storage]]\"\naliases: [storage-model]\n" +
"planned: [SPEC-0009]\n---\n\nbody\n"
front, _ := ParseFront([]byte(src))
if err := core.DefaultSchema().ValidateFrontmatter(front.Frontmatter); err != nil {
t.Fatalf("core rejects the frontmatter this package parsed: %v", err)
}
if front.Supersedes != "SPEC-0003" || len(front.Owners) != 1 {
t.Errorf("core fields lost: %+v", front.Frontmatter)
}
if !front.Has("parent") {
t.Errorf("Present must cover every key, including ones core does not model")
}
if front.Parent != "[[storage]]" {
t.Errorf("Parent = %q", front.Parent)
}
if strings.Join(front.Aliases, ",") != "storage-model" {
t.Errorf("Aliases = %v", front.Aliases)
}
if strings.Join(front.Planned, ",") != "SPEC-0009" {
t.Errorf("Planned = %v", front.Planned)
}
}
func TestFrontSearchTextAndProp(t *testing.T) {
front, _ := ParseFront([]byte("---\nid: SPEC-0007\ntitle: T\nparent: \"[[storage]]\"\n---\n\nbody\n"))
if got := front.Prop("Parent"); got != "[[storage]]" {
t.Errorf("Prop(Parent) = %q", got)
}
st := front.SearchText()
if strings.Contains(st, "[[") {
t.Errorf("search text kept wikilink brackets: %q", st)
}
if !strings.Contains(st, "id: SPEC-0007") {
t.Errorf("search text missing the id line: %q", st)
}
}
func TestLinkTarget(t *testing.T) {
cases := map[string]string{
`"[[storage]]"`: "storage",
`[[storage]]`: "storage",
`[[storage|Storage]]`: "storage",
` [[specs/index]] `: "specs/index",
`storage`: "storage",
`"[[a-b]]"`: "a-b",
``: "",
}
for in, want := range cases {
if got := LinkTarget(in); got != want {
t.Errorf("LinkTarget(%q) = %q, want %q", in, got, want)
}
}
}