package memory import ( "testing" ) // 本文件是**提取留存规则闸**(extract_retention.go,design/03 §10)的验收用例集: // 正例(值得长期记 ⇒ 一条都不许被拦)+ 反例(显然不值得记 ⇒ 必须被拦成第五态) // 各远超 3 条,并逐条钉住**命中哪一条反例规则**(N1~N5)。 // // 为什么正例与反例**一样重要**:本闸判错的方向不对称 —— 漏判只是「库里多一条 // 噪声」(有 T1 与整理兜底),误判却会把一条真记忆丢进临时层(到期删除)。 // design/23 §18.3 的 fail-safe 方向是「**宁可多留,不可误弃**」,所以「正例必须 // 放行」是比「反例必须拦下」更硬的要求。 // // 「闸真在起作用」而不是「恰好如此」的证据是 TestJudgeIngestDefersLowValueItem: // 它在**同一个判定链**上既断言低价值条目拿到 `defer`+`t0_low_value`,又断言它 // **不进灰区**(于是 T1 一个 token 都花不出去);把闸摘掉时那条断言会立刻变红 // (判成 create 且灰区非空)。 // retentionPositiveCases 是正向清单(design/03 §10.4)的脱敏样本与**两道护栏** // 的样本:它们必须**全部**放行(retentionNone)。 func retentionPositiveCases() []struct { name string content string } { return []struct { name string content string }{ {"长期事实", "主人住在杭州"}, {"稳定偏好", "主人喜欢喝冰美式来提神"}, {"习惯", "主人常在深夜与助手聊天,有熬夜习惯"}, {"关系里程碑", "主人与助手约定验收暗号喵喵七号"}, {"项目约定", "迁移号约定从 0023 起"}, {"能力边界", "主人说过不要在群里提她的工作"}, {"健康约束", "主人对花生过敏"}, {"带数值的偏好", "主人每天要喝 2 杯咖啡"}, // 护栏①:显式「记住」是主人的直接指令,短句也不许被拦。 {"显式记住(短)", "记住主人周日去看牙医"}, // 护栏②:短寒暄词出现,但整句带持久信号 ⇒ 放行。 {"寒暄词+持久信号", "主人喜欢用喵打招呼"}, // 长度安全阀:寒暄词出现,但句子长到装得下事实 ⇒ 放行。 {"长句里的致谢", "谢谢你今天特地提醒我这件事,真是帮了大忙了"}, } } // retentionNegativeCases 是反例清单(design/03 §10.5)的脱敏样本,逐条钉住 // 命中的规则码。 func retentionNegativeCases() []struct { name string content string rule retentionRule } { return []struct { name string content string rule retentionRule }{ {"N1 致谢", "谢谢", retentionChatter}, {"N1 寒暄", "早上好呀", retentionChatter}, {"N1 晚安", "晚安~", retentionChatter}, {"N2 情绪", "今天好累", retentionEmotion}, {"N2 情绪(带标点)", "心情不错!", retentionEmotion}, {"N4 时效查询", "现在几点了", retentionTimelyData}, {"N4 天气", "明天天气怎么样", retentionTimelyData}, {"N3 一次性琐事", "帮我订个 7 点的闹钟", retentionOneOffAsk}, {"N3 一次性提醒", "提醒我明天把快递取回来", retentionOneOffAsk}, {"N5 一次性任务", "把这个文件转成 pdf", retentionOneOffTask}, {"N5 一次性查询", "查一下明天的航班", retentionOneOffTask}, } } // TestExtractionRetention_PositivesAreNeverDeferred 钉住正向清单一票放行: // 任何一条正例被判成 defer(= 进临时层、到期删除)都是**记忆丢失**, // 因此这里逐条断言 retentionNone。 func TestExtractionRetention_PositivesAreNeverDeferred(t *testing.T) { t.Parallel() for _, tc := range retentionPositiveCases() { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() if rule := judgeExtractionRetention(tc.content); rule != retentionNone { t.Fatalf("正例 %q 被拦成 %q —— 那是把真记忆丢进临时层(fail-safe 方向被违反)", tc.content, rule) } }) } } // TestExtractionRetention_NegativesAreDeferred 钉住反例清单:每条都必须命中 // 它该命中的规则码(而不是「反正被拦了」——规则码要能解释**为什么**)。 func TestExtractionRetention_NegativesAreDeferred(t *testing.T) { t.Parallel() for _, tc := range retentionNegativeCases() { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() if rule := judgeExtractionRetention(tc.content); rule != tc.rule { t.Fatalf("反例 %q 命中 %q, want %q", tc.content, rule, tc.rule) } }) } } // TestExtractionRetention_UpperCaseASCII 钉住 ASCII 寒暄词(中英混排): // 规范化会把 `HI` / `Thanks` 折成小写再匹配。 func TestExtractionRetention_UpperCaseASCII(t *testing.T) { t.Parallel() for _, content := range []string{"HI", "Thanks", "hello~"} { if rule := judgeExtractionRetention(content); rule != retentionChatter { t.Fatalf("%q 命中 %q, want %q(ASCII 词应先折小写)", content, rule, retentionChatter) } } } // TestJudgeIngestDefersLowValueItem 是**接线**断言:留存规则闸真的接在判定链上, // 命中的新条目拿到第五态 `defer` + 留痕原因码 `t0_low_value`,且**不进灰区** // (design/03 §10.6:规则闸拦下的不进 T1)。 func TestJudgeIngestDefersLowValueItem(t *testing.T) { t.Parallel() item := habitItem("谢谢啦", tidyTestTime(t), 0.9) // 故意给一条「相似候选」:它本该进灰区,但留存规则先命中 ⇒ 灰区必须为空, // 于是 T1 一个 token 都花不出去。 cands := []ingestCandidate{{item: habitItem("谢谢", tidyTestTime(t), 0.9), score: 0.9, mode: tidyRecallLexical}} v := judgeIngest(item, cands) if v.Decision != IngestDefer { t.Fatalf("低价值条目 decision = %q, want %q", v.Decision, IngestDefer) } if v.Reason != retentionLowValueReason { t.Fatalf("低价值条目 reason = %q, want %q", v.Reason, retentionLowValueReason) } if len(v.Gray) != 0 { t.Fatalf("低价值条目仍收集了 %d 条灰区候选 —— T1 会被白调一次(§10.6 不许)", len(v.Gray)) } } // TestJudgeIngestKeepsValuableItemAsCreate 是**变异用例**的对偶:同一条判定链上, // 值得长期留的条目仍然走既有四态(这里是 create),证明闸只拦「显然不值得记」。 func TestJudgeIngestKeepsValuableItemAsCreate(t *testing.T) { t.Parallel() item := habitItem(fixtureHabitShort, tidyTestTime(t), 0.9) v := judgeIngest(item, nil) if v.Decision != IngestCreate { t.Fatalf("正常条目 decision = %q, want %q(闸不许误伤)", v.Decision, IngestCreate) } }