package memory import ( "testing" "time" ) // 本文件是 `dedupe_group` **中文数字门**(r2 复审 P2)的验收用例集: // 中位数写法下的数值冲突(一小时 vs 十五分钟、第二杯 vs 一杯)、单位不可换算的 // 保守拒绝、同值照常合并,以及中文数字解析口径本身。 // TestDedupeGroupRejectsConflictingChineseNumbers 是 r2 复审 **P2 的回归用例**: // 语料里的数字大多写成**中文**,而 `sameNumbers` 只抽 ASCII 数字 token ⇒ // 「午睡一小时 vs 午睡十五分钟」「咖啡不续第二杯 vs 不超过一杯」这类**数值冲突** // 曾被当成同一事实合并掉。 // // 旧逻辑(只有 sameNumbers)下这些对会成簇 ⇒ 用例红;补中文数字门后不成簇 ⇒ 绿。 func TestDedupeGroupRejectsConflictingChineseNumbers(t *testing.T) { t.Parallel() base := tidyTestTime(t) cases := []struct { name string survivor string member string tagsA []string tagsB []string whyItReds string }{ { name: "午睡时长(一小时 vs 十五分钟)", survivor: "用户有午睡习惯,与助手约定午睡一小时、到点叫醒。", member: "主人与人家约定:午睡十五分钟,由人家守时看着时间把她叫醒。", tagsA: []string{"作息", "午睡"}, tagsB: []string{"作息", "午睡"}, }, { name: "咖啡杯数(第二杯 vs 一杯)", survivor: "用户被约定:一天咖啡不许续第二杯,下午犯困时起身走动、喝温水。", member: "用户有喝咖啡的习惯,助手提醒她每天不超过一杯(第二杯不许续)。", tagsA: []string{"习惯", "咖啡"}, tagsB: []string{"习惯", "咖啡"}, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() a := dedupeTestItem(tc.survivor, tc.tagsA, base) b := dedupeTestItem(tc.member, tc.tagsB, base.Add(time.Minute)) // 先钉住「这不是因为别的门挡住的」:数值门确实报出冲突。 if gaps := dedupeNumberConflicts(b.Content, a.Content); len(gaps) == 0 { t.Fatalf("中文数字门没报出冲突:%q vs %q", b.Content, a.Content) } if cands, _ := dedupeCandsOf(t, newTestStore(t), []MemoryItem{a, b}); len(cands) != 0 { t.Fatalf("簇数 = %d, want 0(数值冲突=不同事实,§5.2-2)", len(cands)) } }) } } // TestDedupeGroupRejectsNonConvertibleUnits 钉住「单位换算不到一起 ⇒ 宁可不合并」 // (r2 复审的明确口径):两杯 vs 三瓶是同一个计数量的不同取值,但杯/瓶不能互换。 func TestDedupeGroupRejectsNonConvertibleUnits(t *testing.T) { t.Parallel() base := tidyTestTime(t) tags := []string{"习惯", "饮水"} a := dedupeTestItem("主人每天要喝两杯咖啡,下午不续。", tags, base) b := dedupeTestItem("主人每天要喝三瓶水,下午也要喝。", tags, base.Add(time.Minute)) if gaps := dedupeNumberConflicts(b.Content, a.Content); len(gaps) == 0 { t.Fatalf("单位不可换算没有报冲突:%q vs %q", b.Content, a.Content) } if cands, _ := dedupeCandsOf(t, newTestStore(t), []MemoryItem{a, b}); len(cands) != 0 { t.Fatalf("簇数 = %d, want 0(单位不可换算 ⇒ 保守不合并)", len(cands)) } } // TestDedupeGroupSameChineseNumberStillMerges 是中文数字门的**反向护栏**: // 值相等(含不同单位换算后相等)照常成簇 —— 门收的是「冲突」,不是「出现数字」。 func TestDedupeGroupSameChineseNumberStillMerges(t *testing.T) { t.Parallel() base := tidyTestTime(t) tags := []string{"作息", "午睡"} a := dedupeTestItem("主人午睡一小时,午后精神好。", tags, base) b := dedupeTestItem("主人午睡一小时,下午很清醒。", tags, base.Add(time.Minute)) // 一条写「一个小时」、另一条写「一小时」:换算后同为 3600 秒,属于同一事实。 c := dedupeTestItem("主人午睡一个小时,午后精神不错。", tags, base.Add(2*time.Minute)) cands, _ := dedupeCandsOf(t, newTestStore(t), []MemoryItem{a, b, c}) if len(cands) != 1 { t.Fatalf("簇数 = %d, want 1(同值照常合并 —— 门不许退化成「有中文数字就不合」)", len(cands)) } } // TestTidyQuantitiesChineseNumerals 钉住中文数字的解析口径(含单位换算与习语排除)。 func TestTidyQuantitiesChineseNumerals(t *testing.T) { t.Parallel() cases := []struct { text string value float64 unit string dim string ok bool }{ {"主人午睡一小时", 3600, "小时", tidyDimTime, true}, {"主人午睡十五分钟", 900, "分钟", tidyDimTime, true}, {"主人午睡半个小时", 1800, "小时", tidyDimTime, true}, {"一万两千行的验收纸条", 12000, "行", tidyDimText, true}, {"每天要喝两杯咖啡", 2, "杯", tidyDimCount, true}, // 序数与习语:不是数量。 {"第二天再说", 0, "", "", false}, {"一起散步", 0, "", "", false}, {"一直加班", 0, "", "", false}, } for _, tc := range cases { got := tidyQuantities(tc.text) if !tc.ok { if len(got) != 0 { t.Fatalf("%q 应不产生数量,得到 %+v", tc.text, got) } continue } if len(got) == 0 { t.Fatalf("%q 没解析出数量", tc.text) } if got[0].value != tc.value || got[0].unit != tc.unit || got[0].dim != tc.dim { t.Fatalf("%q 解析为 %+v, want value=%v unit=%s dim=%s", tc.text, got[0], tc.value, tc.unit, tc.dim) } } }