Files
ARTEX-chixuweihu/db/exploration_test.go
T
Autumn-27 b741ee176c feat: 漏洞详情页删除证据/元数据 Tab,新增「链路图」
链路图展示该漏洞所属任务探索图里,从初始节点(任务根)追溯到本漏洞节点的
那条链路:漏洞节点 + 其全部祖先(沿边反向可达)+ 之间的关系边。

- db.ExplorationStore.FindingLineage(nodeID):递归 CTE 取祖先子图(节点+边),
  下游(如 finding→proves→goal)与无关节点不纳入
- GET /api/exploration/findings/{id}/lineage:解析 finding 的 node_id/task_id →
  返回 {nodes,edges};无节点/任务已删 → 空图
- 前端 api.findingLineage;详情页 Tab 改为「概览 / 链路图」
- lineage.tsx:按深度拓扑排序,自根到漏洞纵向展示节点卡片 + 入边关系标注,
  漏洞节点高亮
- db 测试补 FindingLineage 断言(含祖先/排除下游 goal/排除无关意图)

go build / db / server 测试通过。
2026-08-18 13:08:03 -04:00

115 lines
3.3 KiB
Go

package db
import "testing"
func TestExplorationFlow(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("test", "拿下测试目标")
if err != nil {
t.Fatal(err)
}
defer d.Exec(`DELETE FROM explorations WHERE id=$1`, expID) // cascades nodes/edges/activity
es := d.Exploration(expID)
// goal node + two intents
goal, err := es.AddGoal(map[string]any{"text": "getadmin", "vulnclass": "authz"}, "human")
if err != nil {
t.Fatal(err)
}
if _, err := es.AddIntent(map[string]any{"summary": "enumerate endpoints"}, 5, nil, "planner"); err != nil {
t.Fatal(err)
}
i2, err := es.AddIntent(map[string]any{"summary": "test idor"}, 8, nil, "planner")
if err != nil {
t.Fatal(err)
}
// frontier ordered by priority desc → i2(8) before i1(5)
fr, err := es.Frontier(10)
if err != nil {
t.Fatal(err)
}
if len(fr) != 2 || fr[0].ID != i2 {
t.Fatalf("frontier order wrong: %+v", fr)
}
// atomic claim: first wins, second on same id fails
ok, err := es.ClaimIntent(i2, "worker-1")
if err != nil || !ok {
t.Fatalf("claim i2: ok=%v err=%v", ok, err)
}
ok2, _ := es.ClaimIntent(i2, "worker-2")
if ok2 {
t.Fatalf("double-claim should fail")
}
// finding yields from intent, proves goal
find, err := es.AddNode("finding", map[string]any{"vulnclass": "idor", "severity": "high"}, 9, "confirmed", "worker-1", nil)
if err != nil {
t.Fatal(err)
}
if err := es.Link(i2, "yields", find); err != nil {
t.Fatal(err)
}
if err := es.Link(find, "proves", goal); err != nil {
t.Fatal(err)
}
if err := es.SetNodeState(goal, "met"); err != nil {
t.Fatal(err)
}
// lineage: ancestors of the finding traced backward — here {i2, find} joined by
// the yields edge. The proves→goal edge is DOWNSTREAM (goal must be excluded),
// and the unrelated intent i1 is not on any path to the finding (excluded too).
lnNodes, lnEdges, err := es.FindingLineage(find)
if err != nil {
t.Fatal(err)
}
got := map[int64]bool{}
for _, n := range lnNodes {
got[n.ID] = true
}
if len(lnNodes) != 2 || !got[find] || !got[i2] {
t.Fatalf("lineage nodes: want {i2,find}, got %+v", lnNodes)
}
if got[goal] {
t.Fatalf("lineage must exclude the proved goal (it is downstream of the finding)")
}
if len(lnEdges) != 1 || lnEdges[0].From != i2 || lnEdges[0].To != find || lnEdges[0].Rel != "yields" {
t.Fatalf("lineage edges: want i2-yields->find, got %+v", lnEdges)
}
// activity poll by id cursor
id1, err := es.AppendActivity(Activity{Worker: "worker-1", Kind: "tool_use", Tool: "Bash", Summary: "ran curl", Detail: "full output"})
if err != nil {
t.Fatal(err)
}
items, cursor, err := es.ActivityList(nil, 0, 100)
if err != nil {
t.Fatal(err)
}
if len(items) != 1 || cursor != id1 {
t.Fatalf("activity list: items=%d cursor=%d", len(items), cursor)
}
det, _ := es.ActivityDetail(id1)
if det != "full output" {
t.Fatalf("detail want 'full output', got %q", det)
}
// incremental: nothing new after cursor
items2, _, _ := es.ActivityList(nil, cursor, 100)
if len(items2) != 0 {
t.Fatalf("incremental poll should be empty, got %d", len(items2))
}
// stats
st, _ := es.Stats()
if st["intent"] != 2 || st["goal"] != 1 || st["finding"] != 1 {
t.Fatalf("stats: %+v", st)
}
}