mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-04 00:03:59 +00:00
feat(memory): N2a 第二块砖 + N2d 数据面 —— 轻量图记忆装配(temp 可写/主库只读/并集)与回收合入
按用户确认的形态:**独立存储实例**(不给共享记忆层加 space 列)。
## LightMemory:子的图记忆装配(设计 §5.6)
temp 实例(独立存储,读写) ← 子的一切图记忆写入落这里,与子同生共死
主库受限句柄(只读) ← 子只能读(query_only 结构性拒绝写入)
子的查询 = 两实例各查一次 + **应用层合并**(并集)
- 写入**只落 temp**(`Commit` 不接受 main 方向)
- 并集合并规则:实体按**名字**去重(同名保留 mention_count 较大者)、
关系按 (源名, 关系, 目标名) 去重;结果排序确定(便于断言与展示稳定)
- 单侧查询失败不影响另一侧(只有两侧都失败才报错)
- `AllowWrite=false`(用户给的备选简化):**没有 temp 实例**,子对图记忆完全只读,
写入被拒;读主库照常
## ExportTriples + 回收合入(设计 §9,N2d 数据面)
- `GraphDB.ExportTriples(limit)`:导出**活跃**三元组,把实体名一并带出
⇒ 合入侧直接复用 `Commit`(按实体名 upsert + 关系唯一约束)
- 回收主流程:子写 temp → 父导出 → **父选哪几条** → 写进 main。
未选中的**不进**主库;重复收割**幂等**(不产生重复实体)
## 验收(7 项新测试)
`light_memory_test.go`(5):
- 写只落 temp、查询是并集、主库无子的痕迹
- **两个子的 temp 互不可见**(只有 main 共享)
- 写禁用时完全只读(无 temp 实例、写入被拒、读照常)
- 并集去重(同名实体只出现一次)
- 合并确定性(去重 + 排序 + mention_count 取大)
`reclaim_test.go`(2):
- 导出只含活跃关系且带实体名、limit 生效
- 回收主流程(选中的进主库、未选中的不进、重复收割幂等)
全仓 go test ./... 37 包 ok / 0 FAIL。
This commit is contained in:
117
internal/memory/reclaim_test.go
Normal file
117
internal/memory/reclaim_test.go
Normal file
@ -0,0 +1,117 @@
|
||||
package memory
|
||||
|
||||
// N2d 数据面:**回收**=父读子的 temp → 选记录 → 合入 main。
|
||||
//
|
||||
// 设计 §9:回收是"取消语义"(收割后取消该驻留子),且**由父决定纳入哪些**。
|
||||
// 这里只做数据面(读得到、合得进、幂等),控制面(谁来决定、何时取消)在 N3/N6。
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExportTriples_OnlyActiveWithNames(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
g, err := NewGraphDB(filepath.Join(dir, "g.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer g.Close()
|
||||
|
||||
if _, _, err := g.Commit([]Triple{
|
||||
{Subject: "张三", Relation: "任职于", Object: "甲公司"},
|
||||
{Subject: "李四", Relation: "合作", Object: "王五"},
|
||||
}, "sess", 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := g.ExportTriples(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("导出 %d 条,期望 2:%+v", len(got), got)
|
||||
}
|
||||
// 实体名必须带出来(合入侧要靠名字 upsert,而不是内部 id)。
|
||||
seen := map[string]bool{}
|
||||
for _, tr := range got {
|
||||
if tr.Subject == "" || tr.Object == "" || tr.Relation == "" {
|
||||
t.Fatalf("导出的三元组字段不全:%+v", tr)
|
||||
}
|
||||
seen[tr.Subject+"→"+tr.Object] = true
|
||||
}
|
||||
if !seen["张三→甲公司"] || !seen["李四→王五"] {
|
||||
t.Fatalf("导出内容不对:%+v", got)
|
||||
}
|
||||
|
||||
// limit 生效。
|
||||
one, err := g.ExportTriples(1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(one) != 1 {
|
||||
t.Fatalf("limit=1 应导出 1 条,实际 %d", len(one))
|
||||
}
|
||||
}
|
||||
|
||||
// 回收主流程:子写 temp → 父导出 → 选中的合入 main;主库拿到、temp 不变。
|
||||
func TestReclaim_HarvestChildTempIntoMain(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
main, err := NewGraphDB(filepath.Join(dir, "main.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer main.Close()
|
||||
|
||||
child, err := NewLightMemory(main, filepath.Join(dir, "sub.db"), true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer child.Close()
|
||||
|
||||
// 子在 temp 里积累了两条发现。
|
||||
if _, _, err := child.Commit([]Triple{
|
||||
{Subject: "子发现A", Relation: "指向", Object: "结论1"},
|
||||
{Subject: "子发现B", Relation: "指向", Object: "结论2"},
|
||||
}, "sess", 1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 父:导出 → **选一条**("哪些纳入记忆"由父决定)→ 写进 main。
|
||||
all, err := child.Temp().ExportTriples(0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(all) != 2 {
|
||||
t.Fatalf("导出 %d 条,期望 2", len(all))
|
||||
}
|
||||
selected := []Triple{all[0]}
|
||||
if _, _, err := main.Commit(selected, "reclaim", 0); err != nil {
|
||||
t.Fatalf("合入 main 失败: %v", err)
|
||||
}
|
||||
|
||||
// 主库只拿到选中的那条。
|
||||
mainRes, err := main.Recall([]string{"子发现A", "子发现B"}, nil, 1, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !hasName(mainRes.Entities, "子发现A") {
|
||||
t.Fatalf("选中的记录应进主库:%v", names(mainRes.Entities))
|
||||
}
|
||||
if hasName(mainRes.Entities, "子发现B") {
|
||||
t.Fatalf("未选中的记录不该进主库:%v", names(mainRes.Entities))
|
||||
}
|
||||
|
||||
// 重复收割是幂等的(Commit 按实体名 upsert + 关系唯一约束)。
|
||||
before := len(mainRes.Entities)
|
||||
if _, _, err := main.Commit(selected, "reclaim", 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
after, err := main.Recall([]string{"子发现A"}, nil, 1, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(after.Entities) != before {
|
||||
t.Fatalf("重复收割不应产生重复实体:before=%d after=%d", before, len(after.Entities))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user