package portfolio import ( "strings" "testing" ) func TestSplitMarkdownSections(t *testing.T) { cases := []struct { name string input string wantCount int wantHeading []string }{ { name: "frontmatter only", input: `--- title: "Foo" tags: ["go"] --- # Foo Body of foo.`, wantCount: 2, wantHeading: []string{"frontmatter", "Foo"}, }, { name: "no frontmatter, multiple H2s", input: `# Title intro paragraph ## Description description body ## Tech stack - Go - SQLite`, wantCount: 3, wantHeading: []string{"Title", "Description", "Tech stack"}, }, { name: "H3 stays under parent H2", input: `# T ## Section content ### H3 detail H3 content stays here`, wantCount: 1, // T has no body → dropped; H3 content folds into Section wantHeading: []string{"Section"}, }, { name: "drop empty section", input: `## X ok ## Empty ## Y content`, wantCount: 2, wantHeading: []string{"X", "Y"}, }, { name: "long H2 sub-splits", input: "## Long\n" + strings.Repeat("a ", 800), wantCount: 2, // 2 sub-splits of Long }, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { got := SplitMarkdownSections(c.input, DefaultChunkerConfig()) if len(got) != c.wantCount { t.Errorf("got %d chunks, want %d. Headings: %v", len(got), c.wantCount, headings(got)) } if c.wantHeading != nil { if !equalSlice(headings(got), c.wantHeading) { t.Errorf("headings = %v, want %v", headings(got), c.wantHeading) } } }) } } func headings(sections []section) []string { out := make([]string, len(sections)) for i, s := range sections { out[i] = s.Heading } return out } func equalSlice(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } // An oversized section made of H3 entries (a CV's Experience list, a FAQ) // must split per entry, not per byte. Size-splitting used to cut a job entry // mid-word and strand the employer name in the previous chunk. func TestSubSplitPrefersH3BoundariesOverByteOffsets(t *testing.T) { body := "### Metrimex — Frontend Developer\n\n#### Jul 2024 – Jun 2026\n\n" + strings.Repeat("Built an access-control app for physical sites. ", 12) + "\n\n### Didcom — Android Developer\n\n#### Jul 2023 – Jul 2024\n\n" + strings.Repeat("Built an Android app for an on-demand ride-sharing service. ", 12) + "\n\n### Teknol — Software Developer\n\n#### Jun 2016 – Jan 2017\n\n" + strings.Repeat("Maintained an internal tool. ", 12) got := subSplit([]section{{Heading: "Experience", Body: body}}, 400, 40) if len(got) != 3 { t.Fatalf("got %d chunks, want one per job:\n%+v", len(got), got) } for i, want := range []string{"Metrimex", "Didcom", "Teknol"} { if !strings.Contains(got[i].Heading, want) { t.Errorf("chunk %d heading = %q, want it to name %s", i, got[i].Heading, want) } if !strings.HasPrefix(got[i].Body, "### "+want) { t.Errorf("chunk %d body should start at its own heading, got %.40q", i, got[i].Body) } } // No chunk may begin mid-sentence — that is the defect this replaces. for i, s := range got { if strings.HasPrefix(s.Body, "... ") { t.Errorf("chunk %d still starts with a byte-split continuation: %.40q", i, s.Body) } } } // A long section with no H3 structure still falls back to size splitting. func TestSubSplitFallsBackToSizeWithoutH3(t *testing.T) { body := strings.Repeat("plain prose with no subheadings at all. ", 40) got := subSplit([]section{{Heading: "Description", Body: body}}, 300, 30) if len(got) < 2 { t.Fatalf("expected the oversized section to be split, got %d", len(got)) } for _, s := range got { if s.Heading != "Description" { t.Errorf("size-split chunk changed heading to %q", s.Heading) } } }