157 lines
5.7 KiB
Go
157 lines
5.7 KiB
Go
|
|
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<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"340\" height=\"340\" viewBox=\"0 0 340 340\"><path d=\"M40 40h260v260H40z\" fill=\"#111827\"/></svg>\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)), "<svg") {
|
||
|
|
t.Fatalf("expected raw SVG bytes, got %q", string(upload.Data))
|
||
|
|
}
|
||
|
|
case <-time.After(time.Second):
|
||
|
|
t.Fatal("expected uploaded SVG asset")
|
||
|
|
}
|
||
|
|
|
||
|
|
task, err = service.GeneratorTask(ctx, project.ID, started.LastThreadID)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if task.Status != "completed" || len(task.Artifacts) != 1 || task.Artifacts[0].Metadata.Format != "svg" {
|
||
|
|
t.Fatalf("expected completed SVG generator task, got %#v", task)
|
||
|
|
}
|
||
|
|
}
|