textmachine/backend/internal/membank/render_test.go

407 lines
26 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 (
"sort"
"strings"
"testing"
"textmachine/backend/internal/lang"
"textmachine/backend/internal/store"
)
// 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") }
// ⚠ THE ⟨проверить⟩ TAG THIS TEST USED TO REQUIRE IS RETIRED (D39.104 п.2, backlog row 134, ordered by
// this pack's brief). The assertion is inverted rather than deleted: an unsigned row now renders as an
// ordinary line, and the guarantee that replaced «the model is told which rows are candidates» is «every
// row is law and the lines are indistinguishable». A revert reds here.
func TestRenderGlossaryBlock(t *testing.T) {
confirmed := PickedEntry{entry: &entry{src: "阿Q", dst: "А-кью", status: "approved"}, Disp: Confirmed, KeyTrusted: true}
ambiguous := PickedEntry{entry: &entry{src: "小D", dst: "Малыш Дэ", status: "auto"}, Disp: Ambiguous, KeyTrusted: true}
emptyDst := PickedEntry{entry: &entry{src: "鈴木", dst: "", status: "auto"}, Disp: Ambiguous, KeyTrusted: true}
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("unsigned line missing: %q", block)
}
if strings.Contains(block, ruTX().UnverifiedMarker) {
t.Errorf("the ⟨проверить⟩ marker left the wire: %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, KeyTrusted: true}}
}
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)
}
}
// TestTheGenderDirectiveAsksTheMatchAndTheWireAsksNothingElse is what the S7 fix (cold-run session,
// backlog 19) became when the editor's two sections merged. S7's subject was the ROUTING: it separated
// sections by WHO SIGNED a rendering rather than by how confident the match was, because an approved row
// matched by a collision-prone key is downgraded to AMBIGUOUS — a statement about the firing, not the
// authority — and routing on it filed the owner's own signature under «никем не утверждено». Row 134
// removed the sections, so the routing question is gone; the DIRECTIVE question S7 also answered is not,
// and it is what this test now holds, on both blocks at once.
//
// Invisible on zh (Han keys anchor at 2), structural on any phonetic source.
func TestTheGenderDirectiveAsksTheMatchAndTheWireAsksNothingElse(t *testing.T) {
tx := ruTX()
// An owner-SIGNED row whose short phonetic key may have fired inside another word.
signedShort := PickedEntry{entry: &entry{src: "リン", dst: "Рин", status: "approved", gender: "female"}, Via: "りん", Disp: Ambiguous}
// An UNSIGNED row on a clean key — the shape the auto-produced bank actually has.
unsigned := PickedEntry{entry: &entry{src: "方源", dst: "Фан Юань", status: "draft", gender: "male"}, Via: "方源", Disp: Ambiguous, KeyTrusted: true}
block := RenderEditorConstraintBlock([]PickedEntry{signedShort, unsigned}, tx)
if strings.Count(block, tx.EditorHeader) != 1 || strings.Contains(block, tx.EditorUnverifiedHeader) {
t.Fatalf("the editor gets ONE law block: %s", block)
}
for _, want := range []string{"リン → «Рин»", "方源 → «Фан Юань»"} {
if !strings.Contains(block, want) {
t.Errorf("every injected row is in the law block, %q is not: %s", want, block)
}
}
if strings.Contains(block, tx.UnverifiedMarker) {
t.Errorf("no line is marked apart any more: %s", block)
}
// BOTH BLOCKS ASK THE SAME QUESTION, and it is the MATCH. The signature stopped deciding this when the
// editor's two sections merged: with the unsigned RENDERING already carrying the canon's force, holding
// back only the gender was an exception with nothing under it (D39.104 п.1 — the wire is law for every
// row «независимо от статуса»). What is withheld is the doubtful KEY, on both wires, because a gender
// note attached to another entity is wrong rather than merely unsigned.
if !strings.Contains(block, "方源 → «Фан Юань»"+tx.GenderMale) {
t.Errorf("an unsigned row on a clean key must carry its gender to the editor: %s", block)
}
if strings.Contains(block, tx.GenderFemale) {
t.Errorf("a collision-prone firing must carry no gender to the editor: %s", block)
}
draft := RenderGlossaryBlock([]PickedEntry{signedShort, unsigned}, tx)
if !strings.Contains(draft, "方源 → Фан Юань"+tx.GenderMale) {
t.Errorf("an unsigned row on a clean key must carry its gender to the translator: %q", draft)
}
if strings.Contains(draft, tx.GenderFemale) {
t.Errorf("a collision-prone match must carry no gender to the translator either: %q", draft)
}
// The disposition itself is UNCHANGED — it still guards the post-check and the priority budget.
if signedShort.Disp != Ambiguous {
t.Fatal("the split must not upgrade the disposition; it only stops the wire asking it")
}
}
// TestTheLawBlockGivesOneOrderPerSourceTerm is the conflict the section merge made reachable and the render
// silently passed on. Two "lawful" renderings of ONE source term — the owner's signed row and the engine's
// own consolidation of the same surface — used to be two DIFFERENT statements under two headers, one binding
// and one explicitly rejectable. In a single law block they are two ORDERS for one term, and when they carry
// different genders they are two contradictory orders about one character.
//
// ⚠ WHAT YIELDS IS THE UNSIGNED ROW, AND ONLY TO A SIGNED ONE. The first version of this rule deduped on the
// bare `src` and that was a REGRESSION found by planting: the bank's uniqueness key is (src, sense, since,
// until), so one surface legitimately carries SEVERAL signed canons, and dropping one of them because they
// share a surface discards a signature — silently, while the translator's block still rendered both. Both
// halves are asserted here, because either alone is a rule somebody will re-break.
func TestTheLawBlockGivesOneOrderPerSourceTerm(t *testing.T) {
tx := ruTX()
signed := PickedEntry{entry: &entry{src: "方源", dst: "Фан Юань", status: "approved", gender: "male"}, Via: "方源", Disp: Confirmed, KeyTrusted: true}
auto := PickedEntry{entry: &entry{src: "方源", dst: "Фан Юан", status: "draft", gender: "female"}, Via: "方源", Disp: Ambiguous, KeyTrusted: true}
// (1) THE UNSIGNED ROW YIELDS to the signed canon of the same surface.
// ⚠ This case does NOT prove order-independence, though it used to say it did: it is listed first in the
// input, but sortByPriority then puts the signed row in front, so a rule that kept whichever record
// arrived first would pass here too. Case (7) is the one that actually holds that, and it was written
// after a planting walked through this comment's claim.
block := RenderEditorConstraintBlock(sortByPriority([]PickedEntry{auto, signed}), tx)
if n := strings.Count(block, "方源 → "); n != 1 {
t.Fatalf("an unsigned rendering must yield to the signed canon of the same surface, got %d lines:\n%s", n, block)
}
if !strings.Contains(block, "方源 → «Фан Юань»") {
t.Errorf("the SIGNED rendering must be the one that survives:\n%s", block)
}
if strings.Contains(block, tx.GenderFemale) {
t.Errorf("the losing row's gender must not reach the wire alongside the winner's:\n%s", block)
}
if !strings.Contains(block, tx.GenderMale) {
t.Errorf("the surviving row keeps its own directive:\n%s", block)
}
// (2) ⛔ TWO SIGNED SENSES OF ONE SURFACE ARE BOTH LAW. 青山 the surname and 青山 the direction are two
// entities the owner decided about separately — that is what the bank's (src, sense, …) key is FOR — and
// dropping either is discarding a signature. This is the case the first version of the rule broke.
senses := []PickedEntry{
{entry: &entry{src: "青山", dst: "Циншань", sense: "surname", status: "approved"}, Via: "青山", Disp: Confirmed, KeyTrusted: true},
{entry: &entry{src: "青山", dst: "зелёная гора", sense: "direction", status: "approved"}, Via: "青山", Disp: Confirmed, KeyTrusted: true},
}
both := RenderEditorConstraintBlock(sortByPriority(senses), tx)
for _, want := range []string{"青山 → «Циншань»", "青山 → «зелёная гора»"} {
if !strings.Contains(both, want) {
t.Errorf("a signed canon was dropped for sharing a surface with another signed canon (%q):\n%s", want, both)
}
}
// And the translator's block must say the SAME thing — the two wires disagreeing about how many canons a
// surface has is the shape the regression took.
if draft := RenderGlossaryBlock(sortByPriority(senses), tx); strings.Count(draft, "青山 → ") != strings.Count(both, "青山 → ") {
t.Errorf("the two wires disagree about how many canons 青山 has:\neditor: %s\ndraft: %s", both, draft)
}
// (3) A HOMONYMIC dst is the case this must NOT collapse either: two different source terms rendering to
// one Russian surface stay two lines, which is the whole reason the block binds a canon to its source.
homonyms := []PickedEntry{
{entry: &entry{src: "田中", dst: "Танака", status: "approved"}, Via: "田中", Disp: Confirmed, KeyTrusted: true},
{entry: &entry{src: "済", dst: "Танака", status: "approved"}, Via: "済", Disp: Confirmed, KeyTrusted: true},
}
if got := RenderEditorConstraintBlock(homonyms, tx); !strings.Contains(got, "田中 → «Танака»") || !strings.Contains(got, "済 → «Танака»") {
t.Errorf("two source terms with one rendering must stay two lines:\n%s", got)
}
// (4) The same rendering arriving twice — an alias and its main entry — is still ONE order.
dup := []PickedEntry{
{entry: &entry{src: "田中", dst: "Танака", status: "approved"}, Via: "田中", Disp: Confirmed, KeyTrusted: true},
{entry: &entry{src: "田中", dst: "Танака", status: "approved"}, Via: "たなか", Disp: Confirmed, KeyTrusted: true},
}
if n := strings.Count(RenderEditorConstraintBlock(dup, tx), "田中 → «Танака»"); n != 1 {
t.Errorf("one rendering reaching the block twice is one order, got %d", n)
}
// (5) ⛔ SIGNATURE IS A STATUS, NOT A CONFIDENCE. A signed row that happened to fire through a doubtful
// key is still the owner's canon: its MATCH is uncertain, its AUTHORITY is not. Planted — the signed set
// keyed on the disposition instead of the status — an unsigned rendering of the same surface stood beside
// it and the model got two orders for one term. Every other fixture here has Confirmed on the signed row,
// so none of them could tell the two apart.
doubtfulSigned := []PickedEntry{
{entry: &entry{src: "鈴木", dst: "Судзуки", status: "approved", gender: "female"}, Via: "すずき", Disp: Ambiguous, KeyTrusted: false},
// ⚠ NEITHER row is Confirmed, and that is what makes this case able to fail. With the unsigned row
// Confirmed, a rule that keyed the signed set on the DISPOSITION still filled the set — off the
// unsigned row — and the yield happened anyway, so the planting survived under a green assertion.
// The signature must be the ONLY thing that can populate the set here.
{entry: &entry{src: "鈴木", dst: "Судзуки-сан", status: "auto", gender: "male"}, Via: "鈴木", Disp: Ambiguous, KeyTrusted: true},
}
got5 := RenderEditorConstraintBlock(sortByPriority(doubtfulSigned), tx)
if n := strings.Count(got5, "鈴木 → "); n != 1 {
t.Errorf("a SIGNED row that fired on a doubtful key is still the canon others yield to, got %d lines:\n%s", n, got5)
}
if !strings.Contains(got5, "鈴木 → «Судзуки»") {
t.Errorf("the signed rendering must be the survivor even when its own match is uncertain:\n%s", got5)
}
// (6) THE STATUS THAT YIELDS IS «NOT SIGNED», not «draft». `auto` is what the miner writes — the engine's
// own row, which is precisely the row this rule exists to subordinate — and a fixture using only `draft`
// let a planting narrow the rule to `draft` and leave every mined row standing beside the canon.
for _, status := range []string{"draft", "auto"} {
rows := []PickedEntry{
{entry: &entry{src: "元石", dst: "юаньши", status: status, gender: "none"}, Via: "元石", Disp: Ambiguous, KeyTrusted: true},
{entry: &entry{src: "元石", dst: "Юаньши", status: "approved"}, Via: "元石", Disp: Confirmed, KeyTrusted: true},
}
got := RenderEditorConstraintBlock(sortByPriority(rows), tx)
if n := strings.Count(got, "元石 → "); n != 1 {
t.Errorf("an unsigned row with status %q must yield to the signed canon, got %d lines:\n%s", status, n, got)
}
if !strings.Contains(got, "元石 → «Юаньши»") {
t.Errorf("status %q: the signed rendering must be the survivor:\n%s", status, got)
}
}
// (7) ORDER-INDEPENDENCE, asserted where it can actually fail: the block is rendered DIRECTLY, with no
// sortByPriority in front of it, in both orders. A single-pass rule that recorded signatures as it went
// would keep whichever row arrived first and pass every case above, because Select sorts the signed row
// to the front in production. The render must not depend on being handed a sorted slice.
unsignedFirst := []PickedEntry{
{entry: &entry{src: "蛊", dst: "гу-червь", status: "auto"}, Via: "蛊", Disp: Ambiguous, KeyTrusted: true},
{entry: &entry{src: "蛊", dst: "гу", status: "approved"}, Via: "蛊", Disp: Confirmed, KeyTrusted: true},
}
for _, c := range []struct {
name string
rows []PickedEntry
}{
{"unsigned first", unsignedFirst},
{"signed first", []PickedEntry{unsignedFirst[1], unsignedFirst[0]}},
} {
got := RenderEditorConstraintBlock(c.rows, tx)
if n := strings.Count(got, "蛊 → "); n != 1 {
t.Errorf("%s: one order per surface must not depend on the input order, got %d lines:\n%s", c.name, n, got)
}
if !strings.Contains(got, "蛊 → «гу»") {
t.Errorf("%s: the SIGNED rendering must survive whichever order it arrives in:\n%s", c.name, got)
}
}
// (8) A SIGNED ROW WITH NO RENDERING IS NOT A CANON. It orders nothing, so nothing may yield to it —
// otherwise the unsigned rendering is dropped in favour of a row that prints no line at all and the term
// disappears from the law block entirely. The seed loader refuses such a row today, so the clause is
// defensive; it is pinned because «unreachable» is a statement about the CURRENT loader, and the clause
// is one word away from being deleted as dead code.
emptyCanon := []PickedEntry{
{entry: &entry{src: "灵泉", dst: "", status: "approved"}, Via: "灵泉", Disp: Confirmed, KeyTrusted: true},
{entry: &entry{src: "灵泉", dst: "духовный источник", status: "auto"}, Via: "灵泉", Disp: Ambiguous, KeyTrusted: true},
}
if got := RenderEditorConstraintBlock(sortByPriority(emptyCanon), tx); !strings.Contains(got, "灵泉 → «духовный источник»") {
t.Errorf("a signed row with no rendering orders nothing, so the unsigned rendering must stand — "+
"yielding to it deletes the term from the block:\n%s", got)
}
// (9) The signature set and the lookup must agree about whitespace: a src recorded untrimmed and looked
// up trimmed silently stops matching, and every unsigned row of that surface becomes law again.
padded := []PickedEntry{
{entry: &entry{src: " 元海 ", dst: "Юаньхай", status: "approved"}, Via: "元海", Disp: Confirmed, KeyTrusted: true},
{entry: &entry{src: "元海", dst: "Юань Хай", status: "auto"}, Via: "元海", Disp: Ambiguous, KeyTrusted: true},
}
if n := strings.Count(RenderEditorConstraintBlock(sortByPriority(padded), tx), "元海 → "); n != 1 {
t.Errorf("the signed set and the lookup must normalize the surface the same way, got %d lines", n)
}
// (10) ⚠ TWO UNSIGNED ROWS OF ONE SURFACE BOTH STAND — and this is a DECLARED CHANGE OF WIRE BEHAVIOUR,
// written down here because it happened and was not declared when it did.
//
// The rule this block enforces used to dedup on the bare `src`, which collapsed any two renderings of a
// surface to whichever the sort put first. That was corrected because it dropped a second SIGNED canon
// (青山 the surname and 青山 the direction are two decisions the owner made separately). The correction
// keys on (src, dst) and makes only an UNSIGNED row yield, and only to a SIGNED one — so where there is
// no signature at all, nothing yields and both renderings reach the model. Measured across the change:
// the same two unsigned rows produced ONE line before and TWO after.
//
// ⚠ WHAT THIS CASE DOES AND DOES NOT SAY. It pins the RENDER's behaviour: with no signature present
// there is nothing to prefer, so the block does not arbitrate — it shows both and lets the run's own
// unsigned-conflict report (TestUnsignedConflictIsReportedByTheRun) be the place a human is told. A
// silent pick-by-sort-order would be the engine choosing between two of its own guesses and hiding
// that it did, which is strictly worse at THIS layer.
//
// It does NOT say that two unsigned renderings of one surface may exist in the first place. That is an
// UPSTREAM question and the owner has answered it: an unsigned term must have ONE rendering too, and
// the consolidation stage owns that, not the signature. Measured today, the bank accepts the state —
// two rows, one src, one (empty) sense, overlapping spoiler windows, different dst — because the
// same-sense overlap guard (memseed.go) runs PER FILE and the merge (storeOrder) refuses only an exact
// duplicate of the UNIQUE key, so a seed row and a mined-delta row collide only after both have passed
// their own loader. When that is fixed upstream this state stops being reachable and this case becomes
// a statement about something that no longer occurs — which is the right time to delete it, and the
// wrong time to read it as doctrine that two lines are DESIRABLE.
unsignedPair := []PickedEntry{
{entry: &entry{src: "元石", dst: "юаньши", status: "draft"}, Via: "元石", Disp: Ambiguous, KeyTrusted: true},
{entry: &entry{src: "元石", dst: "камень первоисточника", status: "auto"}, Via: "元石", Disp: Ambiguous, KeyTrusted: true},
}
got10 := RenderEditorConstraintBlock(sortByPriority(unsignedPair), tx)
if n := strings.Count(got10, "元石 → "); n != 2 {
t.Errorf("two UNSIGNED renderings of one surface must BOTH reach the model — there is no signature "+
"for either to yield to, and picking one by sort order hides a disagreement the run reports. "+
"Got %d lines:\n%s", n, got10)
}
// And the draft wire must say the same thing: the two wires disagreeing about how many renderings a
// surface has is the shape the original regression took.
if draft := RenderGlossaryBlock(sortByPriority(unsignedPair), tx); strings.Count(draft, "元石 → ") != 2 {
t.Errorf("the two wires disagree about the unsigned pair:\neditor: %s\ndraft: %s", got10, draft)
}
}
// TestTheBudgetKeepsTheTrustworthyMatchFirst is where priorityRank's DISPOSITION axis actually decides
// something, and it is the only place it can be separated from the STATUS axis: two SIGNED rows, one matched
// by a collision-prone key. Everywhere else the two are correlated — an auto row is always ambiguous — which
// is why removing the disposition bit passed the whole tree when it was planted.
//
// The budget is what makes the order observable: it admits one record, and the one that must survive is the
// row whose key is trustworthy, because a doubtful match is the one that may not be about the entity at all.
func TestTheBudgetKeepsTheTrustworthyMatchFirst(t *testing.T) {
// Both APPROVED, both real; only the KEY differs. すずき is three kana — short and purely phonetic, so
// the A2 collision rule downgrades it to AMBIGUOUS despite the signature — while たなかさん is long
// enough to be trusted. Han keys anchor at 2, so this state is only reachable on a phonetic source,
// which is exactly why no zh fixture ever separated the two axes.
clean := gl("たなかさん", "Танака-сан", "", "approved")
doubtful := gl("すずき", "Судзуки", "", "approved")
const chunk = "すずきとたなかさんが来た。"
// ⚠ THE DOUBTFUL ROW IS LISTED FIRST, and that is the whole test: entries keep their frozen order, so a
// priority rule that had lost its disposition axis would leave it first and the assertion below would be
// satisfied by accident. Planted, that is exactly what happened until this order was fixed.
b := bankFrom([]store.GlossaryEntry{doubtful, clean})
sel := b.Select(chunk, 1, nil, 0)
if m := injMap(sel); len(m) != 2 || m["たなかさん"] != Confirmed || m["すずき"] != Ambiguous {
t.Fatalf("premise: one trusted and one collision-prone SIGNED row must fire, got %+v", m)
}
if sel.Injected[0].entry.src != "たなかさん" {
t.Fatalf("the trustworthy match must rank first, so a binding budget keeps it: got %q first", sel.Injected[0].entry.src)
}
// With a budget that admits ONE, the doubtful match is what gets evicted — the preference the
// disposition bit exists for, and the only observable consequence it has.
tight := b.Select(chunk, 1, nil, glossaryLineTokens(sel.Injected[0].entry))
if len(tight.Injected) != 1 || tight.Injected[0].entry.src != "たなかさん" {
t.Fatalf("a binding budget must spend on the trustworthy match: injected %+v, evicted %+v", injMap(tight), tight.Evicted)
}
}
// sortByPriority is what Select does before rendering: the block's order is the budget priority order, and a
// test that hand-builds a selection has to reproduce it or it is testing a state the engine never produces.
func sortByPriority(in []PickedEntry) []PickedEntry {
out := append([]PickedEntry(nil), in...)
sort.SliceStable(out, func(i, j int) bool { return priorityRank(out[i]) < priorityRank(out[j]) })
return out
}
// TestAStickyRecordIsStillLawForTheEditor pins what the block's own doccomment claims — "every injected
// record with a rendering" — and it claimed it about a population no test contained. Planted, excluding
// sticky records from the editor block made them vanish from the law with the whole tree green, golden
// included: a term carried by scene inertia into a pronominal chunk is exactly the case where the editor
// most needs to know what the book calls it, and it would have been the case the block silently dropped.
func TestAStickyRecordIsStillLawForTheEditor(t *testing.T) {
tx := ruTX()
carried := PickedEntry{
entry: &entry{src: "方源", dst: "Фан Юань", status: "approved", gender: "male"},
Sticky: true, Disp: Confirmed, KeyTrusted: true,
}
block := RenderEditorConstraintBlock([]PickedEntry{carried}, tx)
if !strings.Contains(block, "方源 → «Фан Юань»") {
t.Fatalf("a carried record is law for the editor exactly like a fresh hit:\n%s", block)
}
if !strings.Contains(block, tx.GenderMale) {
t.Errorf("and it keeps its directive — the carry does not weaken what the row says:\n%s", block)
}
// The translator's block has always carried them; the two must not disagree about which records exist.
if draft := RenderGlossaryBlock([]PickedEntry{carried}, tx); !strings.Contains(draft, "方源 → Фан Юань") {
t.Errorf("the two wires disagree about a carried record:\n%s", draft)
}
}