Files
Agent-System-Backend/internal/store/messages_test.go
T

121 lines
4.0 KiB
Go

package store
import (
"errors"
"testing"
"time"
core "agent-system-core"
)
// settleRunFixture 把 run 写成终态(completed + ended_at/usage),模拟 runner 结算。
func settleRunFixture(t *testing.T, st *Store, run core.Run, status core.RunStatus) core.Run {
t.Helper()
got, err := st.GetRunByID(run.ID)
if err != nil {
t.Fatal(err)
}
usage := core.Usage{PromptTokens: 7, CompletionTokens: 5, TotalTokens: 12}
now := time.Now().UTC()
got.Status = status
got.Phase = core.PhaseDone
got.Usage = &usage
got.EndedAt = &now
if err := st.UpdateRun(&got); err != nil {
t.Fatal(err)
}
return got
}
// 条件推进的三个结果必须可区分:命中、已被别的写者推进(不写)、行不存在。
func TestResumeRunFromWaitingHITLReportsOutcome(t *testing.T) {
st := openTestStore(t)
conv := seedConversation(t, st)
run := seedRun(t, st, conv, core.RunWaitingHITL)
applied, err := st.ResumeRunFromWaitingHITL(&run)
if err != nil || !applied {
t.Fatalf("first resume: applied=%v err=%v", applied, err)
}
got, err := st.GetRunByID(run.ID)
if err != nil || got.Status != core.RunRunning {
t.Fatalf("resumed row: status=%s err=%v", got.Status, err)
}
// 已推进但未终态(重复/跨实例转交):不写、不报错。
applied, err = st.ResumeRunFromWaitingHITL(&run)
if err != nil || applied {
t.Fatalf("second resume of an advanced row: applied=%v err=%v", applied, err)
}
// 行不存在:与"已被推进"分开报告。
missing := core.Run{ID: NewID("run"), ConversationID: conv.ID, Owner: conv.Owner}
applied, err = st.ResumeRunFromWaitingHITL(&missing)
if !errors.Is(err, ErrNotFound) || applied {
t.Fatalf("missing row: applied=%v err=%v", applied, err)
}
applied, err = st.ResumeRunFromWaitingHITL(&core.Run{})
if !errors.Is(err, ErrNotFound) || applied {
t.Fatalf("empty id: applied=%v err=%v", applied, err)
}
}
// 回归:终态行不得被条件推进覆盖,ended_at/usage 等审计字段必须留存。
// 命中缺陷实现(无条件的整行写)时,这里就是 run 被拉回 running 的那一步。
func TestResumeRunFromWaitingHITLCannotOverwriteTerminalRow(t *testing.T) {
for _, status := range []core.RunStatus{core.RunCompleted, core.RunFailed, core.RunCancelled} {
t.Run(string(status), func(t *testing.T) {
st := openTestStore(t)
conv := seedConversation(t, st)
run := seedRun(t, st, conv, core.RunWaitingHITL)
settled := settleRunFixture(t, st, run, status)
applied, err := st.ResumeRunFromWaitingHITL(&settled)
if err != nil || applied {
t.Fatalf("resume touched a terminal row: applied=%v err=%v", applied, err)
}
after, err := st.GetRunByID(run.ID)
if err != nil {
t.Fatal(err)
}
if after.Status != status {
t.Fatalf("status = %s, want %s", after.Status, status)
}
if after.EndedAt == nil {
t.Fatal("ended_at wiped by the resume")
}
if after.Usage == nil || after.Usage.TotalTokens != 12 {
t.Fatalf("usage = %+v, want the runner's value", after.Usage)
}
if after.Phase != core.PhaseDone {
t.Fatalf("phase = %s, want done", after.Phase)
}
})
}
}
// 条件推进按当前行状态判定:runner 又问一轮时(新 waiting_hitl 行)仍可推进。
func TestResumeRunFromWaitingHITLAppliesPerWaitingRound(t *testing.T) {
st := openTestStore(t)
conv := seedConversation(t, st)
first := seedRun(t, st, conv, core.RunWaitingHITL)
if applied, err := st.ResumeRunFromWaitingHITL(&first); err != nil || !applied {
t.Fatalf("round 1: applied=%v err=%v", applied, err)
}
settled := settleRunFixture(t, st, first, core.RunCompleted)
// 第二轮:runner 再问一次,行回到 waiting_hitl。
round2 := settled
round2.Status = core.RunWaitingHITL
if err := st.UpdateRun(&round2); err != nil {
t.Fatal(err)
}
applied, err := st.ResumeRunFromWaitingHITL(&round2)
if err != nil || !applied {
t.Fatalf("round 2: applied=%v err=%v", applied, err)
}
got, err := st.GetRunByID(first.ID)
if err != nil || got.Status != core.RunRunning {
t.Fatalf("round 2 row: status=%s err=%v", got.Status, err)
}
}