126 lines
6.1 KiB
Go
126 lines
6.1 KiB
Go
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))
|
|
}
|
|
}
|