73 lines
3.5 KiB
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")
|
|
}
|
|
}
|