ci / vet-and-test (pull_request) Successful in 43s
Why: A hard kill left runs in pending, running, or waiting_hitl, a busy send stored a message before the 409, finishRun could mark a run completed without an assistant message, and a full SSE buffer dropped events without closing the stream. Evidence: GOWORK=off go vet ./... and GOWORK=off go test -count=1 ./... passed, including store, runmgr, bus, and server recovery tests. Impact: Migration v14 adds runs.instance_id and messages.request_id. Startup reconciliation and shutdown share one seal. Turn and workflow sends claim a slot before writing. Duplicate request ids return the existing message. SettleRun updates the run and appends the assistant message in one transaction. A full subscriber buffer closes the channel so SSE can replay from the cursor. Refs: Dev feature/pi-durable-borrow Co-authored-by: Cursor <cursoragent@cursor.com>
135 lines
4.0 KiB
Go
135 lines
4.0 KiB
Go
package store
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
core "agent-system-core"
|
|
)
|
|
|
|
func seedConversation(t *testing.T, st *Store) core.Conversation {
|
|
t.Helper()
|
|
p, err := st.CreateProject(OwnerDefault, "p")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c, err := st.CreateConversation(OwnerDefault, p.ID, "c", "")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return c
|
|
}
|
|
|
|
func seedRun(t *testing.T, st *Store, conv core.Conversation, status core.RunStatus) core.Run {
|
|
t.Helper()
|
|
r := core.Run{
|
|
ConversationID: conv.ID, ProjectID: conv.ProjectID, Owner: conv.Owner,
|
|
Kind: core.RunConversation, Status: status, Phase: core.PhaseThinking,
|
|
}
|
|
if err := st.CreateRun(&r); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func TestMessageRequestIDDedup(t *testing.T) {
|
|
st := openTestStore(t)
|
|
conv := seedConversation(t, st)
|
|
first := core.Message{ConversationID: conv.ID, Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "hi"}}}
|
|
if err := st.AppendMessageWithRequest(&first, "req-1"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second := core.Message{ConversationID: conv.ID, Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "hi again"}}}
|
|
err := st.AppendMessageWithRequest(&second, "req-1")
|
|
var dup *DuplicateRequestError
|
|
if !errors.As(err, &dup) || dup.Existing.ID != first.ID {
|
|
t.Fatalf("duplicate request: %v %#v", err, dup)
|
|
}
|
|
blankA := core.Message{ConversationID: conv.ID, Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "a"}}}
|
|
blankB := core.Message{ConversationID: conv.ID, Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "b"}}}
|
|
if err := st.AppendMessageWithRequest(&blankA, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := st.AppendMessageWithRequest(&blankB, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
msgs, err := st.ListMessages(conv.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(msgs) != 3 {
|
|
t.Fatalf("messages: %d", len(msgs))
|
|
}
|
|
}
|
|
|
|
func TestCreateRunRecordsInstance(t *testing.T) {
|
|
st := openTestStore(t)
|
|
st.SetInstanceID("host:8787")
|
|
conv := seedConversation(t, st)
|
|
run := seedRun(t, st, conv, core.RunPending)
|
|
var got string
|
|
if err := st.db.QueryRow(st.q(`SELECT instance_id FROM runs WHERE id=?`), run.ID).Scan(&got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != "host:8787" {
|
|
t.Fatalf("instance_id %q", got)
|
|
}
|
|
}
|
|
|
|
func TestRecoverableRunsRespectInstance(t *testing.T) {
|
|
st := openTestStore(t)
|
|
conv := seedConversation(t, st)
|
|
st.SetInstanceID("inst-a")
|
|
own := seedRun(t, st, conv, core.RunPending)
|
|
st.SetInstanceID("inst-b")
|
|
other := seedRun(t, st, conv, core.RunRunning)
|
|
st.SetInstanceID("")
|
|
legacy := seedRun(t, st, conv, core.RunWaitingHITL)
|
|
done := seedRun(t, st, conv, core.RunCompleted)
|
|
|
|
all := st.Dialect() != "postgres"
|
|
got, err := st.ListRecoverableRuns("inst-a", all)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ids := map[string]bool{}
|
|
for _, r := range got {
|
|
ids[r.ID] = true
|
|
}
|
|
if !ids[own.ID] || !ids[legacy.ID] || ids[done.ID] {
|
|
t.Fatalf("recoverable ids: %v", ids)
|
|
}
|
|
if st.Dialect() == "postgres" && ids[other.ID] {
|
|
t.Fatal("postgres reconcile must leave another instance's run alone")
|
|
}
|
|
if st.Dialect() != "postgres" && !ids[other.ID] {
|
|
t.Fatal("sqlite reconcile scans every open run")
|
|
}
|
|
}
|
|
|
|
func TestSettleRunRollsBackWhenMessageInsertFails(t *testing.T) {
|
|
st := openTestStore(t)
|
|
conv := seedConversation(t, st)
|
|
run := seedRun(t, st, conv, core.RunRunning)
|
|
existing := core.Message{ID: "msg_fixed", ConversationID: conv.ID, Role: core.RoleUser, Parts: []core.Part{{Type: core.PartText, Text: "user"}}}
|
|
if err := st.AppendMessage(&existing); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Now().UTC()
|
|
run.Status = core.RunCompleted
|
|
run.Phase = core.PhaseDone
|
|
run.EndedAt = &now
|
|
dup := core.Message{ID: "msg_fixed", ConversationID: conv.ID, RunID: run.ID, Role: core.RoleAssistant, Parts: []core.Part{{Type: core.PartText, Text: "assistant"}}}
|
|
if _, err := st.SettleRun(&run, &dup); err == nil {
|
|
t.Fatal("duplicate message id must fail the settle")
|
|
}
|
|
got, err := st.GetRunByID(run.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Status != core.RunRunning {
|
|
t.Fatalf("rolled-back status: %s", got.Status)
|
|
}
|
|
}
|