Files
moteva/server/internal/infrastructure/piagent/design_agent_test.go
T
root ffd8d78df7 feat(piagent): bypass planner for direct image generation modes
For image / pure-image / image-generator / image-action modes, skip the
task-planning round trip and hand the prompt straight to the image model,
deriving the requested image count from the composer settings. Also
normalize completed image-generator nodes back into regular image nodes
(clearing the image-generator layer role and defaulting Tone to "visual")
when merging generated results and when restoring finished nodes, so a
finished generator node is not re-appended as a duplicate.

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

700 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package piagent
import (
"context"
"errors"
"strconv"
"strings"
"sync"
"testing"
"unicode/utf8"
"img_infinite_canvas/internal/domain/design"
)
type fakeRunner struct{}
func (fakeRunner) Plan(ctx context.Context, req design.AgentRequest) (design.AgentPlan, error) {
return design.AgentPlan{
Message: design.Message{ID: "message-1", Role: "assistant", Title: "done", Content: "done"},
GeneratedNodes: []design.Node{
{
ID: "node-1",
Type: design.NodeTypeFrame,
Title: "Website Hero",
Width: 520,
Height: 300,
Content: req.Prompt,
},
{
ID: "mock-image-1",
Type: design.NodeTypeImage,
Title: "Mood x Color",
Width: 520,
Height: 640,
Content: req.Prompt,
},
{
ID: "mock-image-2",
Type: design.NodeTypeImage,
Title: "Mobile Page",
Width: 260,
Height: 460,
Content: req.Prompt,
},
},
}, nil
}
func (fakeRunner) Replay(ctx context.Context, project design.Project) (design.AgentReplay, error) {
return design.AgentReplay{ProjectID: project.ID, Messages: project.Messages}, nil
}
type recordingRunner struct {
calls int
}
func (r *recordingRunner) Plan(ctx context.Context, req design.AgentRequest) (design.AgentPlan, error) {
r.calls++
return design.AgentPlan{Message: design.Message{ID: "message-1", Role: "assistant", Title: "done", Content: "done"}}, nil
}
func (r *recordingRunner) Replay(ctx context.Context, project design.Project) (design.AgentReplay, error) {
return design.AgentReplay{ProjectID: project.ID, Messages: project.Messages}, nil
}
type recordingCreativeAgent struct {
buildCalls int
summarizeCalls int
}
func (a *recordingCreativeAgent) Decide(ctx context.Context, req design.AgentDecisionRequest) (design.AgentDecision, error) {
return design.AgentDecision{}, nil
}
func (a *recordingCreativeAgent) Respond(ctx context.Context, req design.AgentConversationRequest) (string, error) {
return "", nil
}
func (a *recordingCreativeAgent) SummarizeImage(ctx context.Context, req design.AgentImageSummaryRequest) (string, error) {
a.summarizeCalls++
return "summary", nil
}
func (a *recordingCreativeAgent) BuildImagePrompt(ctx context.Context, req design.AgentImagePromptRequest) (design.AgentImagePrompt, error) {
a.buildCalls++
return design.AgentImagePrompt{Title: "Rewritten", Prompt: "rewritten by gpt-5.5"}, nil
}
type fakeImageGenerator struct {
mu sync.Mutex
called bool
prompt string
opts ImageRequestOptions
calls int
options []ImageRequestOptions
}
type failingImageGenerator struct {
calls int
err error
}
type partialFailImageGenerator struct {
mu sync.Mutex
calls int
}
func (g *failingImageGenerator) Generate(ctx context.Context, prompt string, opts ImageRequestOptions) (GeneratedImage, error) {
g.calls++
return GeneratedImage{}, g.err
}
func (g *partialFailImageGenerator) Generate(ctx context.Context, prompt string, opts ImageRequestOptions) (GeneratedImage, error) {
g.mu.Lock()
g.calls++
call := g.calls
g.mu.Unlock()
if call == 1 {
return GeneratedImage{URL: "https://example.com/generated-partial.png", URLs: []string{"https://example.com/generated-partial.png"}}, nil
}
return GeneratedImage{}, errors.New("upstream response did not return image")
}
func (g *fakeImageGenerator) Generate(ctx context.Context, prompt string, opts ImageRequestOptions) (GeneratedImage, error) {
g.mu.Lock()
defer g.mu.Unlock()
g.called = true
g.prompt = prompt
g.opts = opts
g.calls++
callIndex := imageOptionIndex(prompt, g.calls)
g.options = append(g.options, opts)
count := opts.Count
if count < 1 {
count = 1
}
urls := make([]string, 0, count)
for index := 0; index < count; index++ {
urls = append(urls, "https://example.com/generated-"+strconv.Itoa(callIndex+index)+".png")
}
return GeneratedImage{URL: urls[0], URLs: urls}, nil
}
func imageOptionIndex(prompt string, fallback int) int {
for index := 1; index <= 10; index++ {
if strings.Contains(prompt, "option "+strconv.Itoa(index)+" of") {
return index
}
}
return fallback
}
func TestDesignAgentImageModeCallsImageModelDirectly(t *testing.T) {
runner := &recordingRunner{}
creative := &recordingCreativeAgent{}
imageGenerator := &fakeImageGenerator{}
agent := NewDesignAgent(runner, imageGenerator, creative)
plan, err := agent.Plan(context.Background(), design.AgentRequest{
Project: design.Project{ID: "project-direct", Title: "Direct image"},
Prompt: "a cute bird flatten ui",
Mode: "image",
ImageModel: "gpt-image-2",
ImageSize: "1536x1024",
TaskPlan: design.AgentTaskPlan{
Intent: "image",
ImageCount: 1,
ImageTasks: []design.AgentImageTask{
{Title: "Cute bird", Brief: "a cute bird flatten ui"},
},
},
})
if err != nil {
t.Fatal(err)
}
if runner.calls != 0 {
t.Fatalf("expected direct image mode to bypass base runner, got %d calls", runner.calls)
}
if creative.buildCalls != 0 || creative.summarizeCalls != 0 {
t.Fatalf("expected direct image mode to bypass gpt-5.5 helpers, got build=%d summarize=%d", creative.buildCalls, creative.summarizeCalls)
}
if !imageGenerator.called {
t.Fatal("expected image generator to be called")
}
if !strings.Contains(imageGenerator.prompt, "a cute bird flatten ui") {
t.Fatalf("expected raw user prompt to reach image generator, got %q", imageGenerator.prompt)
}
if strings.Contains(imageGenerator.prompt, "rewritten by gpt-5.5") || strings.Contains(imageGenerator.prompt, "Create one polished") {
t.Fatalf("expected direct prompt without analysis wrapper, got %q", imageGenerator.prompt)
}
if len(plan.GeneratedNodes) != 1 || plan.GeneratedNodes[0].Content == "" {
t.Fatalf("expected one generated image node, got %#v", plan.GeneratedNodes)
}
}
func TestDesignAgentImageGeneratorModeCallsImageModelDirectly(t *testing.T) {
runner := &recordingRunner{}
creative := &recordingCreativeAgent{}
imageGenerator := &fakeImageGenerator{}
agent := NewDesignAgent(runner, imageGenerator, creative)
var emitted []design.Node
plan, err := agent.PlanIncremental(context.Background(), design.AgentRequest{
Project: design.Project{
ID: "project-image-generator",
Nodes: []design.Node{
{
ID: "generator-1",
Type: design.NodeTypeImage,
Tone: "image-generator",
Status: "generating",
LayerRole: "image-generator",
Width: 512,
Height: 512,
},
},
},
Prompt: "画一只蓝色小鸟",
Mode: "image-generator",
ImageModel: "gpt-image-2",
ImageSize: "1024x1024",
Messages: []design.AgentChatMessage{
{
Role: "user",
Contents: []design.AgentContent{
{Type: "image", ImageURL: "https://example.com/reference.png"},
{Type: "text", Text: "画一只蓝色小鸟", TextSource: "input"},
{Type: "text", Text: "Image generator target node: generator-1", TextSource: "settings"},
},
},
},
TaskPlan: design.AgentTaskPlan{
Intent: "image",
ImageCount: 1,
ImageTasks: []design.AgentImageTask{
{Title: "蓝色小鸟", Brief: "画一只蓝色小鸟"},
},
},
}, func(result design.AgentImageGenerationResult) error {
emitted = append(emitted, result.Node)
return nil
})
if err != nil {
t.Fatal(err)
}
if runner.calls != 0 {
t.Fatalf("expected image generator mode to bypass base runner, got %d calls", runner.calls)
}
if creative.buildCalls != 0 || creative.summarizeCalls != 0 {
t.Fatalf("expected image generator mode to bypass gpt-5.5 helpers, got build=%d summarize=%d", creative.buildCalls, creative.summarizeCalls)
}
if !imageGenerator.called {
t.Fatal("expected image generator to be called")
}
if !strings.Contains(imageGenerator.prompt, "画一只蓝色小鸟") {
t.Fatalf("expected raw image generator prompt to reach image generator, got %q", imageGenerator.prompt)
}
if strings.Contains(imageGenerator.prompt, "rewritten by gpt-5.5") || strings.Contains(imageGenerator.prompt, "Create one polished") {
t.Fatalf("expected direct image generator prompt without analysis wrapper, got %q", imageGenerator.prompt)
}
if len(imageGenerator.opts.Images) != 1 || imageGenerator.opts.Images[0] != "https://example.com/reference.png" {
t.Fatalf("expected reference image to be passed directly, got %#v", imageGenerator.opts.Images)
}
if len(emitted) != 1 || len(plan.GeneratedNodes) != 1 {
t.Fatalf("expected one generated node, emitted=%#v plan=%#v", emitted, plan.GeneratedNodes)
}
node := plan.GeneratedNodes[0]
if node.ID != "generator-1" || node.LayerRole != "" || node.Tone != "visual" || node.Status != "success" {
t.Fatalf("expected image generator slot to become regular image, got %#v", node)
}
}
func (g *fakeImageGenerator) callCount() int {
g.mu.Lock()
defer g.mu.Unlock()
return g.calls
}
type fakeCreativeAgent struct{}
func (fakeCreativeAgent) Decide(ctx context.Context, req design.AgentDecisionRequest) (design.AgentDecision, error) {
return design.AgentDecision{}, nil
}
func (fakeCreativeAgent) Respond(ctx context.Context, req design.AgentConversationRequest) (string, error) {
return "", nil
}
func (fakeCreativeAgent) SummarizeImage(ctx context.Context, req design.AgentImageSummaryRequest) (string, error) {
return "这张图已经生成。建议下一步检查首屏构图、商品露出和转化按钮层级。", nil
}
func (fakeCreativeAgent) BuildImagePrompt(ctx context.Context, req design.AgentImagePromptRequest) (design.AgentImagePrompt, error) {
return design.AgentImagePrompt{
Title: "Still Day 首页升级",
Prompt: "基于 still day 现有首页方向,保留高饱和撞色,升级电商首屏、商品网格和品牌页脚。",
}, nil
}
type longCreativeAgent struct{}
func (longCreativeAgent) Decide(ctx context.Context, req design.AgentDecisionRequest) (design.AgentDecision, error) {
return design.AgentDecision{}, nil
}
func (longCreativeAgent) Respond(ctx context.Context, req design.AgentConversationRequest) (string, error) {
return "", nil
}
func (longCreativeAgent) SummarizeImage(ctx context.Context, req design.AgentImageSummaryRequest) (string, error) {
return "", nil
}
func (longCreativeAgent) BuildImagePrompt(ctx context.Context, req design.AgentImagePromptRequest) (design.AgentImagePrompt, error) {
return design.AgentImagePrompt{
Title: "精简生图指令",
Prompt: strings.Repeat("大量上下文不应该直接进入 gpt-image-2。", 240),
}, nil
}
func TestDesignAgentGeneratesImageThroughPiAgent(t *testing.T) {
imageGenerator := &fakeImageGenerator{}
agent := NewDesignAgent(fakeRunner{}, imageGenerator, fakeCreativeAgent{})
plan, err := agent.Plan(context.Background(), design.AgentRequest{
Prompt: "生成一张 still day 官网首页图片",
Mode: "design",
ImageModel: "gpt-image-2",
ImageSize: "1536x1024",
})
if err != nil {
t.Fatal(err)
}
if !imageGenerator.called {
t.Fatal("expected image generator to be called")
}
if !containsAll(imageGenerator.prompt, []string{"基于 still day 现有首页方向", "高饱和撞色"}) {
t.Fatalf("unexpected prompt: %s", imageGenerator.prompt)
}
if imageGenerator.opts.Model != "gpt-image-2" || imageGenerator.opts.Size != "1536x1024" {
t.Fatalf("unexpected image options: %#v", imageGenerator.opts)
}
if imageGenerator.opts.Count != 1 {
t.Fatalf("expected one image request, got %d", imageGenerator.opts.Count)
}
if len(plan.GeneratedNodes) != 1 {
t.Fatalf("expected one node, got %d", len(plan.GeneratedNodes))
}
node := plan.GeneratedNodes[0]
if node.Type != design.NodeTypeImage {
t.Fatalf("expected image node, got %s", node.Type)
}
if node.Content != "https://example.com/generated-1.png" {
t.Fatalf("expected generated image url, got %s", node.Content)
}
if node.Title != "Still Day 首页升级" {
t.Fatalf("expected generated image title, got %s", node.Title)
}
if node.Width != 1536 || node.Height != 1024 {
t.Fatalf("expected node size from request image size, got %.0fx%.0f", node.Width, node.Height)
}
if plan.Message.Title != "Still Day 首页升级 已生成" {
t.Fatalf("expected contextual completion title, got %s", plan.Message.Title)
}
if plan.Message.Content == "" {
t.Fatal("expected image generation summary")
}
if !containsAll(plan.Message.Content, []string{"首屏构图", "商品露出", "转化按钮"}) {
t.Fatalf("expected creative agent summary, got %s", plan.Message.Content)
}
}
func TestDesignAgentPassesInlineReferenceImagesToImageGenerator(t *testing.T) {
imageGenerator := &fakeImageGenerator{}
agent := NewDesignAgent(fakeRunner{}, imageGenerator, fakeCreativeAgent{})
_, err := agent.Plan(context.Background(), design.AgentRequest{
Prompt: "把参考图里的灰裤子拆成单件商品图",
Mode: "image",
ImageModel: "gpt-image-2",
Messages: []design.AgentChatMessage{
{
Role: "user",
Contents: []design.AgentContent{
{Type: "image", ImageURL: "https://example.com/pants-rack.png", ImageWidth: 1200, ImageHeight: 900},
{Type: "text", Text: "把参考图里的灰裤子拆成单件商品图", TextSource: "input"},
},
},
},
})
if err != nil {
t.Fatal(err)
}
if len(imageGenerator.opts.Images) != 1 || imageGenerator.opts.Images[0] != "https://example.com/pants-rack.png" {
t.Fatalf("expected reference image to be passed to GPT Image 2, got %#v", imageGenerator.opts.Images)
}
if !strings.Contains(imageGenerator.prompt, "参考图") {
t.Fatalf("expected prompt to tell image model to use attached reference, got %s", imageGenerator.prompt)
}
}
func TestDesignAgentPassesPromptDirectiveReferenceImagesToImageGenerator(t *testing.T) {
imageGenerator := &fakeImageGenerator{}
agent := NewDesignAgent(fakeRunner{}, imageGenerator, fakeCreativeAgent{})
_, err := agent.Plan(context.Background(), design.AgentRequest{
Prompt: "参考图 1image):https://example.com/pants-rack.png\n灰裤子拆成单件衣服,为了后面电商配图。",
Mode: "image",
ImageModel: "gpt-image-2",
})
if err != nil {
t.Fatal(err)
}
if len(imageGenerator.opts.Images) != 1 || imageGenerator.opts.Images[0] != "https://example.com/pants-rack.png" {
t.Fatalf("expected directive reference image to be passed to GPT Image 2, got %#v", imageGenerator.opts.Images)
}
}
func TestDesignAgentPreservesInlineReferencePlacementForImageGenerator(t *testing.T) {
imageGenerator := &fakeImageGenerator{}
agent := NewDesignAgent(fakeRunner{}, imageGenerator, fakeCreativeAgent{})
prompt := "参考图 10.jpg):https://example.com/mood.webp 为服装品牌 stillday 创建情绪板 logo 设计 参考图 2image.png):https://example.com/logo.png,包含材质、色彩、摄影姿态。"
_, err := agent.Plan(context.Background(), design.AgentRequest{
Prompt: prompt,
Mode: "image",
ImageModel: "gpt-image-2",
Messages: []design.AgentChatMessage{
{
Role: "user",
Contents: []design.AgentContent{
{Type: "text", Text: prompt, TextSource: "input"},
},
},
},
})
if err != nil {
t.Fatal(err)
}
if len(imageGenerator.opts.Images) != 2 || imageGenerator.opts.Images[0] != "https://example.com/mood.webp" || imageGenerator.opts.Images[1] != "https://example.com/logo.png" {
t.Fatalf("expected ordered reference images, got %#v", imageGenerator.opts.Images)
}
if !containsAll(imageGenerator.prompt, []string{"参考图 2", "image.png", "logo 设计"}) {
t.Fatalf("expected image prompt to carry inline placement semantics, got %s", imageGenerator.prompt)
}
}
func TestDesignAgentUsesDefaultImageSizeWithoutExplicitRequest(t *testing.T) {
imageGenerator := &fakeImageGenerator{}
agent := NewDesignAgent(fakeRunner{}, imageGenerator, fakeCreativeAgent{})
plan, err := agent.Plan(context.Background(), design.AgentRequest{
Prompt: "生成一张 still day 官网首页图片",
Mode: "design",
ImageModel: "gpt-image-2",
})
if err != nil {
t.Fatal(err)
}
if imageGenerator.opts.Size != "1024x1024" {
t.Fatalf("expected default image size, got %q", imageGenerator.opts.Size)
}
if len(plan.GeneratedNodes) != 1 {
t.Fatalf("expected one generated node, got %d", len(plan.GeneratedNodes))
}
if plan.GeneratedNodes[0].Width != 1024 || plan.GeneratedNodes[0].Height != 1024 {
t.Fatalf("expected default generated image size, got %.0fx%.0f", plan.GeneratedNodes[0].Width, plan.GeneratedNodes[0].Height)
}
}
func TestDesignAgentUsesExplicitPromptImageRatio(t *testing.T) {
imageGenerator := &fakeImageGenerator{}
agent := NewDesignAgent(fakeRunner{}, imageGenerator, fakeCreativeAgent{})
plan, err := agent.Plan(context.Background(), design.AgentRequest{
Prompt: "生成一张 16:9 的 still day 官网首页图片",
Mode: "design",
ImageModel: "gpt-image-2",
})
if err != nil {
t.Fatal(err)
}
if imageGenerator.opts.Size != "2048x1152" {
t.Fatalf("expected explicit 16:9 image size, got %q", imageGenerator.opts.Size)
}
if len(plan.GeneratedNodes) != 1 || plan.GeneratedNodes[0].Width != 2048 || plan.GeneratedNodes[0].Height != 1152 {
t.Fatalf("expected node size from explicit ratio, got %#v", plan.GeneratedNodes)
}
}
func TestDesignAgentGeneratesModelPlannedImageTasks(t *testing.T) {
imageGenerator := &fakeImageGenerator{}
agent := NewDesignAgent(fakeRunner{}, imageGenerator, fakeCreativeAgent{})
plan, err := agent.Plan(context.Background(), design.AgentRequest{
Prompt: "设计二次元水墨人物形象,输出角色正面、侧面、细节和色彩方案。",
Mode: "image",
ImageModel: "gpt-image-2",
ImageSize: "1024x1024",
TaskPlan: design.AgentTaskPlan{
Intent: "image",
ImageCount: 3,
ImageTasks: []design.AgentImageTask{
{Title: "水墨角色_正面", Brief: "正面全身立绘"},
{Title: "水墨角色_侧面", Brief: "侧面全身立绘"},
{Title: "水墨角色_细节", Brief: "局部细节拆解"},
},
},
Project: design.Project{
Nodes: []design.Node{
{ID: "slot-1", Type: design.NodeTypeImage, Status: "generating", X: 10},
{ID: "slot-2", Type: design.NodeTypeImage, Status: "generating", X: 20},
{ID: "slot-3", Type: design.NodeTypeImage, Status: "generating", X: 30},
},
},
})
if err != nil {
t.Fatal(err)
}
if imageGenerator.callCount() != 3 {
t.Fatalf("expected 3 image calls, got %d", imageGenerator.callCount())
}
idempotencyKeys := make(map[string]bool)
for _, opts := range imageGenerator.options {
if opts.Count != 1 {
t.Fatalf("expected individual image request count 1, got %#v", imageGenerator.options)
}
if opts.IdempotencyKey == "" {
t.Fatalf("expected idempotency key for image request, got %#v", imageGenerator.options)
}
if idempotencyKeys[opts.IdempotencyKey] {
t.Fatalf("expected unique idempotency keys, got %#v", imageGenerator.options)
}
idempotencyKeys[opts.IdempotencyKey] = true
}
if len(plan.GeneratedNodes) != 3 {
t.Fatalf("expected 3 generated nodes, got %#v", plan.GeneratedNodes)
}
for index, node := range plan.GeneratedNodes {
wantID := "slot-" + strconv.Itoa(index+1)
if node.ID != wantID || node.Content == "" || node.Status != "success" {
t.Fatalf("unexpected generated node %d: %#v", index, node)
}
}
}
func TestDesignAgentGeneratesWhenTaskPlanIntentIsImageEvenInAgentMode(t *testing.T) {
imageGenerator := &fakeImageGenerator{}
agent := NewDesignAgent(fakeRunner{}, imageGenerator, fakeCreativeAgent{})
plan, err := agent.PlanIncremental(context.Background(), design.AgentRequest{
Prompt: "来一版",
Mode: "agent",
ImageModel: "gpt-image-2",
TaskPlan: design.AgentTaskPlan{
Intent: "image",
ImageCount: 1,
ImageTasks: []design.AgentImageTask{
{Title: "画面方案", Brief: "生成一个高级电商主视觉,强调产品层次和留白。"},
},
},
Project: design.Project{
Nodes: []design.Node{
{ID: "slot-1", Type: design.NodeTypeImage, Status: "generating", X: 10},
},
},
}, nil)
if err != nil {
t.Fatal(err)
}
if imageGenerator.callCount() != 1 {
t.Fatalf("expected image generation to run from task plan intent, got %d calls", imageGenerator.callCount())
}
if len(plan.GeneratedNodes) != 1 || plan.GeneratedNodes[0].Content == "" {
t.Fatalf("expected generated image node, got %#v", plan.GeneratedNodes)
}
}
func TestDesignAgentReturnsPartialFailureWhenPlannedImageIsMissing(t *testing.T) {
imageGenerator := &partialFailImageGenerator{}
agent := NewDesignAgent(fakeRunner{}, imageGenerator, fakeCreativeAgent{})
var emitted []design.Node
plan, err := agent.PlanIncremental(context.Background(), design.AgentRequest{
Prompt: "生成两张视觉",
Mode: "image",
TaskPlan: design.AgentTaskPlan{
Intent: "image",
ImageCount: 2,
ImageTasks: []design.AgentImageTask{
{Title: "方案一", Brief: "生成第一张视觉"},
{Title: "方案二", Brief: "生成第二张视觉"},
},
},
Project: design.Project{
Nodes: []design.Node{
{ID: "slot-1", Type: design.NodeTypeImage, Status: "generating"},
{ID: "slot-2", Type: design.NodeTypeImage, Status: "generating"},
},
},
}, func(result design.AgentImageGenerationResult) error {
emitted = append(emitted, result.Node)
return nil
})
if err == nil {
t.Fatal("expected partial generation failure")
}
if !strings.Contains(err.Error(), "only 1 of 2 planned images returned") {
t.Fatalf("expected partial failure count, got %v", err)
}
if len(emitted) != 1 || len(plan.GeneratedNodes) != 1 {
t.Fatalf("expected one successful node to be preserved, emitted=%#v plan=%#v", emitted, plan.GeneratedNodes)
}
}
func TestGeneratedImageSlotsIgnoreCanvasActionPlaceholders(t *testing.T) {
slots := generatedImageSlots(design.AgentRequest{
Project: design.Project{
Nodes: []design.Node{
{ID: "background-placeholder", Type: design.NodeTypeImage, Status: "generating", LayerRole: "background-removed"},
{ID: "agent-placeholder", Type: design.NodeTypeImage, Status: "generating"},
},
},
}, nil, 1)
if len(slots) != 1 || slots[0].ID != "agent-placeholder" {
t.Fatalf("expected agent placeholder slot, got %#v", slots)
}
}
func TestGeneratedImageNodeFromImageGeneratorSlotBecomesRegularImage(t *testing.T) {
node := generatedImageNodeFromSlot(
design.Node{
ID: "generator-1",
Type: design.NodeTypeImage,
Tone: "image-generator",
Status: "generating",
LayerRole: "image-generator",
Width: 320,
Height: 240,
},
design.AgentRequest{Prompt: "生成一张小猫图片"},
"https://example.com/cat.png",
"小猫",
0,
1,
)
if node.LayerRole != "" || node.Tone != "visual" || node.Status != "success" {
t.Fatalf("expected regular completed image node, got %#v", node)
}
}
func TestDesignAgentDoesNotRunFallbackAfterImageTimeout(t *testing.T) {
imageGenerator := &failingImageGenerator{err: context.DeadlineExceeded}
agent := NewDesignAgent(fakeRunner{}, imageGenerator, fakeCreativeAgent{})
_, err := agent.Plan(context.Background(), design.AgentRequest{
Prompt: "生成一张 still day 官网首页图片",
Mode: "image",
ImageModel: "gpt-image-2",
})
if err == nil {
t.Fatal("expected image timeout error")
}
if imageGenerator.calls != 1 {
t.Fatalf("expected timeout to skip fallback generation, got %d calls", imageGenerator.calls)
}
}
func TestDesignAgentCompactsPromptBeforeGPTImage(t *testing.T) {
imageGenerator := &fakeImageGenerator{}
agent := NewDesignAgent(fakeRunner{}, imageGenerator, longCreativeAgent{})
_, err := agent.Plan(context.Background(), design.AgentRequest{
Prompt: "根据当前画布生成一张电商主视觉",
Mode: "image",
ImageModel: "gpt-image-2",
})
if err != nil {
t.Fatal(err)
}
if !imageGenerator.called {
t.Fatal("expected image generator to be called")
}
if utf8.RuneCountInString(imageGenerator.prompt) > imageGenerationPromptTooLargeRunes {
t.Fatalf("expected compact image prompt, got %d runes", utf8.RuneCountInString(imageGenerator.prompt))
}
if containsAll(imageGenerator.prompt, []string{"任务类型", "主体描述", "风格定义", "技术参数", "输出规格"}) {
t.Fatalf("expected compact prompt without forced framework, got %s", imageGenerator.prompt)
}
}
func containsAll(value string, needles []string) bool {
for _, needle := range needles {
if !strings.Contains(value, needle) {
return false
}
}
return true
}