package app import ( "context" "encoding/json" "strings" "testing" "infinite-canvas/backend/internal/model" "infinite-canvas/backend/internal/prompts" ) func agentCanvasPatchForOperation(t *testing.T, state cloudAgentRuntime, operation string) map[string]any { t.Helper() var found map[string]any for _, event := range state.Events { if event.Type == "canvas_updated" && event.Payload["operation"] == operation { if found != nil { t.Fatalf("duplicate canvas event for %s", operation) } found = event.Payload } } if found == nil { t.Fatalf("missing persisted canvas event for %s", operation) } return found } func TestCloudAgentCanvasPatchesPersistDraftSubmissionAndAllTerminalStates(t *testing.T) { for _, status := range []model.TaskStatus{model.TaskStatusSucceeded, model.TaskStatusFailed, model.TaskStatusCancelled} { t.Run(string(status), func(t *testing.T) { s, db, args := agentMediaFixture(t) run, _ := agentMediaRun(t, s, args, "auto") if err := s.advanceCloudAgentByID("user", run.ID); err != nil { t.Fatal(err) } approveAgentMediaDraft(t, s, run.ID) run, _ = s.repo.CloudAgent("user", run.ID) state, _ := cloudAgentDecode(run) taskID := state.MediaTaskID if taskID == "" { t.Fatal("approved task missing") } if status == model.TaskStatusCancelled { if err := s.CancelCloudAgent(context.Background(), "user", run.ID); err != nil { t.Fatal(err) } } else { result := "{}" if status == model.TaskStatusSucceeded { if err := db.Create(&model.Resource{ID: "patch-output", UserID: "user", Kind: "video", Status: "ready", MimeType: "video/mp4"}).Error; err != nil { t.Fatal(err) } result = `{"mode":"video","video":{"storageKey":"resource:patch-output"}}` } if err := db.Model(&model.Task{}).Where("id = ?", taskID).Updates(map[string]any{"status": status, "result_json": result, "error": "上游拒绝"}).Error; err != nil { t.Fatal(err) } if err := s.advanceCloudAgentByID("user", run.ID); err != nil { t.Fatal(err) } } run, _ = s.repo.CloudAgent("user", run.ID) state, _ = cloudAgentDecode(run) draft := agentCanvasPatchForOperation(t, state, "generate_media_draft") patch := draft["canvasPatch"].(map[string]any) changes := creationMaps(patch["nodes"]) if patch["canvasId"] != "agent-canvas" || len(changes) != 1 || changes[0]["before"] != nil || len(creationMaps(patch["connections"])) != 3 { t.Fatalf("draft is not a bounded node/edge delta: %+v", patch) } referenceTitles := map[string]bool{} for _, action := range creationMaps(draft["actions"]) { if action["action"] == "referenced" { referenceTitles[stringValue(action["title"])] = true } } if !referenceTitles["叮当猫在飞"] || !referenceTitles["古风奥特曼"] { t.Fatalf("persisted trace lost referenced node titles: %+v", draft["actions"]) } submission := agentCanvasPatchForOperation(t, state, "generate_media_submit")["canvasPatch"].(map[string]any) submitted := creationMaps(submission["nodes"])[0]["after"].(map[string]any)["metadata"].(map[string]any) if submitted["status"] != "loading" || submitted["taskId"] != taskID || len(creationMaps(submission["connections"])) != 0 { t.Fatalf("submission failed to lock the existing draft: %+v", submission) } completion := agentCanvasPatchForOperation(t, state, "generate_media_complete")["canvasPatch"].(map[string]any) changes = creationMaps(completion["nodes"]) if len(changes) != 1 || len(creationMaps(completion["connections"])) != 0 { t.Fatalf("completion includes unrelated canvas data: %+v", completion) } before := changes[0]["before"].(map[string]any)["metadata"].(map[string]any) after := changes[0]["after"].(map[string]any)["metadata"].(map[string]any) want := "error" if status == model.TaskStatusSucceeded { want = "success" } if before["status"] != "loading" || after["status"] != want || after["taskStatus"] != string(status) || after["taskId"] != taskID { t.Fatalf("terminal patch lost task identity/state: %+v", changes[0]) } // Replaying a checkpoint must not create another charged task or delta. if err := s.advanceCloudAgentByID("user", run.ID); err != nil { t.Fatal(err) } var count int64 db.Model(&model.Task{}).Where("type = ?", "canvas_video").Count(&count) if count != 1 { t.Fatal("terminal handling resubmitted a charged task") } }) } } func TestCloudAgentMediaImageSourceHasActionableCorrection(t *testing.T) { s, _, args := agentMediaFixture(t) run, state := agentMediaRun(t, s, args, "auto") args.SourceNodeID = "cat" if _, _, err := s.prepareCloudAgentMedia(run, &state, agentMediaCall(args)); err == nil || !strings.Contains(err.Error(), "referenceNodeIds") { t.Fatalf("image source must point to the correct field: %v", err) } args.ReferenceNodeIDs = []string{"hero"} if _, _, err := s.prepareCloudAgentMedia(run, &state, agentMediaCall(args)); err == nil || !strings.Contains(err.Error(), "sourceNodeId 仅接受可作为文本输入") { t.Fatalf("nonduplicate image source must also be rejected: %v", err) } args.SourceNodeID = "" if _, _, err := s.prepareCloudAgentMedia(run, &state, agentMediaCall(args)); err != nil { t.Fatalf("reference-only image-to-video should be valid: %v", err) } } func TestCloudAgentCanvasOperationTraceSharesToolCallID(t *testing.T) { s, _, args := agentMediaFixture(t) run, state := agentMediaRun(t, s, args, "auto") call := cloudAgentCall{ID: "canvas-call"} call.Function.Name = "canvas_apply_ops" raw, _ := json.Marshal(map[string]any{"snapshotHash": args.SnapshotHash, "ops": []map[string]any{ {"type": "update_node", "id": "cat", "patch": map[string]any{"title": "叮当猫飞行参考", "content": "下一版参考图提示词"}}, {"type": "add_node", "id": "trace-video", "nodeType": "video", "title": "视频草稿"}, {"type": "connect_nodes", "id": "trace-reference", "fromNodeId": "cat", "toNodeId": "trace-video"}, }}) call.Function.Arguments = string(raw) state.Calls = []cloudAgentCall{call} if err := s.advanceCloudAgentTool(run, &state); err != nil { t.Fatal(err) } run, _ = s.repo.CloudAgent("user", run.ID) state, _ = cloudAgentDecode(run) trace := agentCanvasPatchForOperation(t, state, "canvas_apply_ops") last := state.Events[len(state.Events)-1] if trace["callId"] != call.ID || last.Type != "tool_completed" || last.Payload["callId"] != call.ID { t.Fatalf("canvas delta and tool result cannot be deduplicated: %+v / %+v", trace, last) } actions := creationMaps(trace["actions"]) byActionAndNode := map[string]map[string]any{} for _, action := range actions { byActionAndNode[stringValue(action["action"])+":"+stringValue(action["nodeId"])] = action } updated := byActionAndNode["updated:cat"] if updated == nil || updated["title"] != "叮当猫在飞" || updated["resultTitle"] != "叮当猫飞行参考" { t.Fatalf("canvas operation trace lost updated node identity: %+v", actions) } fieldValues := []string{} switch fields := updated["fields"].(type) { case []string: fieldValues = fields case []any: for _, field := range fields { fieldValues = append(fieldValues, stringValue(field)) } } if len(fieldValues) != 2 || fieldValues[0] != "节点名称" || fieldValues[1] != "下一版提示词" { t.Fatalf("canvas operation trace lost updated fields: %+v", updated) } created := byActionAndNode["created:trace-video"] if created == nil || created["title"] != "视频草稿" || created["nodeType"] != "video" { t.Fatalf("canvas operation trace lost created node identity: %+v", actions) } referenced := byActionAndNode["referenced:cat"] if referenced == nil || referenced["title"] != "叮当猫飞行参考" || referenced["targetNodeId"] != "trace-video" || referenced["targetTitle"] != "视频草稿" || referenced["targetNodeType"] != "video" { t.Fatalf("canvas operation trace lost reference endpoints: %+v", actions) } } func TestCloudAgentMissingCanvasStillCheckpointsMediaFailure(t *testing.T) { s, db, args := agentMediaFixture(t) run, _ := agentMediaRun(t, s, args, "auto") if err := s.advanceCloudAgentByID("user", run.ID); err != nil { t.Fatal(err) } approveAgentMediaDraft(t, s, run.ID) run, _ = s.repo.CloudAgent("user", run.ID) state, _ := cloudAgentDecode(run) if err := db.Model(&model.Task{}).Where("id = ?", state.MediaTaskID).Update("status", model.TaskStatusFailed).Error; err != nil { t.Fatal(err) } if err := db.Where("id = ?", "agent-canvas").Delete(&model.CanvasProject{}).Error; err != nil { t.Fatal(err) } if err := s.advanceCloudAgentByID("user", run.ID); err != nil { t.Fatal(err) } run, _ = s.repo.CloudAgent("user", run.ID) state, _ = cloudAgentDecode(run) if run.Status != "failed" || state.MediaTaskID != "" || state.Events[len(state.Events)-1].Type != "tool_failed" { t.Fatal("deleted canvas left the media checkpoint running") } for _, event := range state.Events { if event.Type == "canvas_updated" && event.Payload["operation"] == "generate_media_complete" { t.Fatal("deleted canvas produced a phantom node update") } } } func TestCloudAgentMediaPolicyAllowsDefaultsWithoutBypassingApproval(t *testing.T) { _, media, err := prompts.LoadAgentPolicies() if err != nil { t.Fatal(err) } for _, text := range []string{"安全默认", "不要求四项逐个确认", "一次性询问", "界面独立审批", "失败不得自动重提"} { if !strings.Contains(media.Text, text) { t.Fatalf("media policy lost %q", text) } } // Deltas remain JSON serializable when a newly created node has no before value. raw, err := json.Marshal(cloudAgentObjectChanges([]any{}, []map[string]any{{"id": "new"}})) if err != nil || !strings.Contains(string(raw), `"before":null`) { t.Fatalf("invalid creation delta: %s %v", raw, err) } }