package pipeline import ( "strings" "testing" "textmachine/backend/internal/store" ) // memory_trustgate_test.go pins the DISPOSITION-GATED longest-match suppressor (D39 слой 4, // L3-suppressor-disposition-blind): a longer but LOWER-trust key (draft/ambiguous 四代族长) must NOT // suppress a nested HIGHER-trust key (approved 族长) — the term-drift code root. The whole-entity // longest-match is preserved when the longer key is ≥ trust. Each refused suppression is recorded // loudly (trustGated). Han keys are used so normalizeSourceKey is (near) identity — the stored // event keys equal the raw src. // trustGateKeys collects the (suppressor→protected) pairs of a selection's trust-gate events. func trustGateKeys(sel memorySelection) map[string]string { m := map[string]string{} for _, e := range sel.trustGated { m[e.Suppressor] = e.Protected } return m } // TestSuppressorTrustGateFires is the FIX: a longer draft key nested over an approved shorter key no // longer eats it. BEFORE the gate the approved 族长 was dropped from Select (never injected, invisible // to post-check) and the draft 四代族长 injected only AMBIGUOUS (editor-excluded) → the reader got // NOTHING. AFTER: the approved 族长 survives as CONFIRMED (editor gets it) and the collision is logged. func TestSuppressorTrustGateFires(t *testing.T) { zuzhang := gl("族长", "глава клана", "title", "approved") zuzhang.Decl = declJSON(false, "глава клана", "главы клана", "главе клана") sidai := gl("四代族长", "четвёртый глава клана", "title", "draft") // longer, LOWER trust b := bankFrom([]store.GlossaryEntry{zuzhang, sidai}) sel := b.Select("四代族长 вошёл в зал.", 1, nil, 0) m := injMap(sel) if m["族长"] != memConfirmed { t.Fatalf("approved 族长 must survive as CONFIRMED (the fix), got %q; injected=%v", m["族长"], m) } if m["四代族长"] != memAmbiguous { t.Errorf("draft 四代族长 must still inject AMBIGUOUS alongside, got %q", m["四代族长"]) } // Editor constraint block: the approved term is now present (was empty before the fix). if blk := renderEditorConstraintBlock(sel.injected); !strings.Contains(blk, "глава клана") { t.Errorf("editor constraint block must carry the approved «глава клана», got %q", blk) } // Loud record of the refused suppression. if got := trustGateKeys(sel); got["四代族长"] != "族长" { t.Errorf("expected a trust-gate event 四代族长⊃族长, got %v", got) } if len(sel.trustGated) != 1 { t.Errorf("expected exactly 1 trust-gate event, got %d", len(sel.trustGated)) } } // TestSuppressorApprovedOverApprovedStillSuppresses is the regression-safety guard: an APPROVED // longer key legitimately eats a nested APPROVED shorter one (whole-entity longest-match, unchanged). // This also spot-verifies the D38.5 promote: once 四代族长 is promoted draft→approved the whole entity // wins — but the FIX above means the promote is no longer LOAD-BEARING for correctness (a draft // 四代族长 no longer silently drops 族长). func TestSuppressorApprovedOverApprovedStillSuppresses(t *testing.T) { zuzhang := gl("族长", "глава клана", "title", "approved") zuzhang.Decl = declJSON(false, "глава клана") sidai := gl("四代族长", "четвёртый глава клана", "title", "approved") // longer, EQUAL trust b := bankFrom([]store.GlossaryEntry{zuzhang, sidai}) sel := b.Select("四代族长 вошёл в зал.", 1, nil, 0) m := injMap(sel) if _, present := m["族长"]; present { t.Errorf("approved 族长 nested in an EQUAL-trust approved 四代族长 must be suppressed (whole-entity), got injected=%v", m) } if m["四代族长"] != memConfirmed { t.Errorf("the whole entity 四代族长 must inject CONFIRMED, got %q", m["四代族长"]) } if len(sel.trustGated) != 0 { t.Errorf("an EQUAL-trust suppression is legitimate — no trust-gate event, got %d", len(sel.trustGated)) } } // TestSuppressorDraftOverDraftStillSuppresses pins the edge the task calls out: two DRAFT keys // (equal trust) behave as before — the longer suppresses the nested, no trust-gate event. func TestSuppressorDraftOverDraftStillSuppresses(t *testing.T) { zuzhang := gl("族长", "глава клана", "title", "draft") sidai := gl("四代族长", "четвёртый глава клана", "title", "draft") b := bankFrom([]store.GlossaryEntry{zuzhang, sidai}) sel := b.Select("四代族长 вошёл в зал.", 1, nil, 0) m := injMap(sel) if _, present := m["族长"]; present { t.Errorf("draft 族长 nested in an equal-trust draft 四代族长 must be suppressed, got %v", m) } if m["四代族长"] != memAmbiguous { t.Errorf("the longer draft must inject AMBIGUOUS, got %q", m["四代族长"]) } if len(sel.trustGated) != 0 { t.Errorf("equal-trust suppression records no event, got %d", len(sel.trustGated)) } } // TestSuppressorSpoilerBlockedLongDoesNotSuppress preserves self-review #6: a spoiler-BLOCKED longer // key is not a valid suppressor at all, so the nested valid approved key survives — and this is NOT a // trust-gate event (the refusal path records only a spoiler-VALID lower-trust suppressor). func TestSuppressorSpoilerBlockedLongDoesNotSuppress(t *testing.T) { zuzhang := gl("族长", "глава клана", "title", "approved") zuzhang.Decl = declJSON(false, "глава клана") sidai := gl("四代族长", "четвёртый глава клана", "title", "approved") sidai.SinceCh = 200 // spoiler-blocked before ch200 b := bankFrom([]store.GlossaryEntry{zuzhang, sidai}) sel := b.Select("四代族长 вошёл в зал.", 1, nil, 0) // ch1 → 四代族长 blocked m := injMap(sel) if m["族长"] != memConfirmed { t.Errorf("approved 族长 must survive a spoiler-blocked longer key, got %q", m["族长"]) } if len(sel.rejected) != 1 || sel.rejected[0].entry.src != "四代族长" { t.Errorf("the spoiler-blocked 四代族长 must be rejected+logged, got %v", sel.rejected) } if len(sel.trustGated) != 0 { t.Errorf("a spoiler-block is not a trust-gate event, got %d", len(sel.trustGated)) } }