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