package memory import ( "context" "strings" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // 本文件钉住**提取器侧的敏感判据**(切片 M63、审计 §7 P0-2): // // 1. prompt 里真的给了判据与正反例(不是「交给模型自由发挥」); // 2. **dry-run 私事不落 `scope=user & sensitive=false`** —— 即模型漏标时,产出侧 // 仍按白名单补上敏感位。这是审计 P0-2 的验收原话,也是最容易复发的一条: // 模型的标注是概率性的,白名单是确定性的。 // 3. 模型显式标 true 的非白名单内容不被撤标。 // // 删掉实现(prompt 里的敏感位、resolveSensitive 在 dedupeAndBuild 里的调用)会让 // 2 直接变红。 // extractOne 跑一次 Extract,返回第一条候选(没有候选即失败)。 func extractOne(t *testing.T, reply string) MemoryItem { t.Helper() adapter := &mockAdapter{fn: func(_ context.Context, _ model.Request, _ model.Options) (model.Response, error) { return model.Response{Content: reply}, nil }} ex := NewLLMExtractor(adapter, "persona.a", "role.1") items, err := ex.Extract(context.Background(), []model.Message{ model.TextMessage(model.RoleUser, "我们聊聊住哪儿"), }) if err != nil { t.Fatalf("Extract: %v", err) } if len(items) == 0 { t.Fatal("Extract 没有产出候选") } return items[0] } // TestExtractPrompt_带敏感位判据与正反例 钉住 prompt 本体:敏感位的键名、判据、 // 正反例(住址 = 私事 / 口味 = 习惯)必须都在。这三样少任何一样,模型就只能瞎猜。 func TestExtractPrompt_带敏感位判据与正反例(t *testing.T) { t.Parallel() for _, want := range []string{ `"sensitive"`, "住址", "私人称呼", "「用户喜欢冰美式」→ sensitive=false", "「主人住在花溪」→ sensitive=true", "拿不准就标 true", } { if !strings.Contains(extractSystemPrompt, want) { t.Fatalf("extractSystemPrompt 缺少 %q —— 提取器没有敏感判据,模型只能把私事当习惯", want) } } } // TestExtract_私事漏标仍落敏感 是审计 P0-2 的验收:模型(这里是脚本化的回复) // 把「主人住在花溪」标成 scope=user 且**没有**给 sensitive 位 —— 产出侧必须补上 // 敏感位,绝不吐出 `user & sensitive=false` 的候选。 func TestExtract_私事漏标仍落敏感(t *testing.T) { t.Parallel() got := extractOne(t, `[{"content":"主人住在花溪(贵阳花溪区),不在上海工作","scope":"user","kind":"world_event","confidence":0.9}]`) if got.Scope != ScopeUser { t.Fatalf("scope = %q, want user(本用例考的正是 user 域里的私事)", got.Scope) } if !got.Sensitive { t.Fatal("产出侧漏了敏感位:候选是 scope=user & sensitive=false,正是审计 P0-2 的复发点") } } // TestExtract_HabitStaysShared 是方向性护栏:习惯类内容在模型标 false(或缺省)时 // 必须保持非敏感 —— 否则本片就把已冻结的「用户习惯可查」(design/03 §5)推翻了。 func TestExtract_HabitStaysShared(t *testing.T) { t.Parallel() got := extractOne(t, `[{"content":"用户喜欢冰美式","scope":"user","kind":"relationship","sensitive":false}]`) if got.Sensitive { t.Fatal("习惯类候选被标成敏感 —— 受限车道会平白丢掉合法习惯") } } // TestExtract_模型显式敏感不被撤标 钉住方向性:模型标 true 的非白名单内容 // (例如家人情况,措辞不在白名单里)必须保持 true。 func TestExtract_模型显式敏感不被撤标(t *testing.T) { t.Parallel() got := extractOne(t, `[{"content":"主人有个弟弟在读高三","scope":"user","kind":"relationship","sensitive":true}]`) if !got.Sensitive { t.Fatal("模型显式标 sensitive=true 被改写成 false —— 产出侧不许撤标") } }