190 lines
5.7 KiB
Go
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)
|
|
}
|
|
}
|