package project import ( "fmt" "path/filepath" "strings" "testing" "cyberstrike-ai/internal/config" "cyberstrike-ai/internal/database" "go.uber.org/zap" ) func TestFormatIncomingLinksHint(t *testing.T) { t.Parallel() hint := FormatIncomingLinksHint([]*database.ProjectFactEdge{ {EdgeType: "discovered_on", SourceFactKey: "finding/x", Confidence: "tentative"}, }) if !strings.Contains(hint, "入边:") { t.Fatalf("expected 入边 label: %q", hint) } if !strings.Contains(hint, "discovered_on←finding/x") { t.Fatalf("unexpected hint: %q", hint) } if !strings.Contains(hint, "tentative") { t.Fatalf("expected tentative in hint: %q", hint) } } func TestFormatIncomingLinksHint_allEdges(t *testing.T) { t.Parallel() edges := make([]*database.ProjectFactEdge, 0, 5) for i := 1; i <= 5; i++ { edges = append(edges, &database.ProjectFactEdge{ EdgeType: "discovered_on", SourceFactKey: fmt.Sprintf("finding/f%d", i), Confidence: "tentative", }) } hint := FormatIncomingLinksHint(edges) if strings.Contains(hint, "+") { t.Fatalf("should not truncate with +N: %q", hint) } for i := 1; i <= 5; i++ { if !strings.Contains(hint, fmt.Sprintf("finding/f%d", i)) { t.Fatalf("missing edge f%d in hint: %q", i, hint) } } } func TestFormatFactIndexLinksHint_incomingOnly(t *testing.T) { t.Parallel() in := []*database.ProjectFactEdge{ {EdgeType: "discovered_on", SourceFactKey: "target/dev", Confidence: "tentative"}, {EdgeType: "exploits", SourceFactKey: "exploit/rce", Confidence: "confirmed"}, } hint := FormatFactIndexLinksHint("finding/sqli", in) if !strings.Contains(hint, "关系边:") { t.Fatalf("missing 关系边 label: %q", hint) } if !strings.Contains(hint, "discovered_on←target/dev") { t.Fatalf("missing discovered_on: %q", hint) } if !strings.Contains(hint, "exploits←exploit/rce") { t.Fatalf("missing exploits: %q", hint) } if strings.Contains(hint, "出边") || strings.Contains(hint, "入边") { t.Fatalf("should not use legacy 出边/入边 labels: %q", hint) } } func TestFormatFactIndexLinksHint_includesAuxiliaryEdgeTypes(t *testing.T) { t.Parallel() in := []*database.ProjectFactEdge{{EdgeType: "supports", SourceFactKey: "note/log"}} hint := FormatFactIndexLinksHint("finding/x", in) if !strings.Contains(hint, "supports←note/log") { t.Fatalf("supports edge should be included: %q", hint) } } func TestBuildFactPathOverviewSection(t *testing.T) { t.Parallel() edges := []*database.ProjectFactEdge{ {EdgeType: "discovered_on", SourceFactKey: "target/dev", TargetFactKey: "finding/sqli", Confidence: "tentative"}, {EdgeType: "exploits", SourceFactKey: "exploit/rce", TargetFactKey: "finding/sqli", Confidence: "confirmed"}, {EdgeType: "supports", SourceFactKey: "note/log", TargetFactKey: "finding/sqli"}, } keys := map[string]struct{}{ "target/dev": {}, "finding/sqli": {}, "exploit/rce": {}, "note/log": {}, } section := BuildFactPathOverviewSection(edges, keys, 800) if !strings.Contains(section, "### 攻击路径(事实关系)") { t.Fatalf("missing header: %q", section) } if !strings.Contains(section, "target/dev → finding/sqli") { t.Fatalf("missing discovered_on line: %q", section) } if !strings.Contains(section, "exploit/rce → finding/sqli") { t.Fatalf("missing exploits line: %q", section) } if !strings.Contains(section, "note/log → finding/sqli") { t.Fatalf("supports edge should be included: %q", section) } } func TestBuildFactIndexBlock_withLinksAndPathOverview(t *testing.T) { t.Parallel() dbPath := filepath.Join(t.TempDir(), "facts.db") db, err := database.NewDB(dbPath, zap.NewNop()) if err != nil { t.Fatal(err) } defer db.Close() proj, err := db.CreateProject(&database.Project{Name: "path-proj"}) if err != nil { t.Fatal(err) } _, err = db.UpsertProjectFact(&database.ProjectFact{ ProjectID: proj.ID, FactKey: "target/dev", Category: "target", Summary: "dev 子域", Confidence: "confirmed", }) if err != nil { t.Fatal(err) } _, err = db.UpsertProjectFact(&database.ProjectFact{ ProjectID: proj.ID, FactKey: "finding/sqli", Category: "finding", Summary: "时间盲注", Confidence: "tentative", }) if err != nil { t.Fatal(err) } _, err = db.AddProjectFactEdge(proj.ID, database.ProjectFactEdgeInput{ To: "finding/sqli", Type: "discovered_on", }, "target/dev", "") if err != nil { t.Fatal(err) } block, err := BuildFactIndexBlock(db, proj.ID, config.ProjectConfig{Enabled: true, FactIndexMaxRunes: 6500, FactIndexPathMaxRunes: 1000}) if err != nil { t.Fatal(err) } if !strings.Contains(block, "关系边: discovered_on←target/dev") { t.Fatalf("finding line should include relation hint: %q", block) } if !strings.Contains(block, "### 攻击路径(事实关系)") { t.Fatalf("missing relation overview: %q", block) } if !strings.Contains(block, "target/dev → finding/sqli") { t.Fatalf("missing overview edge: %q", block) } }