Files
Agent-System-Backend/internal/store/search_deadlock_run_timing_test.go
T
agent-exec-transport 60f37adc04
governance / governance (pull_request) Successful in 20s
ci / vet-and-test (pull_request) Successful in 1m27s
agent-exec transport 9b5caca2968f510a4f81402ca7dfdb4c/f6d16260d971
2026-10-20 03:51:12 +00:00

190 lines
5.7 KiB
Go

package store
import (
"context"
"testing"
"time"
core "agent-system-core"
)
func TestSearchDeadlockRegression(t *testing.T) {
st := openTestStore(t)
ctx := context.Background()
// Seed account for allow/pred callback to query
if err := st.CreateAccount("alice", "", "", false); err != nil {
t.Fatal(err)
}
// Seed knowledge doc and vector
docVec := core.KnowledgeDoc{
ID: "doc_vec_deadlock",
Owner: "alice",
ProjectID: "proj_deadlock",
Source: "src_vec",
Title: "vector doc",
Chunk: "vector chunk content",
}
docFTS := core.KnowledgeDoc{
ID: "doc_fts_deadlock",
Owner: "alice",
ProjectID: "proj_deadlock",
Source: "src_fts",
Title: "Database Optimization Guide",
Chunk: "Performance tuning and indexing for relational databases",
}
docLike := core.KnowledgeDoc{
ID: "doc_like_deadlock",
Owner: "alice",
ProjectID: "proj_deadlock",
Source: "src_like",
Title: "微服务架构治理",
Chunk: "分布式系统与全链路追踪方案",
}
if err := st.AddKnowledge(docVec, docFTS, docLike); err != nil {
t.Fatal(err)
}
if err := st.UpsertKnowledgeVector(ctx, "alice", docVec.ID, "ref_vec", []float32{1.0, 0.0}); err != nil {
t.Fatal(err)
}
runBounded := func(name string, fn func()) {
t.Helper()
done := make(chan struct{})
go func() {
defer close(done)
fn()
}()
select {
case <-done:
case <-time.After(10 * time.Second):
t.Fatalf("deadlock detected in %s: did not finish within 10s", name)
}
}
// 1. Vector scan: allow callback queries the same Store
runBounded("searchKnowledgeVectorsSharedAuthorizedScan", func() {
hits, err := st.SearchKnowledgeVectorsSharedAuthorized(ctx, "bob", "", []float32{1.0, 0.0}, 5, core.RetrievalQuery{}, func(d core.KnowledgeDoc) bool {
// Query the store inside the callback: on a single-connection SQLite store,
// this would deadlock if rows was not closed before invoking the callback.
_, qerr := st.GetAccountByOwner(d.Owner)
if qerr != nil {
t.Errorf("GetAccountByOwner failed in callback: %v", qerr)
}
return true
})
if err != nil {
t.Errorf("SearchKnowledgeVectorsSharedAuthorized failed: %v", err)
}
if len(hits) == 0 {
t.Errorf("expected at least 1 hit, got 0")
}
})
// 2. Content FTS scan: pred callback queries the same Store
runBounded("searchKnowledgeFTSCandidates", func() {
hits, err := st.searchKnowledgeFTSCandidates(func(d core.KnowledgeDoc) bool {
_, qerr := st.GetAccountByOwner(d.Owner)
if qerr != nil {
t.Errorf("GetAccountByOwner failed in callback: %v", qerr)
}
return true
}, core.RetrievalQuery{Text: "database optimization", TopK: 5}, true)
if err != nil {
t.Errorf("searchKnowledgeFTSCandidates failed: %v", err)
}
if len(hits) == 0 {
t.Errorf("expected at least 1 hit, got 0")
}
})
// 3. Content LIKE scan: pred callback queries the same Store
runBounded("searchKnowledgeLikeSharedCandidates", func() {
hits, err := st.searchKnowledgeLikeSharedCandidates("bob", core.RetrievalQuery{Text: "微服务", TopK: 5}, func(d core.KnowledgeDoc) bool {
_, qerr := st.GetAccountByOwner(d.Owner)
if qerr != nil {
t.Errorf("GetAccountByOwner failed in callback: %v", qerr)
}
return true
}, true)
if err != nil {
t.Errorf("searchKnowledgeLikeSharedCandidates failed: %v", err)
}
if len(hits) == 0 {
t.Errorf("expected at least 1 hit, got 0")
}
})
}
func TestRunCreatedAtNonZeroRecentAndMatchesReadBack(t *testing.T) {
st := openTestStore(t)
if err := st.CreateAccount("alice", "", "", false); err != nil {
t.Fatal(err)
}
proj, err := st.CreateProject("alice", "test_proj")
if err != nil {
t.Fatal(err)
}
conv, err := st.CreateConversation("alice", proj.ID, "test_conv", "")
if err != nil {
t.Fatal(err)
}
// 1. CreateRun with zero CreatedAt
before1 := time.Now().UTC().Add(-time.Second)
r1 := core.Run{
ConversationID: conv.ID,
ProjectID: conv.ProjectID,
Owner: conv.Owner,
Kind: core.RunConversation,
Status: core.RunRunning,
Phase: core.PhaseThinking,
}
if err := st.CreateRun(&r1); err != nil {
t.Fatalf("CreateRun failed: %v", err)
}
if r1.CreatedAt.IsZero() {
t.Fatalf("CreateRun: returned run has zero CreatedAt")
}
if r1.CreatedAt.Before(before1) || r1.CreatedAt.After(time.Now().UTC().Add(time.Second)) {
t.Fatalf("CreateRun: returned run CreatedAt %v not recent (before=%v)", r1.CreatedAt, before1)
}
readBack1, err := st.GetRunByID(r1.ID)
if err != nil {
t.Fatalf("GetRunByID failed: %v", err)
}
if !readBack1.CreatedAt.Equal(r1.CreatedAt) {
t.Fatalf("CreateRun: in-memory CreatedAt %v != stored CreatedAt %v", r1.CreatedAt, readBack1.CreatedAt)
}
// 2. CreateRunAccountChecked with zero CreatedAt
before2 := time.Now().UTC().Add(-time.Second)
r2 := core.Run{
ConversationID: conv.ID,
ProjectID: conv.ProjectID,
Owner: conv.Owner,
Kind: core.RunConversation,
Status: core.RunRunning,
Phase: core.PhaseThinking,
}
if err := st.CreateRunAccountChecked(&r2, false); err != nil {
t.Fatalf("CreateRunAccountChecked failed: %v", err)
}
if r2.CreatedAt.IsZero() {
t.Fatalf("CreateRunAccountChecked: returned run has zero CreatedAt")
}
if r2.CreatedAt.Before(before2) || r2.CreatedAt.After(time.Now().UTC().Add(time.Second)) {
t.Fatalf("CreateRunAccountChecked: returned run CreatedAt %v not recent (before=%v)", r2.CreatedAt, before2)
}
readBack2, err := st.GetRunByID(r2.ID)
if err != nil {
t.Fatalf("GetRunByID failed: %v", err)
}
if !readBack2.CreatedAt.Equal(r2.CreatedAt) {
t.Fatalf("CreateRunAccountChecked: in-memory CreatedAt %v != stored CreatedAt %v", r2.CreatedAt, readBack2.CreatedAt)
}
}