70 lines
3.3 KiB
Go
70 lines
3.3 KiB
Go
package chunk
|
||
|
||
import (
|
||
"reflect"
|
||
"strings"
|
||
"testing"
|
||
|
||
"textmachine/backend/internal/lang"
|
||
)
|
||
|
||
// chunker_manifest_test.go: the seam SplitChunksWithChapters adds for the persisted manifest (backlog
|
||
// row 100). Two properties matter and neither is obvious from the signature: the split itself must not
|
||
// have changed, and the chapter texts returned must be index-aligned to the chapter NUMBERS the split
|
||
// emitted — a chapter that consumes no number must not consume a slot either, or every chapter after the
|
||
// first empty one is identified by its neighbour's text.
|
||
|
||
var manifestSeg = SegBudget{DraftBudgetOut: 1797, EditCeilingOut: 3200, FertCJK: 1.1978, FertOther: 0.3852}
|
||
|
||
func TestSplitChunksWithChaptersLeavesTheSplitUnchanged(t *testing.T) {
|
||
chapters := []string{
|
||
"Первая глава книги.",
|
||
" \n\t ", // whitespace-only: consumes NO chapter number
|
||
"Вторая глава книги.",
|
||
"",
|
||
strings.Repeat("Длинный абзац для нарезки. ", 200),
|
||
}
|
||
want := SplitChunks(chapters, manifestSeg, nil, nil)
|
||
got, kept, _ := SplitChunksWithChapters(chapters, manifestSeg, nil, nil)
|
||
if !reflect.DeepEqual(got, want) {
|
||
t.Fatal("SplitChunksWithChapters must produce the SAME cut as SplitChunks — the chunk boundaries decide what goes on the wire")
|
||
}
|
||
if len(kept) != 3 {
|
||
t.Fatalf("three chapters yield text, so three consume a number; got %d: %q", len(kept), kept)
|
||
}
|
||
// Index alignment: kept[n-1] must be the text of chapter n as the chunker numbered it.
|
||
if kept[0] != chapters[0] || kept[1] != chapters[2] || kept[2] != chapters[4] {
|
||
t.Fatalf("kept texts are not aligned to the emitted chapter numbers: %q", kept)
|
||
}
|
||
// …and every emitted chapter number has a slot.
|
||
highest := 0
|
||
for _, c := range got {
|
||
if c.Chapter > highest {
|
||
highest = c.Chapter
|
||
}
|
||
}
|
||
if highest != len(kept) {
|
||
t.Fatalf("the split emitted chapters up to %d but returned %d chapter texts", highest, len(kept))
|
||
}
|
||
}
|
||
|
||
// TestSplitChunksWithChaptersReturnsPreStripText: the text handed back is the chapter AS INGESTED, with
|
||
// its source header still in it. That is what makes an identity built on it survive a heading-rule edit —
|
||
// the rule changes what is STRIPPED and what title is rendered, not what ingest produced. Asserted with a
|
||
// rule ON and OFF over the same chapter, so the invariance is measured rather than assumed.
|
||
func TestSplitChunksWithChaptersReturnsPreStripText(t *testing.T) {
|
||
chapters := []string{"第一节:подзаголовок\nтело главы"}
|
||
rule := &lang.ChapterRule{Structure: lang.NewSourceStructure("第", "节"), Template: "Глава {n}"}
|
||
|
||
withRule, keptWith, _ := SplitChunksWithChapters(chapters, manifestSeg, rule, nil)
|
||
withoutRule, keptWithout, _ := SplitChunksWithChapters(chapters, manifestSeg, nil, nil)
|
||
if len(withRule) == 0 || len(withoutRule) == 0 {
|
||
t.Fatalf("fixture drifted: %d/%d chunks", len(withRule), len(withoutRule))
|
||
}
|
||
if withRule[0].Heading != "Глава 1" || strings.Contains(withRule[0].Text, "第一节") {
|
||
t.Fatalf("precondition: the rule must render a title and strip the marker, got heading=%q text=%q", withRule[0].Heading, withRule[0].Text)
|
||
}
|
||
if keptWith[0] != chapters[0] || keptWithout[0] != chapters[0] {
|
||
t.Fatalf("the chapter text must be returned AS INGESTED under both rules, got %q / %q", keptWith[0], keptWithout[0])
|
||
}
|
||
}
|