textmachine/backend/internal/membank/render_test.go

123 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package membank
import (
"strings"
"testing"
"textmachine/backend/internal/lang"
)
// render_test.go: the injection RENDERER fixtures — the exact bytes the bank puts on the wire for
// each role. ruTX is the ru target wire-text (pair-14 §2: the strings live in lang/data/injection.txt).
func ruTX() lang.InjectionTexts { return lang.InjectionTextsFor("ru") }
func TestRenderGlossaryBlock(t *testing.T) {
confirmed := PickedEntry{entry: &entry{src: "阿Q", dst: "А-кью", status: "approved"}, Disp: Confirmed}
ambiguous := PickedEntry{entry: &entry{src: "小D", dst: "Малыш Дэ", status: "auto"}, Disp: Ambiguous}
emptyDst := PickedEntry{entry: &entry{src: "鈴木", dst: "", status: "auto"}, Disp: Ambiguous}
block := RenderGlossaryBlock([]PickedEntry{confirmed, ambiguous, emptyDst}, ruTX())
if !strings.Contains(block, "阿Q → А-кью") {
t.Errorf("confirmed line missing: %q", block)
}
if !strings.Contains(block, "小D → Малыш Дэ ⟨проверить⟩") {
t.Errorf("ambiguous line missing its unverified tag: %q", block)
}
if strings.Contains(block, "鈴木") {
t.Errorf("empty-dst candidate should be skipped (nothing to inject): %q", block)
}
// Nothing to inject → empty block (better nothing).
if RenderGlossaryBlock(nil, ruTX()) != "" {
t.Error("empty selection must render an empty block")
}
if RenderGlossaryBlock([]PickedEntry{emptyDst}, ruTX()) != "" {
t.Error("only-empty-dst selection must render an empty block")
}
}
// TestDC3GenderInjection pins the DC3 gender constraint annotation on the editor block (WS5 §5(б)): a
// gendered CONFIRMED term carries a gender directive; a genderless term does not (byte-identical line).
func TestDC3GenderInjection(t *testing.T) {
mk := func(gender string) []PickedEntry {
return []PickedEntry{{entry: &entry{src: "方源", dst: "Фан Юань", status: "approved", gender: gender}, Via: "方源", Disp: Confirmed}}
}
male := RenderEditorConstraintBlock(mk("male"), ruTX())
if !strings.Contains(male, "方源 → «Фан Юань» (муж.") {
t.Fatalf("male term must carry a masculine directive, got:\n%s", male)
}
hidden := RenderEditorConstraintBlock(mk("hidden"), ruTX())
if !strings.Contains(hidden, "пол СКРЫТ") {
t.Fatalf("hidden term must carry the gender-avoidance mandate, got:\n%s", hidden)
}
none := RenderEditorConstraintBlock(mk(""), ruTX())
if !strings.HasSuffix(none, "«Фан Юань»") {
t.Fatalf("a genderless term must have NO gender note (line ends at the dst), got:\n%s", none)
}
}
// TestZeroValuePickedEntryAndBankDoNotPanic pins the pack-16 tail: both types are EXPORTED with exported
// fields, so a caller outside this package can construct a zero value whose private row/matcher is nil.
// Every consumer used to nil-deref on it — a panic in a paid run. The contract is now "a row-less record is
// skipped; a matcher-less bank selects nothing", which cannot change a real bank's behaviour because
// Materialize and Select always populate both.
func TestZeroValuePickedEntryAndBankDoNotPanic(t *testing.T) {
zero := []PickedEntry{{Via: "x", Disp: Confirmed}}
tx := lang.InjectionTextsFor("ru")
if got := RenderGlossaryBlock(zero, tx); got != "" {
t.Fatalf("a row-less record must render nothing, got %q", got)
}
if got := RenderEditorConstraintBlock(zero, tx); got != "" {
t.Fatalf("a row-less record must render no constraint, got %q", got)
}
var b Bank
if misses := b.Postcheck(zero, "любой текст"); !misses.Empty() {
t.Fatalf("a row-less record must produce no post-check miss, got %+v", misses)
}
if sel := b.Select("непустой текст для матчера", 1, nil, 100); len(sel.Injected) != 0 {
t.Fatalf("a zero-value bank must select nothing, got %+v", sel.Injected)
}
}
// TestEditorSectionFollowsProvenance pins the S7 fix (cold-run session, backlog 19): the editor's two
// sections separate WHO SIGNED a rendering, not how confident the match was. dispositionFor downgrades an
// approved row matched by a collision-prone key (short + purely phonetic) to AMBIGUOUS — a statement about
// the firing, not the authority — and routing the section on it filed the owner's own signature under
// «никем не утверждено». Invisible on zh (Han keys anchor at 2), structural on any phonetic source.
func TestEditorSectionFollowsProvenance(t *testing.T) {
signedShort := PickedEntry{entry: &entry{src: "リン", dst: "Рин", status: "approved", gender: "female"}, Via: "りん", Disp: Ambiguous}
unsigned := PickedEntry{entry: &entry{src: "方源", dst: "Фан Юань", status: "draft"}, Via: "方源", Disp: Ambiguous}
block := RenderEditorConstraintBlock([]PickedEntry{signedShort, unsigned}, ruTX())
tx := ruTX()
canon, rest, ok := strings.Cut(block, tx.EditorUnverifiedHeader)
if !ok {
t.Fatalf("both sections must be present, got:\n%s", block)
}
if !strings.Contains(canon, "リン → «Рин»") {
t.Errorf("an owner-SIGNED row belongs to the canon section, got:\n%s", canon)
}
if strings.Contains(canon, "方源") {
t.Errorf("an unsigned row must not be canon, got:\n%s", canon)
}
if !strings.Contains(rest, "方源 → «Фан Юань»") || !strings.Contains(rest, tx.UnverifiedMarker) {
t.Errorf("the unsigned row must be in the marked section, got:\n%s", rest)
}
if strings.Contains(canon, tx.UnverifiedMarker) {
t.Errorf("the canon section must carry no unverified marker, got:\n%s", canon)
}
// The gender DIRECTIVE still keys on the match, not on the signature: on a collision-prone firing the
// row may not be about the entity on the page, and «женский род» would then be an instruction to get
// it wrong. Authority moves the section; confidence keeps the directive.
if strings.Contains(canon, tx.GenderFemale) {
t.Errorf("a collision-prone firing must carry no gender directive, got:\n%s", canon)
}
// The disposition itself is UNCHANGED — it still guards the post-check and the translator block's
// ⟨проверить⟩ tag. Only the section moved.
if signedShort.Disp != Ambiguous {
t.Fatal("the fix must not upgrade the disposition; it only re-routes the section")
}
if draft := RenderGlossaryBlock([]PickedEntry{signedShort}, ruTX()); !strings.Contains(draft, tx.UnverifiedMarker) {
t.Errorf("the TRANSLATOR block still marks a collision-prone match unverified, got: %q", draft)
}
}