textmachine/backend/internal/pipeline/titleraw_test.go

73 lines
3.5 KiB
Go

package pipeline
import (
"strings"
"testing"
"textmachine/backend/internal/chunk"
"textmachine/backend/internal/config"
"textmachine/backend/internal/lang"
)
// TestTitleRawSurvivesAChapterThatTakesNoNumber is the alignment guard the source title needs and the
// rendered heading never did.
//
// ⛔ THE FAILURE IS SILENT. Numbering is DENSE: a chapter left empty after its header is stripped consumes no
// number. Titles arrive as a slice parallel to the RAW chapters, so selecting them by ordinal instead of by
// the chunker's own kept indices hands chapter N's title to chapter N+1 — and nothing looks wrong, because
// every chapter still has a title. Only its content is another chapter's.
func TestTitleRawSurvivesAChapterThatTakesNoNumber(t *testing.T) {
body := strings.Repeat("蛊", 200)
chapters := []string{
"第一章\n" + body,
"第二章", // header only — stripHeading empties it, so it takes no number
"第三章\n" + body,
}
doc := &chunk.Document{Chapters: chapters, Titles: []string{"第一章", "第二章", "第三章"}}
rule := &lang.ChapterRule{Structure: lang.DefaultCJKStructure(), Template: "Глава {n}"}
seg := chunk.SegBudget{DraftBudgetOut: 4000, EditCeilingOut: 8000, FertCJK: 1.2, FertOther: 0.4}
_, kept, keptIdx := chunk.SplitChunksWithChapters(chapters, seg, rule, nil)
if len(kept) != 2 {
t.Fatalf("kept %d chapters, want 2 — the header-only chapter must take no number", len(kept))
}
dense := denseFrom(doc, keptIdx)
want := []string{"第一章", "第三章"}
for i, w := range want {
if dense.titles[i] != w {
t.Fatalf("chapter %d title = %q, want %q — the titles shifted past the numberless chapter",
i+1, dense.titles[i], w)
}
}
if len(dense.texts) != len(dense.titles) {
t.Fatalf("texts %d vs titles %d — the pairing that makes misalignment unreachable is broken",
len(dense.texts), len(dense.titles))
}
}
// TestTheManifestKeyCarriesTheStructureGrammar is the third of the three pins the structure component needs,
// and it was the one nobody held: the cut TAG is guarded by reflection over cutInputs, the wave snapshot by
// the golden, and the manifest KEY by nothing at all.
//
// ⛔ The key is what decides whether a stored manifest is still current. Leave the grammar out of it and a
// sidecar written under a DIFFERENT grammar keeps validating — a reader is handed a chapter tree that
// describes boundaries the engine no longer draws, and every id in it looks exactly as valid as before.
func TestTheManifestKeyCarriesTheStructureGrammar(t *testing.T) {
src := sourceFingerprint{SHA: "sha-fixture", Bytes: 42}
base := &Runner{Pipeline: &config.Pipeline{}, Book: &config.Book{}, structure: lang.DefaultCJKStructure()}
other := &Runner{Pipeline: &config.Pipeline{}, Book: &config.Book{}, structure: lang.NewSourceStructure("제", "장")}
if base.structure.Fingerprint() == other.structure.Fingerprint() {
t.Fatal("the two grammars must differ, or this test proves nothing")
}
if base.manifestKey(src) == other.manifestKey(src) {
t.Fatal("the manifest key does not carry the structure grammar: a sidecar cut by another grammar would keep validating")
}
// And the absence of a grammar is itself a value the key must distinguish — "no grammar declared" cuts
// differently from any grammar at all.
none := &Runner{Pipeline: &config.Pipeline{}, Book: &config.Book{}}
if none.manifestKey(src) == base.manifestKey(src) {
t.Fatal("a book with NO grammar keys the same as one cut by the CJK default")
}
}