Files
artex/db/finding_retests_test.go
T
dela 0335d572de
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
First Commit
2026-10-09 08:38:16 +08:00

243 lines
7.8 KiB
Go

package db
import (
"context"
"encoding/json"
"errors"
"strings"
"sync"
"testing"
)
func retestDB(t *testing.T) (*DB, int64) {
t.Helper()
d, err := Open(testDSN(t))
if err != nil {
t.Fatalf("open test database: %v", err)
}
fid, err := d.AddFinding(0, 0, "retest-test", "测试漏洞", "high", "original summary", "original evidence", "worker", nil)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_, _ = d.Exec(`DELETE FROM conversations WHERE id IN (SELECT conversation_id FROM finding_retests WHERE finding_id=$1)`, fid)
_, _ = d.DeleteFinding(fid)
d.Close()
})
return d, fid
}
func TestRetestAtomicDeduplicationAndHistory(t *testing.T) {
d, fid := retestDB(t)
var wg sync.WaitGroup
var mu sync.Mutex
createdCount := 0
ids := make([]int64, 0, 8)
for range 8 {
wg.Go(func() {
r, c, created, err := d.CreateFindingRetest(t.Context(), fid, " 修复版本 v2 ")
if err != nil {
t.Error(err)
return
}
mu.Lock()
defer mu.Unlock()
ids = append(ids, r.ID)
if created {
createdCount++
if c == nil {
t.Error("created without conversation")
}
}
})
}
wg.Wait()
if createdCount != 1 || len(ids) != 8 {
t.Fatalf("created=%d ids=%v", createdCount, ids)
}
for _, id := range ids {
if id != ids[0] {
t.Fatal("duplicate active retests", ids)
}
}
rows, err := d.ListFindingRetests(fid)
if err != nil || len(rows) != 1 {
t.Fatalf("history=%v err=%v", rows, err)
}
r := rows[0]
if r.Snapshot != nil {
t.Fatal("history leaks large snapshot")
}
ctx := t.Context()
full, err := d.FindingRetestForConversation(ctx, *r.ConversationID)
if err != nil || !strings.Contains(string(full.Snapshot), "original evidence") {
t.Fatalf("snapshot=%+v err=%v", full, err)
}
var messages int
if err := d.QueryRow(`SELECT count(*) FROM conversation_activities WHERE conversation_id=$1 AND kind='user'`, *r.ConversationID).Scan(&messages); err != nil || messages != 1 {
t.Fatalf("messages=%d err=%v", messages, err)
}
_, _ = d.Exec(`UPDATE findings SET evidence='changed evidence' WHERE id=$1`, fid)
full, err = d.FindingRetestForConversation(ctx, *r.ConversationID)
if err != nil || strings.Contains(string(full.Snapshot), "changed evidence") {
t.Fatal("snapshot changed", err)
}
if err = d.RecordFindingRetestResult(ctx, *r.ConversationID, "fixed", "summary", "proof"); !errors.Is(err, ErrRetestNotRunning) {
t.Fatal("pending accepted result", err)
}
if ok, err := d.StartFindingRetest(ctx, r.ID); err != nil || !ok {
t.Fatalf("start=%t %v", ok, err)
}
if err := d.RecordFindingRetestResult(ctx, 0, "fixed", "summary", "proof"); !errors.Is(err, ErrRetestNotRunning) {
t.Fatal("unscoped write accepted", err)
}
for range 2 {
if err := d.RecordFindingRetestResult(ctx, *r.ConversationID, "fixed", "修复验证通过", "正常对照可用,原触发条件失效"); err != nil {
t.Fatal(err)
}
}
if err := d.RecordFindingRetestResult(ctx, *r.ConversationID, "reproduced", "different", "proof"); !errors.Is(err, ErrRetestNotRunning) {
t.Fatal("overwrote staged result", err)
}
if err := d.FinishFindingRetest(r.ID, "completed", ""); err != nil {
t.Fatal(err)
}
if err := d.RecordFindingRetestResult(ctx, *r.ConversationID, "fixed", "summary", "proof"); !errors.Is(err, ErrRetestNotRunning) {
t.Fatal("overwrote sealed result", err)
}
f, _ := d.GetFinding(fid)
if f.Status != FindingFixed || f.Evidence != "changed evidence" || f.Report != "" {
t.Fatal("fixed retest did not update only triage", f)
}
next, _, created, err := d.CreateFindingRetest(ctx, fid, "second")
if err != nil || !created || next.ID == r.ID {
t.Fatalf("new history=%+v created=%t err=%v", next, created, err)
}
if err := d.FinishFindingRetest(next.ID, "completed", ""); err != nil {
t.Fatal(err)
}
rows, _ = d.ListFindingRetests(fid)
if len(rows) != 2 || rows[0].Status != "failed" || rows[0].Error == "" || rows[1].Verdict != "fixed" {
t.Fatal("missing verdict treated as successful", rows)
}
}
func TestRetestDeletionAndRestart(t *testing.T) {
d, fid := retestDB(t)
r, c, _, err := d.CreateFindingRetest(t.Context(), fid, "delete")
if err != nil {
t.Fatal(err)
}
if err := d.DeleteConversation(c.ID); err != nil {
t.Fatal(err)
}
rows, _ := d.ListFindingRetests(fid)
if len(rows) != 1 || rows[0].ConversationID != nil || rows[0].Status != "stopped" {
t.Fatal(rows)
}
next, c2, created, err := d.CreateFindingRetest(t.Context(), fid, "restart")
if err != nil || !created {
t.Fatal("deleted session blocks retry", err)
}
if ok, err := d.StartFindingRetest(t.Context(), next.ID); err != nil || !ok {
t.Fatal(err)
}
if err := d.RecoverFindingRetests(); err != nil {
t.Fatal(err)
}
if err := d.FinishFindingRetest(next.ID, "completed", ""); err != nil {
t.Fatal(err)
}
full, _ := d.FindingRetestForConversation(t.Context(), c2.ID)
if full.Status != "stopped" || full.FinishedAt == nil {
t.Fatal("restart result overwritten", full)
}
if _, err := d.DeleteFinding(fid); err != nil {
t.Fatal(err)
}
var count int
_ = d.QueryRow(`SELECT count(*) FROM finding_retests WHERE id IN ($1,$2)`, r.ID, next.ID).Scan(&count)
if count != 0 {
t.Fatal("finding deletion did not cascade")
}
_ = d.DeleteConversation(c2.ID)
}
func TestRetestValidationAndRollback(t *testing.T) {
d, fid := retestDB(t)
ctx, cancel := context.WithCancel(t.Context())
cancel()
if _, _, _, err := d.CreateFindingRetest(ctx, fid, ""); err == nil {
t.Fatal("cancelled creation succeeded")
}
for _, args := range [][3]string{{"unknown", "summary", "proof"}, {"fixed", " ", "proof"}, {"fixed", "summary", ""}, {"fixed", strings.Repeat("x", 16001), "proof"}} {
if err := d.RecordFindingRetestResult(t.Context(), 0, args[0], args[1], args[2]); err == nil {
t.Fatal("bad result accepted", args[0])
}
}
r, c, _, err := d.CreateFindingRetest(t.Context(), fid, "snapshot")
if err != nil {
t.Fatal(err)
}
full, err := d.FindingRetestForConversation(t.Context(), c.ID)
var snap map[string]json.RawMessage
if err != nil || json.Unmarshal(full.Snapshot, &snap) != nil || len(snap["finding"]) == 0 {
t.Fatal("invalid snapshot", err)
}
if r.Notes != "snapshot" {
t.Fatal("notes lost")
}
}
func TestRetestFixedTriageOnlyAfterSuccessfulCompletion(t *testing.T) {
for _, tc := range []struct{ verdict, terminal, want string }{
{"fixed", "completed", FindingFixed},
{"fixed", "failed", FindingInProgress},
{"fixed", "stopped", FindingInProgress},
{"reproduced", "completed", FindingInProgress},
{"inconclusive", "completed", FindingInProgress},
{"", "completed", FindingInProgress},
} {
t.Run(tc.verdict+"/"+tc.terminal, func(t *testing.T) {
d, fid := retestDB(t)
if _, err := d.SetFindingStatus(fid, FindingInProgress); err != nil {
t.Fatal(err)
}
r, c, _, err := d.CreateFindingRetest(t.Context(), fid, "check triage")
if err != nil {
t.Fatal(err)
}
if _, err := d.StartFindingRetest(t.Context(), r.ID); err != nil {
t.Fatal(err)
}
if tc.verdict != "" {
if err := d.RecordFindingRetestResult(t.Context(), c.ID, tc.verdict, "summary", "proof"); err != nil {
t.Fatal(err)
}
}
f, err := d.GetFinding(fid)
if err != nil || f.Status != FindingInProgress {
t.Fatal("staged verdict changed triage", err, f)
}
if err := d.FinishFindingRetest(r.ID, tc.terminal, ""); err != nil {
t.Fatal(err)
}
f, err = d.GetFinding(fid)
if err != nil || f.Status != tc.want || f.Evidence != "original evidence" {
t.Fatalf("finding=%+v err=%v want=%s", f, err, tc.want)
}
// Re-delivering completion must not undo a later user decision.
if _, err := d.SetFindingStatus(fid, FindingIgnored); err != nil {
t.Fatal(err)
}
if err := d.FinishFindingRetest(r.ID, "completed", ""); err != nil {
t.Fatal(err)
}
f, err = d.GetFinding(fid)
if err != nil || f.Status != FindingIgnored {
t.Fatal("replayed completion overwrote triage", err, f)
}
})
}
}