package memory import ( "strings" "testing" "time" ) // 本文件是 `dedupe_group` **取值门**(r1 复审 P1)的验收用例集: // 同属性不同取值(`「老K」` vs `「阿七」`)、拉丁取值、以及簇级的「取值不丢」不变式。 // 分簇/候选构造的用例在 tidy_dedupe_test.go,中文数字门在 tidy_dedupe_numbers_test.go。 // TestDedupeGroupRejectsSameTagDifferentValue 是 r1 复审 **P1-1 的回归用例**: // 同 tag 集、但**取值不同**(`「老K」` vs `「阿七」`)的两条**不许同簇**。 // // 语料原型(脱敏):生产库里 `用户的昵称是「叶酱」。` 曾被并进 // `主人的昵称是「喵喵七号」…` 的存活行,而同库另有 4 行明确写着「旧的称呼 // 『喵喵七号』已作废,不要再使用」—— 一次 apply 就会让当前事实消失,并留下与库内 // 其它行直接矛盾的结论。因此这条用例钉的不是「少减一点」,而是「不许毁数据」。 func TestDedupeGroupRejectsSameTagDifferentValue(t *testing.T) { t.Parallel() base := tidyTestTime(t) tags := []string{"昵称", "称呼"} // 取值不同:只剩存活行一条 ⇒ 不成簇、一条候选都不产。 survivor := dedupeTestItem("主人的昵称是「老K」,是相处中慢慢形成的亲昵称呼;具体来历不明,不可编造。", tags, base) staleA := dedupeTestItem("用户的昵称是「阿七」。", tags, base.Add(time.Minute)) staleB := dedupeTestItem("主人被称呼为「阿七」,助手平时就这么叫她。", tags, base.Add(2*time.Minute)) cands, _ := dedupeCandsOf(t, newTestStore(t), []MemoryItem{survivor, staleA, staleB}) if len(cands) != 0 { t.Fatalf("簇数 = %d, want 0(取值不同=不同事实,§5.2-4/§5.2-5)", len(cands)) } for _, m := range []MemoryItem{staleA, staleB} { if gaps := dedupeValueGaps(m.Content, survivor.Content); len(gaps) == 0 { t.Fatalf("取值门没认出 %q 里的取值与存活行不同", m.Content) } } // 对照组:同样的 tag 集 + **相同取值**,必须照常成簇(证明上面那条不是因为门太宽)。 sameValue := dedupeTestItem("主人被称呼为「老K」,助手平时就这么叫她。", tags, base.Add(3*time.Minute)) cands, _ = dedupeCandsOf(t, newTestStore(t), []MemoryItem{survivor, sameValue}) if len(cands) != 1 { t.Fatalf("对照组簇数 = %d, want 1(同 tag 同取值必须成簇)", len(cands)) } if cands[0].TargetID != survivor.ID { t.Fatalf("对照组存活行 = %s, want 最长的那条 %s", cands[0].TargetID, survivor.ID) } } // TestDedupeGroupLatinTokenIsAlsoAValue 钉住取值门的第二类取值:**拉丁 token** // (人名 / 型号 / 代号)。`助手名为 Cyrene` 与 `助手名为 Alice` 同 tag 集,但取值不同。 func TestDedupeGroupLatinTokenIsAlsoAValue(t *testing.T) { t.Parallel() base := tidyTestTime(t) tags := []string{"身份", "名字"} keep := dedupeTestItem("助手名为 Cyrene,主人习惯这么叫我。", tags, base) other := dedupeTestItem("助手名为 Alice,主人习惯这么叫我。", tags, base.Add(time.Minute)) if cands, _ := dedupeCandsOf(t, newTestStore(t), []MemoryItem{keep, other}); len(cands) != 0 { t.Fatalf("簇数 = %d, want 0(拉丁取值不同=不同事实)", len(cands)) } } // TestDedupeGroupNeverLosesAValue 是**簇级不变式**:任何产出的候选,被吸收行里的 // 每个取值(引号取值 + 拉丁 token)都必须出现在存活行正文里 —— 否则那条取值会随 // 合并从 active 集合里消失。它比单点用例强:新增的簇判据 / 门顺序变化都要过它。 func TestDedupeGroupNeverLosesAValue(t *testing.T) { t.Parallel() base := tidyTestTime(t) tags := []string{"暗号", "约定"} items := []MemoryItem{ dedupeTestItem("用户与助手的验收暗号是「青柠」", tags, base), dedupeTestItem("用户与助手的验收暗号是「青柠」,用于确认文档摄取正常", tags, base.Add(time.Minute)), dedupeTestItem("验收暗号「青柠」用于确认文档摄取功能正常", tags, base.Add(2*time.Minute)), // 同一 tag 集但取值不同(不许进簇)。 dedupeTestItem("用户与助手的验收暗号是「柠檬」", tags, base.Add(3*time.Minute)), // 同 tag 集、取值包含了额外信息(不许进簇:那条取值会丢)。 dedupeTestItem("验收暗号「青柠」配合口令「开门」一起使用", tags, base.Add(4*time.Minute)), } cands, _ := dedupeCandsOf(t, newTestStore(t), items) if len(cands) != 1 { t.Fatalf("簇数 = %d, want 1", len(cands)) } survivor := cands[0].MergedContent for _, member := range cands[0].Evidence.DedupeMembers { for _, value := range dedupeValues(member) { if !strings.Contains(dedupeKey(survivor), value) { t.Fatalf("取值 %q 会随合并消失(member=%q survivor=%q)", value, member, survivor) } } } for _, member := range cands[0].Evidence.DedupeMembers { if strings.Contains(member, "柠檬") || strings.Contains(member, "开门") { t.Fatalf("不该被吸收的成员进了簇:%q", member) } } }