98 lines
5.3 KiB
Go
98 lines
5.3 KiB
Go
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)
|
||
}
|
||
}
|