fix(agent): let stopped threads start a new task and scope thread state to the latest task
Replace the stored cancel func with a handle so a stale deferred cleanup can't delete a newer task's cancellation, and clear a thread's cancelled flag when it submits fresh work instead of refusing to restart. Thread progress/finished state now derives from the latest user task in the thread rather than the whole history. Adds coverage for continuing a stopped thread and for sanitized generation-failure messages. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,42 @@ func TestMarkGenerationFailedRemovesCanvasPlaceholders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkGenerationFailedDoesNotExposeInternalEndpoint(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := memory.NewProjectRepository()
|
||||
service := NewDesignService(repo, nil, nil, nil, nil)
|
||||
project := design.Project{
|
||||
ID: "project-safe-error",
|
||||
Title: "Safe failure",
|
||||
Brief: "prompt",
|
||||
Status: design.StatusExploring,
|
||||
UpdatedAt: time.Now(),
|
||||
Nodes: []design.Node{
|
||||
{ID: "placeholder", Type: design.NodeTypeImage, Title: "生成中", Content: "prompt", Status: "generating"},
|
||||
},
|
||||
}
|
||||
if err := repo.Save(ctx, project); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
failure := errors.New(`gpt image 2 generation failed: Post "http://154.21.87.3:8080/v1/images/generations": context deadline exceeded (Client.Timeout exceeded while awaiting headers)`)
|
||||
if err := service.markGenerationFailed(ctx, project.ID, failure); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
saved, err := repo.Get(ctx, project.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := saved.Messages[len(saved.Messages)-1].Content
|
||||
if strings.Contains(content, "154.21.87.3") || strings.Contains(content, "/v1/images/generations") || strings.Contains(content, "Client.Timeout") {
|
||||
t.Fatalf("expected sanitized user-facing failure, got %q", content)
|
||||
}
|
||||
if !strings.Contains(content, "图片生成耗时较久") {
|
||||
t.Fatalf("expected friendly timeout message, got %q", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkGenerationFailedKeepsRealImageNodeActionTarget(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := memory.NewProjectRepository()
|
||||
@@ -895,7 +931,7 @@ func TestCreateProjectGenerationPartialFailurePreservesSuccessfulImagesAndCloses
|
||||
if message.Name == "generate_media" && message.Status == "success" {
|
||||
foundArtifact = true
|
||||
}
|
||||
if message.Role == "error" && message.Title == "图片生成部分失败" && strings.Contains(message.Content, "image 2") {
|
||||
if message.Role == "error" && message.Title == "图片生成部分失败" && strings.Contains(message.Content, "已保留成功结果") {
|
||||
foundError = true
|
||||
}
|
||||
}
|
||||
@@ -1154,8 +1190,8 @@ func TestCreateProjectGenerationFailureUsesJobUserContext(t *testing.T) {
|
||||
t.Fatalf("expected failed project, got %s", saved.Status)
|
||||
}
|
||||
last := saved.Messages[len(saved.Messages)-1]
|
||||
if last.Role != "error" || last.Content != "upstream image response timed out" {
|
||||
t.Fatalf("expected original failure message for job user, got %#v", last)
|
||||
if last.Role != "error" || strings.Contains(last.Content, "upstream") || !strings.Contains(last.Content, "画布内容已保留") {
|
||||
t.Fatalf("expected safe failure message for job user, got %#v", last)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1486,9 +1522,10 @@ func TestStopAgentTaskClearsAgentPlaceholdersWithoutStoppingImageGenerator(t *te
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentChatDoesNotRestartAlreadyCancelledThread(t *testing.T) {
|
||||
func TestAgentChatContinuesStoppedThreadWithNewTask(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repo := memory.NewProjectRepository()
|
||||
queue := &fakeJobQueue{}
|
||||
service := NewDesignService(repo, nil, nil, nil, nil, fakeCreativeChatAgent{
|
||||
decision: design.AgentDecision{Intent: "image"},
|
||||
taskPlan: design.AgentTaskPlan{
|
||||
@@ -1498,6 +1535,7 @@ func TestAgentChatDoesNotRestartAlreadyCancelledThread(t *testing.T) {
|
||||
ImageTasks: []design.AgentImageTask{{Title: "视觉方案", Brief: "视觉方案"}},
|
||||
},
|
||||
})
|
||||
service.SetJobQueue(queue)
|
||||
project := design.Project{
|
||||
ID: "project-cancelled-thread",
|
||||
Title: "Cancelled",
|
||||
@@ -1511,6 +1549,7 @@ func TestAgentChatDoesNotRestartAlreadyCancelledThread(t *testing.T) {
|
||||
if err := repo.Save(ctx, project); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
service.cancelAgentTask(project.ID, "thread-cancelled")
|
||||
|
||||
thread, err := service.AgentChat(ctx, project.ID, design.AgentChatRequest{
|
||||
ThreadID: "thread-cancelled",
|
||||
@@ -1526,14 +1565,20 @@ func TestAgentChatDoesNotRestartAlreadyCancelledThread(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if thread.Status != "cancelled" {
|
||||
t.Fatalf("expected cancelled thread response, got %s", thread.Status)
|
||||
if thread.Status != "running" {
|
||||
t.Fatalf("expected stopped thread to accept a new running task, got %s", thread.Status)
|
||||
}
|
||||
saved, err := repo.Get(ctx, project.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(saved.Nodes) != 0 || len(saved.Messages) != 1 {
|
||||
t.Fatalf("expected cancelled thread not to create new work, got nodes=%#v messages=%#v", saved.Nodes, saved.Messages)
|
||||
if len(saved.Nodes) != 1 || saved.Nodes[0].Status != "generating" {
|
||||
t.Fatalf("expected new image task placeholder, got %#v", saved.Nodes)
|
||||
}
|
||||
if len(saved.Messages) <= 1 || saved.Messages[len(saved.Messages)-1].Status != "success" {
|
||||
t.Fatalf("expected new thread messages after cancelled task, got %#v", saved.Messages)
|
||||
}
|
||||
if len(queue.jobs) != 1 || queue.jobs[0].Kind != design.JobFollowupGeneration {
|
||||
t.Fatalf("expected new followup generation job, got %#v", queue.jobs)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user