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)), "