package platform import ( "encoding/json" "reflect" "testing" ) const legacyMigrationFixture = `{"schema_version":1,"nodes":[ {"id":"f","type":"frame","position":{"x":100,"y":200},"width":640,"height":400,"data":{"kind":"frame","title":"Frame","text":"","labelMode":"fixed"}}, {"id":"empty","type":"frame","position":{"x":0,"y":0},"width":400,"height":300,"data":{"kind":"frame","title":"Empty","text":""}}, {"id":"a","type":"material","position":{"x":150,"y":260},"data":{"kind":"text","title":"A","text":"preserved","frameId":"f","fontSize":24}}, {"id":"b","type":"material","position":{"x":250,"y":300},"data":{"kind":"script","title":"B","text":"","frameId":"f"}}, {"id":"outside","type":"material","position":{"x":160,"y":270},"data":{"kind":"text","title":"Not a member","text":"overlaps frame"}} ],"edges":[{"id":"internal","source":"a","target":"b","sourceHandle":"right","targetHandle":"left"},{"id":"boundary","source":"b","target":"outside","sourceHandle":"right","targetHandle":"left"},{"id":"boundary-2","source":"b","target":"outside","sourceHandle":"right","targetHandle":"top"}],"viewport":{"x":2000,"y":3000,"zoom":2}}` func TestLegacyMigrationOwnershipAndBoundary(t *testing.T) { parent := canvasSnapshot{BoardID: "parent", Version: 5, Document: json.RawMessage(legacyMigrationFixture)} plan := legacyPlan("p", "w", parent) if plan.ChildCount != 2 || len(plan.Frames) != 2 { t.Fatal("empty frame omitted") } if !reflect.DeepEqual(plan.Frames[1].NodeIDs, []string{"a", "b"}) { t.Fatalf("non-explicit member included: %+v", plan.Frames) } children := map[string]string{"f": "11111111-1111-4111-8111-111111111111", "empty": "22222222-2222-4222-8222-222222222222"} migrated, docs, err := migrateLegacyDocument(parent.Document, children) if err != nil { t.Fatal(err) } data := canvasNodeData(migrated) if data["a"] != nil || data["b"] != nil || data["outside"] == nil { t.Fatal("ownership duplicated or overlapping node absorbed") } ports := data["f"]["ports"].([]any) if len(ports) != 1 { t.Fatalf("ports not deduplicated: %+v", ports) } if ports[0].(map[string]any)["nodeId"] != "b" { t.Fatal("boundary port targets wrong child node") } var child map[string]any _ = json.Unmarshal(docs["f"], &child) nodes := child["nodes"].([]any) if len(nodes) != 2 { t.Fatal("child membership") } a := nodes[0].(map[string]any) position := a["position"].(map[string]any) if position["x"] != float64(26) || position["y"] != float64(24) { t.Fatalf("inverse projection used viewport: %v", position) } if a["data"].(map[string]any)["text"] != "preserved" || a["data"].(map[string]any)["frameId"] != nil { t.Fatal("member data changed") } if len(child["edges"].([]any)) != 1 { t.Fatal("internal edge lost") } var p map[string]any _ = json.Unmarshal(migrated, &p) if len(p["edges"].([]any)) != 2 { t.Fatal("boundary edges lost") } for _, v := range p["edges"].([]any) { e := v.(map[string]any) if e["source"] != "f" { t.Fatal("boundary edge not remapped") } } // A second plan skips linked frames and does not create more children. if legacyPlan("p", "w", canvasSnapshot{BoardID: "parent", Version: 6, Document: migrated}).ChildCount != 0 { t.Fatal("migration not repeatable") } changed := parent changed.Version++ if legacyPlan("p", "w", changed).PlanID == plan.PlanID { t.Fatal("plan not bound to version") } } func TestMigrationPreservesBezierGeometry(t *testing.T) { var doc map[string]any _ = json.Unmarshal([]byte(legacyMigrationFixture), &doc) doc["schema_version"] = 2 e := doc["edges"].([]any)[0].(map[string]any) e["data"] = map[string]any{"path": map[string]any{"type": "bezier-chain", "startControl": map[string]any{"x": 90, "y": 0}, "knots": []any{map[string]any{"id": "k", "position": map[string]any{"x": 200, "y": 280}, "in": map[string]any{"x": -20, "y": 0}, "out": map[string]any{"x": 20, "y": 0}, "mode": "symmetric"}}}} raw, _ := json.Marshal(doc) _, children, err := migrateLegacyDocument(raw, map[string]string{"f": "11111111-1111-4111-8111-111111111111", "empty": "22222222-2222-4222-8222-222222222222"}) if err != nil { t.Fatal(err) } var child map[string]any _ = json.Unmarshal(children["f"], &child) path := child["edges"].([]any)[0].(map[string]any)["data"].(map[string]any)["path"].(map[string]any) knot := path["knots"].([]any)[0].(map[string]any) if knot["position"].(map[string]any)["x"] != float64(76) || knot["position"].(map[string]any)["y"] != float64(44) { t.Fatal("knot position not translated") } if knot["in"].(map[string]any)["x"] != float64(-20) || path["startControl"].(map[string]any)["x"] != float64(90) { t.Fatal("control vector translated") } }