ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
171 lines
5.4 KiB
Go
171 lines
5.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|