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: "参考图 1(image):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 := "参考图 1(0.jpg):https://example.com/mood.webp 为服装品牌 stillday 创建情绪板 logo 设计 参考图 2(image.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 }