101 lines
4.4 KiB
Go
101 lines
4.4 KiB
Go
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)
|
||
}
|
||
}
|