Files
root 44406b72db Initial commit: img-infinite-canvas AI design workbench MVP
Moteva-style AI design workbench replica. Home prompt creates a project;
the project page provides an infinite canvas with node drag, zoom/pan,
a design chat panel, history replay, image asset upload, and project
save/regenerate.

- Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory
  or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage;
  sky-valley/pi agent runtime adapter.
- Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n,
  shadcn/ui components, auth (OTP/Turnstile/Google/WeChat).
- Deploy: Docker Compose and k3s manifests.

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

235 lines
8.0 KiB
Go

package application
import (
"context"
"image/color"
"testing"
"time"
"img_infinite_canvas/internal/domain/design"
"img_infinite_canvas/internal/infrastructure/memory"
)
func TestGenerateMockupModelUsesAnalyzerAndPersistsInlineModelData(t *testing.T) {
ctx := context.Background()
repo := memory.NewProjectRepository()
service := NewDesignService(repo, nil, nil, nil, nil)
analyzer := &fakeMockupAnalyzer{}
service.SetMockupModelAnalyzer(analyzer)
project := design.Project{
ID: "project-mockup-model",
Title: "Mockup model",
Status: design.StatusReady,
UpdatedAt: time.Now(),
Nodes: []design.Node{
{
ID: "target",
Type: design.NodeTypeImage,
Title: "Target",
Width: 640,
Height: 800,
Content: testPNGDataURL(color.NRGBA{R: 220, G: 220, B: 210, A: 255}, 6, 8),
Status: "success",
},
},
}
if err := repo.Save(ctx, project); err != nil {
t.Fatal(err)
}
first, err := service.GenerateMockupModel(ctx, project.ID, "target")
if err != nil {
t.Fatal(err)
}
if !hasMockupModel(first.Model) {
t.Fatalf("expected generated mockup model, got %#v", first.Model)
}
if len(first.Model.DepthMap.Data) != first.Model.DepthMap.Width*first.Model.DepthMap.Height {
t.Fatalf("expected inline depth data, got %#v", first.Model.DepthMap)
}
saved, err := repo.Get(ctx, project.ID)
if err != nil {
t.Fatal(err)
}
if !hasMockupModel(saved.Nodes[0].MockupModel) {
t.Fatalf("expected model to be persisted on target node, got %#v", saved.Nodes[0].MockupModel)
}
if saved.Nodes[0].MockupSourceContent != project.Nodes[0].Content {
t.Fatal("expected source content fingerprint to be persisted")
}
second, err := service.GenerateMockupModel(ctx, project.ID, "target")
if err != nil {
t.Fatal(err)
}
if analyzer.calls != 1 {
t.Fatalf("expected cached inline model to avoid a second analyzer call, got %d", analyzer.calls)
}
if len(second.Model.DepthMap.Data) != len(first.Model.DepthMap.Data) {
t.Fatal("expected cached inline model data")
}
}
func TestGenerateMockupModelAsyncQueuesAndCompletesInlineModel(t *testing.T) {
ctx := context.Background()
repo := memory.NewProjectRepository()
queue := &fakeJobQueue{}
service := NewDesignService(repo, nil, nil, nil, nil)
service.SetJobQueue(queue)
service.SetMockupModelAnalyzer(&fakeMockupAnalyzer{})
project := design.Project{
ID: "project-mockup-model-async",
Title: "Mockup model async",
Status: design.StatusReady,
UpdatedAt: time.Now(),
Nodes: []design.Node{
{
ID: "target",
Type: design.NodeTypeImage,
Title: "Target",
Width: 640,
Height: 800,
Content: testPNGDataURL(color.NRGBA{R: 220, G: 220, B: 210, A: 255}, 6, 8),
Status: "success",
},
},
}
if err := repo.Save(ctx, project); err != nil {
t.Fatal(err)
}
thread, err := service.GenerateMockupModelAsync(ctx, project.ID, "target")
if err != nil {
t.Fatal(err)
}
if thread.Status != "running" || thread.ThreadID == "" {
t.Fatalf("expected running mockup thread, got %#v", thread)
}
if len(queue.jobs) != 1 || queue.jobs[0].Kind != design.JobMockupModelGeneration || queue.jobs[0].ThreadID != thread.ThreadID {
t.Fatalf("expected mockup model job, got %#v", queue.jobs)
}
saved, err := repo.Get(ctx, project.ID)
if err != nil {
t.Fatal(err)
}
if saved.Nodes[0].Status != "mockup-modeling" || saved.Status != design.StatusExploring {
t.Fatalf("expected mockup node to be marked active, got project=%s node=%#v", saved.Status, saved.Nodes[0])
}
if len(saved.Messages) != 2 || saved.Messages[0].Name != "mockup_model" || saved.Messages[0].Status != "running" || saved.Messages[1].Name != "generator_task" {
t.Fatalf("expected running mockup message, got %#v", saved.Messages)
}
if err := service.ProcessJob(ctx, queue.jobs[0]); err != nil {
t.Fatal(err)
}
saved, err = repo.Get(ctx, project.ID)
if err != nil {
t.Fatal(err)
}
if saved.Nodes[0].Status != "success" || !mockupModelCurrent(saved.Nodes[0]) {
t.Fatalf("expected completed inline mockup model, got %#v", saved.Nodes[0])
}
if len(saved.Messages) != 3 || saved.Messages[2].Name != "mockup_model" || saved.Messages[2].Status != "success" {
t.Fatalf("expected successful mockup message, got %#v", saved.Messages)
}
}
func TestNormalizeMockupModelMapRepairsNearMissDataLength(t *testing.T) {
shortData := make([]float64, 254)
for index := range shortData {
shortData[index] = 0.42
}
normalized, err := normalizeMockupModelMap(design.MockupModelMap{Width: 16, Height: 16, Data: shortData}, "depthMap", 0, 1)
if err != nil {
t.Fatal(err)
}
if len(normalized.Data) != 256 {
t.Fatalf("expected repaired 16x16 data, got %d", len(normalized.Data))
}
if normalized.Data[254] != 0.42 || normalized.Data[255] != 0.42 {
t.Fatalf("expected missing tail values to be filled from edge value, got tail %#v", normalized.Data[252:])
}
longData := make([]float64, 260)
for index := range longData {
longData[index] = float64(index)
}
normalized, err = normalizeMockupModelMap(design.MockupModelMap{Width: 16, Height: 16, Data: longData}, "depthMap", 0, 255)
if err != nil {
t.Fatal(err)
}
if len(normalized.Data) != 256 || normalized.Data[255] != 255 {
t.Fatalf("expected long data to be cropped and clamped, got len=%d last=%v", len(normalized.Data), normalized.Data[255])
}
}
func TestNormalizeMockupAnalysisRepairsLogoBoundSurfaceAndMismatchedSegmentation(t *testing.T) {
target := design.Node{ID: "target", Type: design.NodeTypeImage, Width: 800, Height: 800}
analysis := fakeMockupAnalysis()
analysis.Placement = design.MockupPlacement{X: 0.32, Y: 0.28, Width: 0.36, Height: 0.16}
analysis.Surface.Polygon = []design.MockupPoint{{X: 0.32, Y: 0.28}, {X: 0.68, Y: 0.28}, {X: 0.68, Y: 0.44}, {X: 0.32, Y: 0.44}}
analysis.Model.SegmentationMap = mockupTestMap(32, 32, bottomOnlyMockupMask(32, 32))
analysis.Model.CameraIntrinsics.Matrix = []float64{1.4, 0, 333.5, 0, 1.4, 333.5, 0, 0, 1}
placement, surface, model, err := normalizeMockupAnalysis(target, analysis)
if err != nil {
t.Fatal(err)
}
if placement.Height < 0.29 {
t.Fatalf("expected logo-bound surface to expand to the usable print zone, got %#v", placement)
}
if coverage := mockupSegmentationSurfaceCoverage(model.SegmentationMap, surface.Polygon); coverage < 0.7 {
t.Fatalf("expected segmentation to be repaired against printable surface, got coverage %.2f", coverage)
}
if got := model.CameraIntrinsics.Matrix[2]; got < 0 || got > 1 {
t.Fatalf("expected normalized cx, got %v", got)
}
}
type fakeMockupAnalyzer struct {
calls int
}
func (f *fakeMockupAnalyzer) AnalyzeMockupModel(ctx context.Context, req design.MockupModelAnalysisRequest) (design.MockupModelAnalysis, error) {
f.calls++
return fakeMockupAnalysis(), nil
}
func fakeMockupAnalysis() design.MockupModelAnalysis {
return design.MockupModelAnalysis{
Placement: design.MockupPlacement{X: 0.2, Y: 0.25, Width: 0.4, Height: 0.5},
Surface: design.MockupSurface{
Polygon: []design.MockupPoint{{X: 0.2, Y: 0.25}, {X: 0.6, Y: 0.25}, {X: 0.6, Y: 0.75}, {X: 0.2, Y: 0.75}},
},
Model: design.MockupModel{
Version: 2,
DepthMap: mockupTestMap(2, 2, []float64{0.45, 0.5, 0.52, 0.58}),
DepthScaleMin: 0,
DepthScaleMax: 1,
ShadeMap: mockupTestMap(2, 2, []float64{0.8, 0.82, 0.78, 0.8}),
SegmentationMap: mockupTestMap(2, 2, []float64{0, 255, 255, 0}),
XOffsetMap: mockupTestMap(2, 2, []float64{-0.1, 0.1, -0.08, 0.08}),
YOffsetMap: mockupTestMap(2, 2, []float64{0.02, 0.04, -0.02, -0.04}),
CameraIntrinsics: design.MockupCameraIntrinsics{
Matrix: []float64{1.4, 0, 0.5, 0, 1.4, 0.5, 0, 0, 1},
},
ColorCorrection: "limited_brightness",
},
}
}
func mockupTestMap(width int, height int, data []float64) design.MockupModelMap {
return design.MockupModelMap{Width: width, Height: height, Data: data}
}
func bottomOnlyMockupMask(width int, height int) []float64 {
data := make([]float64, width*height)
for y := height * 3 / 4; y < height; y++ {
for x := 0; x < width; x++ {
data[y*width+x] = 255
}
}
return data
}