159 lines
5.7 KiB
Go
159 lines
5.7 KiB
Go
package app
|
||
|
||
import (
|
||
"testing"
|
||
"time"
|
||
|
||
"infinite-canvas/backend/internal/model"
|
||
"infinite-canvas/backend/internal/repository"
|
||
)
|
||
|
||
func TestStuckCloudAgentIsTerminated(t *testing.T) {
|
||
s, db, a := agentMediaFixture(t)
|
||
if sqlDB, err := db.DB(); err == nil {
|
||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||
}
|
||
run, state := agentMediaRun(t, s, a, "request_approval", "stuck-idle")
|
||
state.ActiveTaskID, state.MediaTaskID, state.StoryboardTaskID = "", "", ""
|
||
// 事件日志是 append-only(已落库的行不能再改写),所以这里用"追加一条时间戳在十分钟前的
|
||
// 新事件"表达"最后进展在十分钟前",而不是回头改上一条已落库的事件。
|
||
state.event(run.ID, "assistant_message", map[string]any{"text": "起个头"})
|
||
state.Events[len(state.Events)-1].CreatedAt = time.Now().Add(-10 * time.Minute)
|
||
if err := s.repo.MutateCloudAgent("user", run.ID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
|
||
return cloudAgentSave(current, &state)
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
run, err := s.repo.CloudAgent("user", run.ID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !s.terminateStuckCloudAgent(run) {
|
||
t.Fatal("长时间没有任何进展的运行应当被判定卡死")
|
||
}
|
||
after, err := s.repo.CloudAgent("user", run.ID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if after.Status == "running" || after.Status == "queued" {
|
||
t.Fatalf("判定卡死后必须收尾,实际 status=%s", after.Status)
|
||
}
|
||
if after.FailureMessage == "" {
|
||
t.Fatal("卡死收尾必须留下原因,否则用户只看到突然停了")
|
||
}
|
||
}
|
||
|
||
func TestStuckDetectionLeavesActiveRunAlone(t *testing.T) {
|
||
s, db, a := agentMediaFixture(t)
|
||
if sqlDB, err := db.DB(); err == nil {
|
||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||
}
|
||
run, state := agentMediaRun(t, s, a, "request_approval", "stuck-fresh")
|
||
state.ActiveTaskID, state.MediaTaskID, state.StoryboardTaskID = "", "", ""
|
||
if len(state.Events) == 0 {
|
||
state.event(run.ID, "assistant_message", map[string]any{"text": "起个头"})
|
||
}
|
||
if err := s.repo.MutateCloudAgent("user", run.ID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
|
||
return cloudAgentSave(current, &state)
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
run, _ = s.repo.CloudAgent("user", run.ID)
|
||
if s.terminateStuckCloudAgent(run) {
|
||
t.Fatal("刚有进展的运行不该被判卡死")
|
||
}
|
||
}
|
||
|
||
func TestStuckDetectionSkipsRunsWithLiveWork(t *testing.T) {
|
||
s, db, a := agentMediaFixture(t)
|
||
if sqlDB, err := db.DB(); err == nil {
|
||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||
}
|
||
run, state := agentMediaRun(t, s, a, "request_approval", "stuck-live")
|
||
live := &model.Task{ID: "live-task", UserID: "user", ProjectID: "agent-canvas",
|
||
Type: "canvas_text", Status: model.TaskStatusRunning, CreatedAt: time.Now(), UpdatedAt: time.Now()}
|
||
if err := db.Create(live).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
state.ActiveTaskID = live.ID
|
||
state.TaskIDs = append(state.TaskIDs, live.ID)
|
||
// 同上:只能追加新事件,不能改写已有事件的时间戳。
|
||
state.event(run.ID, "assistant_message", map[string]any{"text": "起个头"})
|
||
state.Events[len(state.Events)-1].CreatedAt = time.Now().Add(-10 * time.Minute)
|
||
if err := s.repo.MutateCloudAgent("user", run.ID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
|
||
return cloudAgentSave(current, &state)
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
run, _ = s.repo.CloudAgent("user", run.ID)
|
||
if s.terminateStuckCloudAgent(run) {
|
||
t.Fatal("任务还在跑就不算卡死,不能误杀")
|
||
}
|
||
}
|
||
|
||
func TestStuckDetectionTreatsTerminalTaskAsIdle(t *testing.T) {
|
||
s, db, a := agentMediaFixture(t)
|
||
if sqlDB, err := db.DB(); err == nil {
|
||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||
}
|
||
run, state := agentMediaRun(t, s, a, "request_approval", "stuck-dead")
|
||
dead := &model.Task{ID: "dead-storyboard-task", UserID: "user", ProjectID: "agent-canvas",
|
||
Type: "canvas_text", Status: model.TaskStatusFailed, CreatedAt: time.Now(), UpdatedAt: time.Now()}
|
||
if err := db.Create(dead).Error; err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
state.ActiveTaskID, state.MediaTaskID = "", ""
|
||
state.StoryboardTaskID = dead.ID
|
||
// 同上:只能追加新事件,不能改写已有事件的时间戳。
|
||
state.event(run.ID, "assistant_message", map[string]any{"text": "起个头"})
|
||
state.Events[len(state.Events)-1].CreatedAt = time.Now().Add(-10 * time.Minute)
|
||
if err := s.repo.MutateCloudAgent("user", run.ID, run.Revision, func(current *model.CloudAgentExecution, _ *repository.Repository) error {
|
||
return cloudAgentSave(current, &state)
|
||
}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
run, _ = s.repo.CloudAgent("user", run.ID)
|
||
if !s.terminateStuckCloudAgent(run) {
|
||
t.Fatal("指向的任务已经终态时应当算没有在跑,兜底必须触发")
|
||
}
|
||
}
|
||
|
||
func TestFailCloudAgentPersistsFailureMessage(t *testing.T) {
|
||
s, db, a := agentMediaFixture(t)
|
||
if sqlDB, err := db.DB(); err == nil {
|
||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||
}
|
||
run, state := agentMediaRun(t, s, a, "request_approval", "fail-message")
|
||
run, err := s.repo.CloudAgent("user", run.ID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
const reason = "模型输入超过当前能力预算"
|
||
if err := s.failCloudAgent(run, &state, reason); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
after, err := s.repo.CloudAgent("user", run.ID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if after.Status != "failed" {
|
||
t.Fatalf("状态应为 failed,得到 %s", after.Status)
|
||
}
|
||
if after.FailureMessage != reason {
|
||
t.Fatalf("失败原因必须落库,得到 %q", after.FailureMessage)
|
||
}
|
||
decoded, err := cloudAgentDecode(after)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
found := false
|
||
for _, event := range decoded.Events {
|
||
if event.Type == "run_failed" && stringValue(event.Payload["text"]) == reason {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatal("事件里的 run_failed 也要保留原因")
|
||
}
|
||
}
|