package db import "testing" // mustIntent / mustNode / mustLink are terse builders for the delete-cascade tests. func mustIntent(t *testing.T, es *ExplorationStore, summary string) int64 { t.Helper() id, err := es.AddIntent(map[string]any{"summary": summary}, 1, nil, "planner") if err != nil { t.Fatal(err) } return id } func mustNode(t *testing.T, es *ExplorationStore, kind, summary string) int64 { t.Helper() id, err := es.AddNode(kind, map[string]any{"summary": summary}, 1, "confirmed", "worker", nil) if err != nil { t.Fatal(err) } return id } func mustLink(t *testing.T, es *ExplorationStore, from int64, rel string, to int64) { t.Helper() if err := es.Link(from, rel, to); err != nil { t.Fatal(err) } } func gone(t *testing.T, es *ExplorationStore, id int64) bool { t.Helper() n, err := es.GetNode(id) if err != nil { t.Fatal(err) } return n == nil } // TestSoftDeleteIntent 假删除置 deleted + delete_reason,保留节点。 func TestSoftDeleteIntent(t *testing.T) { d, err := Open(testDSN(t)) if err != nil { t.Skipf("postgres unavailable (%v) — skipping", err) } defer d.Close() expID, err := d.CreateExploration("soft delete", "假删除") if err != nil { t.Fatal(err) } defer d.Exec(`DELETE FROM explorations WHERE id=$1`, expID) es := d.Exploration(expID) intent := mustIntent(t, es, "待删意图") if err := es.SetIntentState(intent, "paused"); err != nil { t.Fatal(err) } summary, err := es.SoftDeleteIntent(intent, "方向判断错误") if err != nil { t.Fatal(err) } if summary != "待删意图" { t.Fatalf("summary=%q, want 待删意图", summary) } n, err := es.GetNode(intent) if err != nil || n == nil { t.Fatalf("intent removed by soft delete: n=%+v err=%v", n, err) } if n.State != StateIntentDeleted || n.DeleteReason != "方向判断错误" { t.Fatalf("state=%q delete_reason=%q, want deleted/方向判断错误", n.State, n.DeleteReason) } // 待领(open)意图也允许假删除。 openIntent := mustIntent(t, es, "待领意图") if _, err := es.SoftDeleteIntent(openIntent, "方向不需要了"); err != nil { t.Fatalf("soft delete open intent: %v", err) } if n, err := es.GetNode(openIntent); err != nil || n == nil || n.State != StateIntentDeleted { t.Fatalf("open intent not soft-deleted: n=%+v err=%v", n, err) } // 已删除(deleted)等其它状态不能再假删除。 if _, err := es.SoftDeleteIntent(intent, "再删"); err == nil { t.Fatal("soft-deleting an already-deleted intent unexpectedly succeeded") } } // TestHardDeleteCascadesExclusiveDescendants 真删除沿链路级联删除独占子孙到叶子。 func TestHardDeleteCascadesExclusiveDescendants(t *testing.T) { d, err := Open(testDSN(t)) if err != nil { t.Skipf("postgres unavailable (%v) — skipping", err) } defer d.Close() expID, err := d.CreateExploration("hard cascade", "级联删除") if err != nil { t.Fatal(err) } defer d.Exec(`DELETE FROM explorations WHERE id=$1`, expID) es := d.Exploration(expID) // intent1 --yields--> fact1 --derived_from--> intent2 --yields--> fact2(叶子) intent1 := mustIntent(t, es, "根意图") fact1 := mustNode(t, es, KindFact, "事实1") mustLink(t, es, intent1, RelYields, fact1) intent2 := mustIntent(t, es, "衍生意图") mustLink(t, es, fact1, RelDerivedFrom, intent2) fact2 := mustNode(t, es, KindFact, "事实2") mustLink(t, es, intent2, RelYields, fact2) cleanup, err := es.CancelIntent(intent1) if err != nil { t.Fatal(err) } if cleanup.Intents != 2 || cleanup.Facts != 2 { t.Fatalf("cleanup=%+v, want 2 intents / 2 facts", cleanup) } for _, id := range []int64{intent1, fact1, intent2, fact2} { if !gone(t, es, id) { t.Fatalf("node %d survived cascade", id) } } } // TestHardDeletePreservesSharedAndGoal 真删除保留共享子孙(还有其它父)与目标。 func TestHardDeletePreservesSharedAndGoal(t *testing.T) { d, err := Open(testDSN(t)) if err != nil { t.Skipf("postgres unavailable (%v) — skipping", err) } defer d.Close() expID, err := d.CreateExploration("hard preserve", "保留共享/目标") if err != nil { t.Fatal(err) } defer d.Exec(`DELETE FROM explorations WHERE id=$1`, expID) es := d.Exploration(expID) goal, err := es.AddGoal(map[string]any{"text": "拿下后台"}, "human") if err != nil { t.Fatal(err) } // intent1 独占 finding(proves goal),intent1 与 intentX 共享 fact1(fact1 衍生出 intent2)。 intent1 := mustIntent(t, es, "待删意图") intentX := mustIntent(t, es, "旁路意图") finding := mustNode(t, es, KindFinding, "漏洞") mustLink(t, es, intent1, RelYields, finding) mustLink(t, es, finding, RelProves, goal) shared := mustNode(t, es, KindFact, "共享事实") mustLink(t, es, intent1, RelYields, shared) mustLink(t, es, intentX, RelYields, shared) intent2 := mustIntent(t, es, "由共享事实衍生") mustLink(t, es, shared, RelDerivedFrom, intent2) cleanup, err := es.CancelIntent(intent1) if err != nil { t.Fatal(err) } // 只删 intent1 与其独占的 finding;shared(有 intentX 父)及其下游 intent2、goal 全保留。 if cleanup.Intents != 1 || cleanup.Findings != 1 || cleanup.Facts != 0 { t.Fatalf("cleanup=%+v, want 1 intent / 1 finding / 0 fact", cleanup) } if !gone(t, es, intent1) || !gone(t, es, finding) { t.Fatal("intent1/finding should be removed") } for _, id := range []int64{goal, intentX, shared, intent2} { if gone(t, es, id) { t.Fatalf("node %d was wrongly cascaded", id) } } }