复审 p2-1items(tip cc6904b)要求的三处,逐条:
1) SQL 双引号改单引号(唯一返工项):
- preinsert_store.go 的 revertMergedSQL:`merged_into = ""` → `''`。今天功能等价,
但靠的是 SQLite「双引号标识符解析不到 ⇒ 回退成字符串字面量」的 legacy 行为;
这是一条**改数据**的语句,还被 RevertMerged 与整理侧回滚共用,不该依赖回退。
- retention_migration_test.go 的 `WHERE retention_tier = ""` 一并统一成 `''`,
并在原位留下「为什么」。
2) 两处过期注释:
- retention_schema.go:「反过来(先自举、后迁移)也成立」是**假结论**,改为
「生产顺序成立;反向顺序在 0019 处即失败(duplicate column name: status,既有问题)」。
- store.go 的 ExpiresAt:「本字段目前只写不读」已半过期(Add 现在会把最终落库
时刻回填到返回值),改成「读回语义是半开的」:Add 返回的条目有值,从库里读回的
恒为零值。
3) 人工路径(塔裁定「人工动作默认应为 long」):包内的 `RememberManual`(「记住 X」)
显式钉 `RetentionTier = long`,留存规则因此不许把人工明确要求记住的东西降成临时层
(extract_test.go 加断言)。面板网关那条(adminAddMemory 直调 Add、不传档位)
按塔的口径不在本片、留给后续切片 —— 本片对它的唯一影响见回报。
不动(按塔的裁定):preinsert.go 514 行(dev 既有 tech debt)、
design/03 §10.5 与 design/23 §18 的章节(塔的账)、gateway 人工路径。
回归:go build ./... + go vet ./... + go test -count=1 ./internal/runtime/memory/... ./internal/db/...
全绿,内存包 -race 亦绿。
115 lines
3.8 KiB
Go
115 lines
3.8 KiB
Go
package memory
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
|
)
|
|
|
|
func TestManualExtractor_Extract_ParseRemember_正常(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ex := NewManualExtractor("persona.a", "role.1")
|
|
conversation := []model.Message{
|
|
model.TextMessage(model.RoleUser, "记住她喜欢喝冰美式"),
|
|
model.TextMessage(model.RoleAssistant, "好的,我记住了"),
|
|
model.TextMessage(model.RoleUser, "今天天气不错"), // 无指令
|
|
model.TextMessage(model.RoleUser, "请记住周三下午有项目会"),
|
|
}
|
|
|
|
items, err := ex.Extract(context.Background(), conversation)
|
|
if err != nil {
|
|
t.Fatalf("Extract: %v", err)
|
|
}
|
|
if len(items) != 2 {
|
|
t.Fatalf("got %d items, want 2", len(items))
|
|
}
|
|
if items[0].Content != "她喜欢喝冰美式" {
|
|
t.Errorf("items[0].Content = %q, want the stripped directive", items[0].Content)
|
|
}
|
|
if items[0].PersonaID != "persona.a" || items[0].RoleID != "role.1" {
|
|
t.Errorf("items[0] persona/role = %q/%q, want persona.a/role.1", items[0].PersonaID, items[0].RoleID)
|
|
}
|
|
if items[1].Content != "周三下午有项目会" {
|
|
t.Errorf("items[1].Content = %q, want the 请记住 content", items[1].Content)
|
|
}
|
|
}
|
|
|
|
func TestManualExtractor_Extract_NoRemember_正常(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ex := NewManualExtractor("persona.a", "role.1")
|
|
conversation := []model.Message{
|
|
model.TextMessage(model.RoleUser, "今天天气不错"),
|
|
model.TextMessage(model.RoleUser, "记得带伞吗"),
|
|
model.TextMessage(model.RoleAssistant, "记住是记忆"), // 非用户角色跳过
|
|
}
|
|
|
|
items, err := ex.Extract(context.Background(), conversation)
|
|
if err != nil {
|
|
t.Fatalf("Extract: %v", err)
|
|
}
|
|
if len(items) != 0 {
|
|
t.Fatalf("got %d items, want 0", len(items))
|
|
}
|
|
}
|
|
|
|
func TestManualExtractor_Extract_Canceled_异常(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ex := NewManualExtractor("persona.a", "role.1")
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
if _, err := ex.Extract(ctx, []model.Message{model.TextMessage(model.RoleUser, "记住x")}); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("Extract with canceled ctx err = %v, want context.Canceled", err)
|
|
}
|
|
}
|
|
|
|
func TestStore_RememberManual_正常(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
item, err := s.RememberManual(ctx, "persona.a", "role.1", "记住她喜欢喝咖啡")
|
|
if err != nil {
|
|
t.Fatalf("RememberManual: %v", err)
|
|
}
|
|
// 落库内容保留指令原文;这是手动单条目
|
|
// 路径,与会剥离指令的提取器不同。
|
|
if item.Content != "记住她喜欢喝咖啡" {
|
|
t.Fatalf("RememberManual content = %q, want the passed text", item.Content)
|
|
}
|
|
if item.PersonaID != "persona.a" || item.Scope != ScopePersona || item.Kind != KindRelationship {
|
|
t.Fatalf("item persona/scope/kind = %q/%q/%q, want persona.a/persona/relationship",
|
|
item.PersonaID, item.Scope, item.Kind)
|
|
}
|
|
// 人工动作(有人明确说「记住 X」)的档位恒为 long —— 留存规则不许把它降成临时层
|
|
// (否则一条「记住 晚安」会在 7 天后被静默删掉)。见 store.go 的 RememberManual 注释。
|
|
if item.RetentionTier != RetentionLong {
|
|
t.Fatalf("RememberManual 的档位 = %q, want %q(人工动作默认长期)", item.RetentionTier, RetentionLong)
|
|
}
|
|
|
|
got, err := s.Get(ctx, item.ID)
|
|
if err != nil {
|
|
t.Fatalf("Get: %v", err)
|
|
}
|
|
if got.Content != item.Content {
|
|
t.Fatalf("persisted content = %q, want %q", got.Content, item.Content)
|
|
}
|
|
}
|
|
|
|
func TestStore_RememberManual_Empty_异常(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
s := newTestStore(t)
|
|
|
|
for _, text := range []string{"", " "} {
|
|
if _, err := s.RememberManual(ctx, "persona.a", "role.1", text); !errors.Is(err, ErrMissingContent) {
|
|
t.Fatalf("RememberManual(%q) err = %v, want ErrMissingContent", text, err)
|
|
}
|
|
}
|
|
}
|