feat(memory): 记忆整理 v1(design/23)——低峰定期扫描 + 事实包含判定 + 规则否决 + 只产建议队列(不自动应用)
This commit is contained in:
1 parent
012e9f3ad4
commit
91a3209c9b
25 files changed
+3418
-13
No files matched your search
@@ -0,0 +1,169 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 本文件逐条锁定 design/23 §5.1/§5.2 的内容判据(包含、时效、数值、来源),
|
||||
// 因为它们是「该合 / 不该合」的全部依据 —— 判据错了,整理就会丢信息。
|
||||
|
||||
// TestJudgeContainment子集与并列 覆盖 design/23 §1.3 的核心区分:
|
||||
// 子集(同一事实的碎片)判为包含;并列事实(红牛 vs 冰美式)判为互不包含。
|
||||
func TestJudgeContainment子集与并列(t *testing.T) {
|
||||
t.Parallel()
|
||||
if c := judgeContainment(fixtureHabitShort, fixtureHabitLong); c.Kind != containASubsetB {
|
||||
t.Fatalf("碎片 ⊂ 完整 = %q, want %q", c.Kind, containASubsetB)
|
||||
}
|
||||
if c := judgeContainment(fixtureHabitLong, fixtureHabitShort); c.Kind != containBSubsetA {
|
||||
t.Fatalf("反向包含 = %q, want %q", c.Kind, containBSubsetA)
|
||||
}
|
||||
if c := judgeContainment(fixtureHabitShort, fixtureHabitShort); c.Kind != containBoth {
|
||||
t.Fatalf("同一内容 = %q, want %q", c.Kind, containBoth)
|
||||
}
|
||||
if c := judgeContainment(fixtureEnergyRedBull, fixtureEnergyIced); c.Kind != containNone {
|
||||
t.Fatalf("并列偏好 = %q, want %q(相似不等于包含)", c.Kind, containNone)
|
||||
}
|
||||
if len(judgeContainment(fixtureEnergyRedBull, fixtureEnergyIced).Missing) == 0 {
|
||||
t.Fatal("互不包含时必须给出缺失要素证据(diff 预览要用)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestJudgeContainmentEmoji要素 覆盖 design/23 §1.3 的 emoji 判据:emoji 是
|
||||
// 关键要素,因此「多一个 emoji」的一方不可能是子集。
|
||||
//
|
||||
// ⚠️ 与 design/23 §12.1 P4 的差异如实记录在这里:P4 把「[9] 有 🎁、[24] 无」
|
||||
// 期望为 `link`(非包含)。但按设计稿 §1.3 的核心规则(A ⊆ B 才并入),
|
||||
// **少 🎁 的那条确实是子集** —— 它的每一个关键要素都在对方里,合并不会丢信息。
|
||||
// 本实现跟随核心规则(产 merge 建议,且第一版全部交人工确认、不自动应用),
|
||||
// 并把「各有一个对方没有的 emoji」([4] 的 🎁 vs [20] 的 👀)判为互不包含 ⇒ link。
|
||||
// 该不一致已在交付报告里列为「要登记的契约/设计变更点」。
|
||||
func TestJudgeContainmentEmoji要素(t *testing.T) {
|
||||
t.Parallel()
|
||||
withGift := "主人送过助手礼物🎁,助手很开心"
|
||||
withoutGift := "主人送过助手礼物,助手很开心"
|
||||
if c := judgeContainment(withGift, withoutGift); c.Kind != containBSubsetA {
|
||||
t.Fatalf("含 🎁 的一方不是子集,无 🎁 的一方是:got %q, want %q", c.Kind, containBSubsetA)
|
||||
}
|
||||
if c := judgeContainment(withoutGift, withGift); c.Kind != containASubsetB {
|
||||
t.Fatalf("少 🎁 的一方是子集:got %q, want %q", c.Kind, containASubsetB)
|
||||
}
|
||||
if c := judgeContainment("主人送过助手礼物🎁", "主人送过助手礼物👀"); c.Kind != containNone {
|
||||
t.Fatalf("各有一个对方没有的 emoji ⇒ 互不包含,got %q", c.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHasAbsoluteTimestamp 覆盖 design/23 §5.1 T0-5:绝对日期/时刻是一次性
|
||||
// 事件的信号,而「22点后 / 十点多」这类周期性表述不是。
|
||||
func TestHasAbsoluteTimestamp(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
content string
|
||||
want bool
|
||||
}{
|
||||
{fixtureOneOffEvent, true},
|
||||
{"2026-09-08 的晚上", true},
|
||||
{"国庆黄金周 10/3 假期第三天", true},
|
||||
{"9月8日那晚聊到很晚", true},
|
||||
{"周二晚上他还在加班", true},
|
||||
{"主人常在晚上22点后保持活跃", false},
|
||||
{"主人常熬夜到十点多", false},
|
||||
{"每周二晚上一起看剧", false},
|
||||
{fixtureHabitLong, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := hasAbsoluteTimestamp(tc.content); got != tc.want {
|
||||
t.Errorf("hasAbsoluteTimestamp(%q) = %v, want %v", tc.content, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHasTemporalState 覆盖 design/23 §5.2-1:有期限的状态词(假期/本周…)
|
||||
// 命中即否决合并。
|
||||
func TestHasTemporalState(t *testing.T) {
|
||||
t.Parallel()
|
||||
for _, content := range []string{
|
||||
fixtureHabitVacation, "国庆假期期间处于休假状态", "本周在赶项目", "这几天暂时没空",
|
||||
} {
|
||||
if !hasTemporalState(content) {
|
||||
t.Errorf("hasTemporalState(%q) = false, want true", content)
|
||||
}
|
||||
}
|
||||
for _, content := range []string{
|
||||
fixtureHabitLong, fixtureEnergyRedBull, "主人喜欢喝冰美式",
|
||||
} {
|
||||
if hasTemporalState(content) {
|
||||
t.Errorf("hasTemporalState(%q) = true, want false", content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSameNumbers 覆盖 design/23 §5.2-2:数值不同即不同事实。
|
||||
func TestSameNumbers(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !sameNumbers("服务跑在 8080 端口", "服务跑在 8080 端口上") {
|
||||
t.Fatal("同一数值应视为一致")
|
||||
}
|
||||
if sameNumbers("服务跑在 8080 端口", "服务跑在 9090 端口") {
|
||||
t.Fatal("数值不同必须判定不一致")
|
||||
}
|
||||
if sameNumbers("主人常在深夜加班", fixtureHabitLong) == false {
|
||||
t.Fatal("两边都无数值时视为一致(无数值可比)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNonOwnerSource 覆盖 design/23 §5.2-9/§6:群聊/陌生人来源只读不写,
|
||||
// 而本进程的系统会话(backthink / 整理自身)与主人链路必须放行。
|
||||
func TestNonOwnerSource(t *testing.T) {
|
||||
t.Parallel()
|
||||
cases := []struct {
|
||||
source string
|
||||
want bool
|
||||
}{
|
||||
{FormatSourceConversation("sess-telegram:group1", "m1"), true},
|
||||
{FormatSourceConversation("sess-kook:3bef", ""), true},
|
||||
{FormatSourceConversation(backthinkSessionID, "m2"), false},
|
||||
{FormatSourceConversation(TidySessionID, ""), false},
|
||||
{FormatSourceConversation("web", "m3"), false},
|
||||
{FormatSourceConversation("web:sub-1", ""), false},
|
||||
{"主会话 2026-09-08 深夜", false}, // 裁决前的 LLM 自由文本:无法机器判定,放行
|
||||
{"", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := nonOwnerSource(tc.source); got != tc.want {
|
||||
t.Errorf("nonOwnerSource(%q) = %v, want %v", tc.source, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestExtractTidyFacts 锁定事实要素抽取的两个关键形状:时刻是**一个**数值
|
||||
// token(22:38),emoji 是词项。
|
||||
func TestExtractTidyFacts(t *testing.T) {
|
||||
t.Parallel()
|
||||
facts := extractTidyFacts("2026年9月8日周二晚22:38,主人送了我🎁")
|
||||
for _, want := range []string{"22:38", "2026", "9"} {
|
||||
if _, ok := facts.nums[want]; !ok {
|
||||
t.Errorf("数值要素缺 %q:%v", want, facts.nums)
|
||||
}
|
||||
}
|
||||
if _, ok := facts.terms["🎁"]; !ok {
|
||||
t.Errorf("emoji 应是词项:%v", facts.terms)
|
||||
}
|
||||
if _, ok := facts.terms["熬夜"]; ok {
|
||||
t.Errorf("不存在的词项不该出现:%v", facts.terms)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTidyNegationConflictVetoed 覆盖 design/23 §5.2-3:X 与非X 是覆盖关系,
|
||||
// 不是重复,即使事实集合看起来包含也必须否决合并。
|
||||
func TestTidyNegationConflictVetoed(t *testing.T) {
|
||||
t.Parallel()
|
||||
before := habitItem("主人喜欢喝红牛提神", tidyTestTime(t), 0.9)
|
||||
after := habitItem("主人不喜欢喝红牛提神", tidyTestTime(t), 0.9)
|
||||
verdict := judgePair(before, after, 0.98)
|
||||
if verdict.Veto != VetoConflict {
|
||||
t.Fatalf("冲突对 veto = %q, want %q", verdict.Veto, VetoConflict)
|
||||
}
|
||||
if verdict.Action != "" {
|
||||
t.Fatalf("冲突对不得带动作,got %q(冲突是覆盖,要走人工)", verdict.Action)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user