package memory import ( "context" "testing" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/embedding" ) // 本文件是「轮末自动落地」测试的**共用夹具**:正文夹具常量、整理器构造(带审计捕获)、 // 自动档审计行筛选、记忆行断言辅助,以及手工落候选的构造器。两个用例文件 // (tidy_auto_test.go 与 tidy_auto_reclassify_test.go)共用它们 —— 拆文件只为守 // AGENTS §10「文件 ≤500 行」,用例与断言逐字未变。 const ( // fixtureHabitClock 是「被钟点误判」的典型:正文里带绝对时刻(23:30)⇒ 规则层 // 会把它当一次性事件产 archive 候选,但它明明白白是**作息习惯**(「常在」「习惯」)。 fixtureHabitClock = "主人常在深夜23:30后还醒着,有熬夜习惯" // fixtureDupText 是字面重复用例的正文(两侧逐字相同)。 fixtureDupText = "主人喜欢在深夜喝冰美式" // fixtureEquivA / fixtureEquivB 是**事实集合等价但不同措辞**的一对 // (关键要素逐一互相包含 ⇒ containBoth,正文不同 ⇒ 不是字面重复)。 fixtureEquivA = "主人喜欢喝红牛" fixtureEquivB = "红牛,主人喜欢喝" ) // newAutoTestTidier 构建整理器并把审计出口捕获下来(自动档的审计是断言面之一)。 func newAutoTestTidier(t *testing.T, store *Store, cfg TidyConfig, emb embedding.Embedder) (*Tidier, *[]tidyAuditRow) { t.Helper() rows := make([]tidyAuditRow, 0) tr, err := NewTidier(cfg, TidyDeps{ Store: store, Embedder: emb, Log: testLogger(), Audit: func(who, what, result string) { rows = append(rows, tidyAuditRow{who: who, what: what, result: result}) }, }) if err != nil { t.Fatalf("NewTidier: %v", err) } return tr, &rows } // autoAuditRows 返回捕获到的 memory.tidy.apply(自动档)审计行。 func autoAuditRows(rows *[]tidyAuditRow) []tidyAuditRow { out := make([]tidyAuditRow, 0) for _, r := range *rows { if r.what == tidyAutoApplyVerb { out = append(out, r) } } return out } // countArchived 数给定 id 里已归档的行数。 func countArchived(t *testing.T, store *Store, ids []string) int { t.Helper() n := 0 for _, id := range ids { if loadTidyItemRow(t, store, id).status == tidyStatusArchived { n++ } } return n } // autoReclassifyCandidate 造一条 reclassify 候选(形状 = 规则层产出的那种: // 恰 1 个 source、无 target、理由 kind_mismatch、KindA = 该行**当前**的 kind)。 // // strayContent 是**故意**留给畸形用例的口子:规则层产出的 reclassify 候选 // ProposedContent 恒为空(buildCandidates 把它置空),手工带上它就是「候选想改正文」 // 的畸形行 —— 自动档只许采纳它的分类,正文一个字都不许采纳。 func autoReclassifyCandidate(item MemoryItem, proposed Kind, strayContent string, at time.Time) TidyCandidate { return TidyCandidate{ ID: mustNewTidyID(), PersonaID: item.PersonaID, Scope: item.Scope, ProjectID: item.ProjectID, Kind: item.Kind, Action: TidyReclassify, SourceIDs: []string{item.ID}, ProposedContent: strayContent, Reason: tidyReasonKindMismatch, Evidence: TidyEvidence{ KindA: string(item.Kind), ProposedKind: string(proposed), ContentA: item.Content, }, RuleVersion: TidyRuleVersion, Status: tidyCandidatePending, CreatedAt: at, } } // insertAutoCandidateOrFail 落一条手工候选:夹具不成立(没落进去)就该当场失败, // 否则后面的断言会验一个不存在的场景。 func insertAutoCandidateOrFail(t *testing.T, store *Store, c TidyCandidate) { t.Helper() if created, err := store.insertTidyCandidate(context.Background(), c); err != nil || !created { t.Fatalf("落候选 %s = (%t, %v), want (true, nil)", c.ID, created, err) } } // seedWorldEventItem 落一条 world_event 记忆(reclassify 用例的被改对象)。 func seedWorldEventItem(t *testing.T, store *Store, content string, at time.Time) MemoryItem { t.Helper() return seedTidyItem(t, store, MemoryItem{ PersonaID: "persona.a", Scope: ScopePersona, Kind: KindWorldEvent, Content: content, Confidence: 0.9, CreatedAt: at, SourceConversation: FormatSourceConversation("web", "msg-1"), }) } // autoArchiveCandidate 造一条一次性事件的归档候选(形状 = 规则层产出的那种: // 理由 one_off_event、ProposedContent = 原文、证据 Containment=none)。 func autoArchiveCandidate(item MemoryItem, at time.Time) TidyCandidate { return TidyCandidate{ ID: mustNewTidyID(), PersonaID: item.PersonaID, Scope: item.Scope, ProjectID: item.ProjectID, Kind: item.Kind, Action: TidyArchive, SourceIDs: []string{item.ID}, ProposedContent: item.Content, Reason: tidyReasonOneOffEvent, Evidence: TidyEvidence{Containment: string(containNone), ContentA: item.Content}, RuleVersion: TidyRuleVersion, Status: tidyCandidatePending, CreatedAt: at, } }