textmachine/backend/internal/membank/mempostcheck_test.go

98 lines
5.3 KiB
Go
Raw Permalink 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
// mempostcheck_test.go: mutation tests for the #10 fix (D39.39) — a single-Han-rune firing key demotes a
// dst-absent post-check miss from a hard CONFIRMED consistency-failure to the Unverified observability
// channel, because its Aho-Corasick substring firing (转 inside 转身) is not reliable evidence the entity
// occurred. Multi-rune Han keys keep a word-like span and stay CONFIRMED.
import (
"testing"
"textmachine/backend/internal/store"
)
func TestSingleHanKeyMissDemoted(t *testing.T) {
// 转 (a rank head) seeded with allow_short, approved. It fires as a SUBSTRING of 转身 «turned around»,
// where the rank rendering «ранг» is legitimately absent — the classic #10 false CONFIRMED miss.
e := gl("转", "ранг", "", "approved")
e.AllowShort = true
b := bankFrom([]store.GlossaryEntry{e})
sel := b.Select("他转身离开,头也不回。", 1, nil, 0)
if len(sel.Injected) != 1 {
t.Fatalf("单 Han key with allow_short should fire (substring), got %d injected", len(sel.Injected))
}
res := b.Postcheck(sel.Injected, "Он повернулся и ушёл, не оглядываясь.") // no «ранг» in the output
if res.ConfirmedCount() != 0 {
t.Errorf("#10: a single-Han-rune key miss must NOT be a CONFIRMED miss, got %d confirmed", res.ConfirmedCount())
}
if len(res.Unverified) != 1 {
t.Errorf("#10: the single-Han-rune miss must survive as Unverified observability, got %d", len(res.Unverified))
}
if len(res.Unverified) == 1 && !(res.Unverified[0].Demoted && res.Unverified[0].Disp == "confirmed") {
t.Errorf("#10: the demoted miss must be flagged Demoted while keeping disp:confirmed (the injection trust), got %+v", res.Unverified[0])
}
}
func TestMultiHanKeyMissStaysConfirmed(t *testing.T) {
// Control: a TWO-rune Han key keeps a word-like span, so a genuine drop is still a CONFIRMED miss —
// the #10 demote must be narrow (single rune only), not a blanket Han exemption.
e := gl("元石", "первокамень", "", "approved")
b := bankFrom([]store.GlossaryEntry{e})
sel := b.Select("他手里握着元石。", 1, nil, 0)
if len(sel.Injected) != 1 {
t.Fatalf("2-rune Han key should fire, got %d", len(sel.Injected))
}
res := b.Postcheck(sel.Injected, "Он держал в руке камень.") // «первокамень» absent → a real miss
if res.ConfirmedCount() != 1 {
t.Errorf("a multi-rune Han key miss must stay CONFIRMED, got %d", res.ConfirmedCount())
}
}
func TestSingleNonHanKeyMissStaysConfirmed(t *testing.T) {
// The #10 demote is Han-ONLY by RATIFIED SCOPE, not because kana is segmented: a lone katakana ロ firing as
// a substring of ゼロ is equally weak evidence — kana is NOT boundary-checked either (memory.go:536). The
// demote is deliberately scoped to Han until the B6 tokenizer (backlog row 81), so a single NON-Han key must
// stay a CONFIRMED miss (and carry no Demoted flag). This kills the mutant that demotes on len(r)==1 ALONE
// (dropping the unicode.Han guard in singleHanKeyFired), which the rest of the suite otherwise survives.
e := gl("ロ", "Зеро", "", "approved")
e.AllowShort = true
b := bankFrom([]store.GlossaryEntry{e})
sel := b.Select("彼はゼロを見た。", 1, nil, 0)
if len(sel.Injected) != 1 {
t.Fatalf("single non-Han (kana) key with allow_short should fire, got %d injected", len(sel.Injected))
}
res := b.Postcheck(sel.Injected, "Он посмотрел вверх.") // «Зеро» absent → a real miss
if res.ConfirmedCount() != 1 {
t.Errorf("a single NON-Han (kana) key miss must stay CONFIRMED (#10 demote is Han-only), got %d", res.ConfirmedCount())
}
if len(res.Unverified) != 0 {
t.Errorf("a non-Han single-rune miss must not be demoted to Unverified, got %d", len(res.Unverified))
}
}
func TestAmbiguousMissNotDemoted(t *testing.T) {
// The negative twin of the #10 demote: an AMBIGUOUS (auto/draft) injection that misses lands in Unverified
// as the model's ENTITLED refusal, not as a demotion. It must carry Demoted=false — Demoted is a
// single-Han-KEY-evidence axis (a SIGNED miss on weak evidence), orthogonal to the injection trust. This
// kills the mutant `m.Demoted = true` in Postcheck's default branch, which the rest of the suite otherwise
// survives (every other Unverified case is a demoted CONFIRMED miss, so none pins Demoted=false on ambiguous).
e := gl("元石", "первокамень", "", "auto") // auto → Ambiguous injection; 2-rune Han key → no #10 demote
b := bankFrom([]store.GlossaryEntry{e})
sel := b.Select("他手里握着元石。", 1, nil, 0)
if len(sel.Injected) != 1 {
t.Fatalf("2-rune auto key should fire, got %d", len(sel.Injected))
}
res := b.Postcheck(sel.Injected, "Он держал в руке камень.") // «первокамень» absent → a miss
if res.ConfirmedCount() != 0 {
t.Errorf("an ambiguous miss must not be CONFIRMED, got %d", res.ConfirmedCount())
}
if len(res.Unverified) != 1 {
t.Fatalf("the ambiguous miss must survive as Unverified, got %d", len(res.Unverified))
}
if res.Unverified[0].Demoted {
t.Errorf("an ambiguous miss must carry Demoted=false (Demoted is a single-Han-key axis, not the injection trust), got %+v", res.Unverified[0])
}
if res.Unverified[0].Disp != "ambiguous" {
t.Errorf("the ambiguous miss must keep disp:ambiguous, got %q", res.Unverified[0].Disp)
}
}