Files
moteva/server/internal/application/visual_annotation_test.go
T
root bb8946b79b feat(canvas): add visual annotation edit action
Add a VisualAnnotationPanel letting users mark regions on an image (brush,
circle, rectangle, cross) with per-mark edit instructions, composited into
an annotation guide image. The new "visual-annotation" node action sends
the unmarked source plus the annotated guide to the image model, editing
only numbered regions. Server validates the annotation image/instructions
and threads them through the generator task.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 10:03:59 +08:00

99 lines
3.6 KiB
Go

package application
import (
"context"
"errors"
"strings"
"testing"
"time"
"img_infinite_canvas/internal/domain/design"
"img_infinite_canvas/internal/infrastructure/memory"
)
func TestRunVisualAnnotationCarriesCleanSourceAndGuideImage(t *testing.T) {
ctx := context.Background()
repo := memory.NewProjectRepository()
queue := &fakeJobQueue{}
service := NewDesignService(repo, nil, nil, nil, nil)
service.SetJobQueue(queue)
project := design.Project{
ID: "project-visual-annotation",
Title: "Annotated edit",
Status: design.StatusReady,
UpdatedAt: time.Now(),
Nodes: []design.Node{{
ID: "image-1",
Type: design.NodeTypeImage,
Title: "Source",
Width: 640,
Height: 480,
Content: "https://example.com/source.png",
Status: "success",
}},
}
if err := repo.Save(ctx, project); err != nil {
t.Fatal(err)
}
_, err := service.RunNodeActionAsync(ctx, project.ID, "image-1", design.NodeActionRequest{
Action: "visual-annotation",
Prompt: "1. Remove the logo",
})
if !errors.Is(err, design.ErrInvalidInput) {
t.Fatalf("expected missing guide image to be rejected, got %v", err)
}
selection := `{"annotations":[{"index":1,"type":"cross","instruction":"Remove the logo","points":[{"x":0.4,"y":0.3},{"x":0.6,"y":0.5}]}]}`
guideURL := "https://example.com/annotation-guide.png"
next, err := service.RunNodeActionAsync(ctx, project.ID, "image-1", design.NodeActionRequest{
Action: "visual_annotation",
Prompt: "1. Cross: Remove the logo",
Selection: selection,
AnnotationImage: guideURL,
})
if err != nil {
t.Fatal(err)
}
if len(next.Nodes) != 2 {
t.Fatalf("expected the source plus an annotated-edit copy, got %#v", next.Nodes)
}
sourceNode := next.Nodes[0]
placeholder := next.Nodes[1]
if sourceNode.Status != "success" || sourceNode.Title != "Source" || sourceNode.Content != project.Nodes[0].Content {
t.Fatalf("expected the source image to remain unchanged, got %#v", sourceNode)
}
if placeholder.Status != "generating" || placeholder.Title != "标注改图中" || placeholder.ParentID != sourceNode.ID || placeholder.LayerRole != "visual-annotation-edit" {
t.Fatalf("expected a generating annotated-edit copy, got %#v", placeholder)
}
if placeholder.X != sourceNode.X+sourceNode.Width+32 || placeholder.Y != sourceNode.Y || placeholder.Content != sourceNode.Content {
t.Fatalf("expected the annotated-edit copy beside the source, got %#v", placeholder)
}
if len(queue.jobs) != 1 {
t.Fatalf("expected one visual annotation job, got %#v", queue.jobs)
}
request := queue.jobs[0].Request
if request.Action != "visual-annotation" || request.AnnotationImage != guideURL || request.Selection != selection {
t.Fatalf("visual annotation request was not preserved in the job: %#v", request)
}
if request.Options[nodeActionResultNodeIDOption] != placeholder.ID {
t.Fatalf("expected the job to target copied node %q, got %#v", placeholder.ID, request.Options)
}
task, err := service.GeneratorTask(ctx, project.ID, next.LastThreadID)
if err != nil {
t.Fatal(err)
}
if len(task.OriginalInputArgs.Image) != 2 || task.OriginalInputArgs.Image[0] != project.Nodes[0].Content || task.OriginalInputArgs.Image[1] != guideURL {
t.Fatalf("expected clean source followed by annotation guide, got %#v", task.OriginalInputArgs.Image)
}
prompt := buildNodeActionPrompt(request, project.Nodes[0])
for _, fragment := range []string{"first image is the clean source", "second image is an annotated guide only", "never reproduce guide marks", selection} {
if !strings.Contains(prompt, fragment) {
t.Fatalf("expected visual annotation prompt to contain %q, got %q", fragment, prompt)
}
}
}