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