package application import ( "context" "strings" "testing" "time" "img_infinite_canvas/internal/domain/design" "img_infinite_canvas/internal/infrastructure/memory" ) type fakeImageVectorizer struct { result design.ImageVectorization err error calls int last design.ImageVectorizationRequest } func (v *fakeImageVectorizer) VectorizeImage(ctx context.Context, req design.ImageVectorizationRequest) (design.ImageVectorization, error) { v.calls++ v.last = req if v.err != nil { return design.ImageVectorization{}, v.err } return v.result, nil } func TestVectorizeActionWritesSVGResultToCopiedNodeBesideTarget(t *testing.T) { ctx := context.Background() repo := memory.NewProjectRepository() assets := newFakeAssetStorage() queue := &fakeJobQueue{} vectorizer := &fakeImageVectorizer{ result: design.ImageVectorization{ SVG: "```svg\n\n```", Width: 340, Height: 340, }, } service := NewDesignService(repo, nil, assets, nil, nil) service.SetJobQueue(queue) service.SetImageVectorizer(vectorizer) source := testPNGDataURL(testColor(32, 96, 180, 255), 4, 3) project := design.Project{ ID: "project-vectorize", Title: "Vectorize", Status: design.StatusReady, UpdatedAt: time.Now(), Nodes: []design.Node{{ ID: "image-1", Type: design.NodeTypeImage, Title: "Moteva简洁科技Logo", X: 100, Y: 120, Width: 340, Height: 340, Content: source, Status: "success", }}, } if err := repo.Save(ctx, project); err != nil { t.Fatal(err) } started, err := service.RunNodeActionAsync(ctx, project.ID, "image-1", design.NodeActionRequest{ Action: "vectorize", Prompt: "转换成可下载 SVG", }) if err != nil { t.Fatal(err) } if len(queue.jobs) != 1 || queue.jobs[0].Request.Action != "vectorize" { t.Fatalf("expected vectorize job, got %#v", queue.jobs) } if len(started.Nodes) != 2 { t.Fatalf("expected source plus vectorize placeholder, got %#v", started.Nodes) } sourceNode := started.Nodes[0] placeholder := started.Nodes[1] if sourceNode.Status != "success" || sourceNode.Title != project.Nodes[0].Title || sourceNode.Content != project.Nodes[0].Content { t.Fatalf("expected source node unchanged, got %#v", sourceNode) } if placeholder.Status != "generating" || placeholder.Title != "矢量化中" { t.Fatalf("expected vectorize placeholder to enter generating state, got %#v", placeholder) } if placeholder.X != sourceNode.X+sourceNode.Width+32 || placeholder.Y != sourceNode.Y { t.Fatalf("expected placeholder beside source, got x=%f y=%f", placeholder.X, placeholder.Y) } if placeholder.Content != sourceNode.Content || placeholder.ParentID != sourceNode.ID { t.Fatalf("expected placeholder to copy source image, got %#v", placeholder) } if queue.jobs[0].Request.Options[nodeActionResultNodeIDOption] != placeholder.ID { t.Fatalf("expected job to carry result node id %s, got %#v", placeholder.ID, queue.jobs[0].Request.Options) } task, err := service.GeneratorTask(ctx, project.ID, started.LastThreadID) if err != nil { t.Fatal(err) } if task.GeneratorName != imageVectorizerGeneratorName || task.QueueInfo.DisableUnlimitedPrice.Price != 14 { t.Fatalf("unexpected vectorizer task metadata: %#v", task) } if len(task.OriginalInputArgs.Image) != 1 || task.OriginalInputArgs.Image[0] != project.Nodes[0].Content || task.OriginalInputArgs.Prompt != "" || task.OriginalInputArgs.Resolution != "" { t.Fatalf("unexpected vectorizer input args: %#v", task.OriginalInputArgs) } if err := service.ProcessJob(ctx, queue.jobs[0]); err != nil { t.Fatal(err) } if vectorizer.calls != 1 || !strings.HasPrefix(vectorizer.last.ImageDataURL, "data:image/png;base64,") || vectorizer.last.Prompt != "转换成可下载 SVG" { t.Fatalf("expected vectorizer to receive image data URL and prompt, got calls=%d req=%#v", vectorizer.calls, vectorizer.last) } saved, err := repo.Get(ctx, project.ID) if err != nil { t.Fatal(err) } if len(saved.Nodes) != 2 { t.Fatalf("expected source plus vectorized result node, got %#v", saved.Nodes) } if saved.Nodes[0].Status != "success" || saved.Nodes[0].Content != project.Nodes[0].Content || saved.Nodes[0].Title != project.Nodes[0].Title { t.Fatalf("expected source node unchanged after vectorize, got %#v", saved.Nodes[0]) } node := saved.Nodes[1] expectedURL := "http://localhost:19000/canvas-assets/project-vectorize-" + placeholder.ID + "-vectorized.svg" if node.Content != expectedURL || node.Title != "Moteva简洁科技Logo_SVG" || node.Status != "success" { t.Fatalf("unexpected vectorized node: %#v", node) } if node.LayerRole != "vectorized-image" || node.Tone != "vector-image" || node.Width != 340 || node.Height != 340 || node.ParentID != "image-1" { t.Fatalf("unexpected vectorized node metadata: %#v", node) } select { case upload := <-assets.uploaded: if upload.ContentType != "image/svg+xml" { t.Fatalf("expected SVG upload, got %s", upload.ContentType) } if !strings.HasSuffix(upload.FileName, ".svg") { t.Fatalf("expected .svg filename, got %s", upload.FileName) } if !strings.HasPrefix(strings.TrimSpace(string(upload.Data)), "