package pipeline import ( "testing" "unicode" "textmachine/backend/internal/store" "textmachine/backend/internal/terminology" ) // stateWithProposals builds one per-chunk telemetry row carrying the given banknote lines. func stateWithProposals(chapter, chunk int, lines ...bankEntry) store.RetrievalState { return store.RetrievalState{Chapter: chapter, ChunkIdx: chunk, BanknoteDetail: bankProposalsJSON(lines)} } // TestBankFoldJoinsEnclosedAndBareSurfaces pins the measured defect: one draft wave proposed the same // title twice, once wrapped in paired marks and once bare, with two different renderings. Unfolded that is // two rows of the owner's signature map, and neither joins the miner's candidate space. func TestBankFoldJoinsEnclosedAndBareSurfaces(t *testing.T) { obs := bankObservedByKey([]store.RetrievalState{ stateWithProposals(1, 0, bankEntry{Src: "《咏梅》", Dst: "Ода сливе", Type: "title"}), stateWithProposals(1, 1, bankEntry{Src: "咏梅", Dst: "Воспевая сливу", Type: "title"}), }, nil) if len(obs) != 1 { t.Fatalf("the two surfaces must fold into ONE candidate, got %d: %+v", len(obs), obs) } if obs[0].Key != "咏梅" || obs[0].Src != "咏梅" { t.Fatalf("key and displayed surface must both be the bare form, got key=%q src=%q", obs[0].Key, obs[0].Src) } if len(obs[0].Proposals) != 2 { t.Fatalf("both renderings must survive as alternatives for the owner, got %+v", obs[0].Proposals) } } // TestBankFoldScreensTheAnswerLanguage pins the draft side of the answer-language screen: a rendering in // another script never becomes evidence, and the same rows pass untouched when no script is declared. func TestBankFoldScreensTheAnswerLanguage(t *testing.T) { states := []store.RetrievalState{stateWithProposals(1, 0, bankEntry{Src: "元海", Dst: "Primordial Sea", Type: "term"}, bankEntry{Src: "方源", Dst: "Фан Юань", Type: "name"}, )} f := newBankFold(unicode.Scripts["Cyrillic"]) addRetrievalStates(f, states) obs := f.observed() if len(obs) != 1 || obs[0].Key != "方源" { t.Fatalf("only the target-language proposal may become evidence, got %+v", obs) } if f.OffLanguage != 1 { t.Fatalf("the dropped proposal must be counted, got %d", f.OffLanguage) } if inert := bankObservedByKey(states, nil); len(inert) != 2 { t.Fatalf("with no declared script the fold must be inert, got %+v", inert) } } // TestBankFoldCountsChunksNotRows is the aggregation invariant in miniature: the same chunk seen twice // (a re-drafted chunk keeps both checkpoints) votes ONCE, so a union never inflates the evidence a // consolidation is ranked on, while a genuinely different chunk does add its vote. func TestBankFoldCountsChunksNotRows(t *testing.T) { prop := []bankProposal{{SrcKey: "方源", Src: "方源", Dst: "Фан Юань", Type: "name"}} f := newBankFold(nil) f.add(1, 0, prop) f.add(1, 0, prop) // the same chunk, drafted a second time if got := f.observed()[0].Proposals[0].Chunks; got != 1 { t.Fatalf("one chunk must count once however many stored answers mention it, got %d", got) } f.add(1, 1, prop) if got := f.observed()[0].Proposals[0].Chunks; got != 2 { t.Fatalf("a second chunk must add its own vote, got %d", got) } } // TestBankFoldIsDeterministic pins that neither the key order nor the rendering order depends on map // iteration — the signature map must be byte-identical between two runs over the same evidence. func TestBankFoldIsDeterministic(t *testing.T) { states := []store.RetrievalState{ stateWithProposals(1, 0, bankEntry{Src: "花家", Dst: "Дом Хуа", Type: "name"}), stateWithProposals(1, 1, bankEntry{Src: "方源", Dst: "Фан Юань", Type: "name"}), stateWithProposals(1, 2, bankEntry{Src: "方源", Dst: "Фан Юань", Type: "name"}), stateWithProposals(1, 3, bankEntry{Src: "方源", Dst: "Фан-Юань", Type: "name"}), } var first []terminology.Observed for i := 0; i < 8; i++ { got := bankObservedByKey(states, nil) if i == 0 { first = got continue } if len(got) != len(first) { t.Fatalf("unstable fold size: %d vs %d", len(got), len(first)) } for j := range got { if got[j].Key != first[j].Key || got[j].Proposals[0].Dst != first[j].Proposals[0].Dst { t.Fatalf("unstable order at %d: %+v vs %+v", j, got[j], first[j]) } } } if first[0].Key != "方源" || first[0].Proposals[0].Dst != "Фан Юань" { t.Fatalf("keys sort, renderings rank most-proposed-first: %+v", first) } }