First Commit
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
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
detections / detections (push) Canceled after 0s

This commit is contained in:
dela
2026-10-09 08:38:16 +08:00
commit 0335d572de
756 changed files with 201663 additions and 0 deletions
+343
View File
@@ -0,0 +1,343 @@
package server
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"github.com/Autumn-27/artex/db"
)
func TestFindingPaginationParam(t *testing.T) {
for _, test := range []struct {
raw, name string
fallback, upper, want int
}{
{name: "missing", raw: "", fallback: 10, upper: 100, want: 10},
{name: "invalid", raw: "not-a-number", fallback: 10, upper: 100, want: 10},
{name: "zero", raw: "0", fallback: 10, upper: 100, want: 10},
{name: "negative", raw: "-2", fallback: 1, upper: 0, want: 1},
{name: "bounded", raw: "500", fallback: 10, upper: 100, want: 100},
{name: "valid", raw: "25", fallback: 10, upper: 100, want: 25},
} {
t.Run(test.name, func(t *testing.T) {
if got := findingPaginationParam(test.raw, test.fallback, test.upper); got != test.want {
t.Fatalf("findingPaginationParam(%q,%d,%d)=%d, want %d",
test.raw, test.fallback, test.upper, got, test.want)
}
})
}
}
func TestFindingGroupsReturnsTaskBucketsAndNormalizesPagination(t *testing.T) {
m, err := NewManager(t.TempDir(), "")
if err != nil {
t.Skipf("postgres unavailable (%v) — skipping", err)
}
defer m.Close()
marker := fmt.Sprintf("__server_finding_groups_%d__", time.Now().UnixNano())
createdTaskIDs := []int64{}
defer func() {
_, _ = m.pg.Exec(`DELETE FROM findings WHERE vulnclass=$1`, marker)
for _, taskID := range createdTaskIDs {
_ = m.pg.DeleteTask(taskID)
}
}()
taskA, err := m.CreateTask("finding group A", "goal", nil, 0, 0)
if err != nil {
t.Fatal(err)
}
createdTaskIDs = append(createdTaskIDs, mustTaskID(t, taskA.ID))
taskB, err := m.CreateTask("finding group B", "goal", nil, 0, 0)
if err != nil {
t.Fatal(err)
}
createdTaskIDs = append(createdTaskIDs, mustTaskID(t, taskB.ID))
for i, seed := range []struct {
taskID string
severity string
}{
{taskA.ID, db.SeverityCritical},
{taskA.ID, db.SeverityHigh},
{taskB.ID, db.SeverityLow},
} {
if _, err := m.pg.AddFinding(mustTaskID(t, seed.taskID), 0, marker, "", seed.severity,
fmt.Sprintf("finding %d", i), "", "test", nil); err != nil {
t.Fatalf("AddFinding[%d]: %v", i, err)
}
}
s := &Server{m: m}
request := httptest.NewRequest(http.MethodGet,
"/api/exploration/findings/groups?vulnclass="+marker+"&sort=severity&page=1&limit=1", nil)
recorder := httptest.NewRecorder()
s.findingGroups(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("groups status=%d body=%s", recorder.Code, recorder.Body.String())
}
var page struct {
Items []db.FindingGroup `json:"items"`
Total int `json:"total"`
FindingTotal int `json:"finding_total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
if err := json.Unmarshal(recorder.Body.Bytes(), &page); err != nil {
t.Fatal(err)
}
if len(page.Items) != 1 || page.Total != 2 || page.FindingTotal != 3 || page.Page != 1 || page.PageSize != 1 {
t.Fatalf("unexpected grouped page: %+v", page)
}
if page.Items[0].TaskID == nil || *page.Items[0].TaskID != mustTaskID(t, taskA.ID) ||
page.Items[0].Count != 2 || page.Items[0].Critical != 1 || page.Items[0].High != 1 {
t.Fatalf("unexpected first group: %+v", page.Items[0])
}
request = httptest.NewRequest(http.MethodGet,
"/api/exploration/findings/groups?vulnclass="+marker+"&q=finding+2&page=1&limit=10", nil)
recorder = httptest.NewRecorder()
s.findingGroups(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("searched groups status=%d body=%s", recorder.Code, recorder.Body.String())
}
page = struct {
Items []db.FindingGroup `json:"items"`
Total int `json:"total"`
FindingTotal int `json:"finding_total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}{}
if err := json.Unmarshal(recorder.Body.Bytes(), &page); err != nil {
t.Fatal(err)
}
if len(page.Items) != 1 || page.Total != 1 || page.FindingTotal != 1 || page.Items[0].TaskID == nil ||
*page.Items[0].TaskID != mustTaskID(t, taskB.ID) {
t.Fatalf("unexpected searched group page: %+v", page)
}
request = httptest.NewRequest(http.MethodGet,
"/api/exploration/findings/groups?vulnclass="+marker+"&page=-4&limit=0", nil)
recorder = httptest.NewRecorder()
s.findingGroups(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("normalized groups status=%d body=%s", recorder.Code, recorder.Body.String())
}
page = struct {
Items []db.FindingGroup `json:"items"`
Total int `json:"total"`
FindingTotal int `json:"finding_total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}{}
if err := json.Unmarshal(recorder.Body.Bytes(), &page); err != nil {
t.Fatal(err)
}
if page.Page != 1 || page.PageSize != 10 || len(page.Items) != 2 {
t.Fatalf("pagination was not normalized: %+v", page)
}
}
func TestDeepenFindingCreatesAuditedIntentAndRevivesTask(t *testing.T) {
m, err := NewManager(t.TempDir(), "")
if err != nil {
t.Skipf("postgres unavailable (%v) — skipping", err)
}
defer m.Close()
task, err := m.CreateTask("deepen finding", "goal", nil, 0, 0)
if err != nil {
t.Fatal(err)
}
store := task.Store
findingNodeID, err := store.AddNode(db.KindFinding, map[string]any{"summary": "source"}, 5, "confirmed", "worker", nil)
if err != nil {
t.Fatal(err)
}
taskID, _ := strconv.ParseInt(task.ID, 10, 64)
defer m.pg.DeleteTask(taskID)
findingID, err := m.pg.AddFinding(taskID, findingNodeID, "test", "source", db.SeverityHigh, "source", "", "worker", nil)
if err != nil {
t.Fatal(err)
}
defer m.pg.DeleteFinding(findingID)
if err := m.SetTaskStatus(task.ID, "done"); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel() // admission is exercised without starting background planner/worker work
s := &Server{m: m, engine: NewEngine(m), ctx: ctx}
live, unsubscribe := s.engine.Broadcaster().Subscribe(task.ID)
defer unsubscribe()
body := bytes.NewBufferString(`{"description":"验证完整利用链并保留可复现证据"}`)
req := httptest.NewRequest(http.MethodPost, "/api/exploration/findings/1/deepen", body)
req.SetPathValue("id", strconv.FormatInt(findingID, 10))
rec := httptest.NewRecorder()
s.deepenFinding(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("deepen status=%d body=%s", rec.Code, rec.Body.String())
}
var response struct {
TaskID string `json:"task_id"`
IntentID string `json:"intent_id"`
State string `json:"state"`
Queued bool `json:"queued"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &response); err != nil {
t.Fatal(err)
}
intentID, err := strconv.ParseInt(response.IntentID, 10, 64)
if err != nil || intentID <= 0 || response.TaskID != task.ID || response.State != "open" || response.Queued {
t.Fatalf("unexpected response: %+v", response)
}
intent, err := store.GetNode(intentID)
if err != nil || intent == nil {
t.Fatalf("intent=%+v err=%v", intent, err)
}
if intent.Priority != 10 || intent.Origin != "human" || intent.State != "open" {
t.Fatalf("intent metadata: %+v", intent)
}
if task.Status != "running" || task.Paused || task.Queued {
t.Fatalf("task was not revived through admission: %+v", task)
}
activity, _, err := store.ActivityList(&intentID, 0, 50)
if err != nil {
t.Fatal(err)
}
var persistedAudit db.Activity
auditCount := 0
for _, item := range activity {
if item.Worker == "system" && strings.Contains(item.Summary, auditFindingFollowUpSummary) {
persistedAudit = item
auditCount++
}
}
if auditCount != 1 || persistedAudit.ID <= 0 {
t.Fatalf("atomic system audit count=%d activity=%+v", auditCount, activity)
}
select {
case broadcast := <-live:
if broadcast.ID != persistedAudit.ID || broadcast.NodeID == nil || *broadcast.NodeID != intentID ||
broadcast.CreatedAt.IsZero() {
t.Fatalf("broadcast must reuse committed activity: broadcast=%+v persisted=%+v", broadcast, persistedAudit)
}
default:
t.Fatal("committed follow-up audit was not broadcast")
}
select {
case duplicate := <-live:
t.Fatalf("follow-up audit was broadcast more than once: %+v", duplicate)
default:
}
// The retained finding can no longer be deepened after its task and node are
// deleted; this must be a conflict rather than silently creating orphan work.
if err := m.pg.DeleteTask(taskID); err != nil {
t.Fatal(err)
}
req = httptest.NewRequest(http.MethodPost, "/api/exploration/findings/1/deepen", bytes.NewBufferString(`{"description":"retry"}`))
req.SetPathValue("id", strconv.FormatInt(findingID, 10))
rec = httptest.NewRecorder()
s.deepenFinding(rec, req)
if rec.Code != http.StatusConflict {
t.Fatalf("deleted origin status=%d body=%s", rec.Code, rec.Body.String())
}
}
func TestDeepenFindingValidatesDescription(t *testing.T) {
s := &Server{}
tooLong, _ := json.Marshal(map[string]string{"description": strings.Repeat("x", maxFindingFollowUpRunes+1)})
for _, body := range []string{`{}`, `{"description":" "}`, string(tooLong)} {
req := httptest.NewRequest(http.MethodPost, "/api/exploration/findings/1/deepen", bytes.NewBufferString(body))
req.SetPathValue("id", "1")
rec := httptest.NewRecorder()
s.deepenFinding(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("body=%s status=%d response=%s", body, rec.Code, rec.Body.String())
}
}
oversized := `{"description":"` + strings.Repeat("x", (32<<10)+1) + `"}`
req := httptest.NewRequest(http.MethodPost, "/api/exploration/findings/1/deepen", strings.NewReader(oversized))
req.SetPathValue("id", "1")
rec := httptest.NewRecorder()
s.deepenFinding(rec, req)
if rec.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("oversized status=%d response=%s", rec.Code, rec.Body.String())
}
}
func TestDeepenAdmissionFailureDiscardsFollowUpIntent(t *testing.T) {
m, err := NewManager(t.TempDir(), "")
if err != nil {
t.Skipf("postgres unavailable (%v) - skipping", err)
}
defer m.Close()
task, err := m.CreateTask("deepen rollback", "leave no orphan intent", nil, 0, 0)
if err != nil {
t.Fatal(err)
}
taskID, _ := strconv.ParseInt(task.ID, 10, 64)
defer func() {
_, _ = m.pg.Exec(`UPDATE tasks SET deleted_at=NULL WHERE id=$1`, taskID)
_, _ = m.DeleteTask(task.ID, DeleteTaskOptions{})
}()
findingNodeID, err := task.Store.AddNode(db.KindFinding, map[string]any{"summary": "source"}, 5, "confirmed", "worker", nil)
if err != nil {
t.Fatal(err)
}
findingID, err := m.pg.AddFinding(taskID, findingNodeID, "test", "source", db.SeverityHigh, "source", "", "worker", nil)
if err != nil {
t.Fatal(err)
}
before, err := task.Store.ListByKind(db.KindIntent, 1000)
if err != nil {
t.Fatal(err)
}
var activitiesBefore int
if err := m.pg.QueryRow(`SELECT COUNT(*) FROM activity WHERE exploration_id=$1`, task.Store.ID()).Scan(&activitiesBefore); err != nil {
t.Fatal(err)
}
if err := m.SetTaskStatus(task.ID, "done"); err != nil {
t.Fatal(err)
}
// Keep the in-memory handle and graph available while forcing the atomic
// admission UPDATE to reject this soft-deleted row.
if _, err := m.pg.Exec(`UPDATE tasks SET deleted_at=now() WHERE id=$1`, taskID); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
s := &Server{m: m, engine: NewEngine(m), ctx: ctx}
req := httptest.NewRequest(http.MethodPost, "/api/exploration/findings/1/deepen",
bytes.NewBufferString(`{"description":"this intent must be rolled back"}`))
req.SetPathValue("id", strconv.FormatInt(findingID, 10))
rec := httptest.NewRecorder()
s.deepenFinding(rec, req)
if rec.Code != http.StatusInternalServerError {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
after, err := task.Store.ListByKind(db.KindIntent, 1000)
if err != nil {
t.Fatal(err)
}
if len(after) != len(before) {
t.Fatalf("failed admission left an orphan follow-up: before=%d after=%d nodes=%+v", len(before), len(after), after)
}
var activitiesAfter int
if err := m.pg.QueryRow(`SELECT COUNT(*) FROM activity WHERE exploration_id=$1`, task.Store.ID()).Scan(&activitiesAfter); err != nil {
t.Fatal(err)
}
if activitiesAfter != activitiesBefore {
t.Fatalf("failed admission left an orphan audit: before=%d after=%d", activitiesBefore, activitiesAfter)
}
}