package platform import ( "encoding/json" "fmt" "testing" ) const modelRootID = "10000000-0000-4000-8000-000000000001" const modelChildID = "10000000-0000-4000-8000-000000000002" const modelLegacyFrame = `{"id":"f","type":"frame","position":{"x":100,"y":200},"width":640,"height":400,"data":{"kind":"frame","title":"Partition","text":"","labelMode":"fixed"}}` func modelJSON(value any) json.RawMessage { raw, _ := json.Marshal(value); return raw } func modelTestID(n int) string { return fmt.Sprintf("90000000-0000-4000-8000-%012d", n) } func modelTestAllocator() func() string { next := 0 return func() string { next++; return modelTestID(next) } } func modelTestNode(id, owner string) map[string]any { return map[string]any{"id": id, "incarnation": float64(1), "ownerCanvasId": owner, "componentId": "text", "dataVersion": float64(1), "structure": "node", "data": map[string]any{"title": id, "text": "preserved"}, "geometry": map[string]any{"x": float64(0), "y": float64(0), "width": float64(260), "height": float64(200), "rotation": float64(0)}} } func modelTestDocument() canvasModel { model := emptyModel(modelRootID) model.Canvases[modelRootID] = modelCanvas{ID: modelRootID} model.Versions[modelRootID] = 1 model.Elements["a"] = modelTestNode("a", modelRootID) return model } func TestCanonicalMigrationResolvesCanvasLocalIdentitiesAndPorts(t *testing.T) { root := `{"schema_version":2,"nodes":[{"id":"a","type":"plainText","position":{"x":0,"y":0},"width":240,"height":72,"data":{"kind":"plain-text","title":"parent","text":""}},{"id":"f","type":"frame","position":{"x":100,"y":200},"width":640,"height":400,"data":{"kind":"frame","title":"F","text":"","subgraph":{"boardId":"` + modelChildID + `"},"ports":[{"id":"p","nodeId":"a","handle":"right","direction":"output"}]}}],"edges":[{"id":"e","source":"f","sourceHandle":"port:p","target":"a","targetHandle":"left"}],"viewport":{"x":0,"y":0,"zoom":1}}` child := `{"schema_version":2,"nodes":[{"id":"a","type":"plainText","position":{"x":10,"y":20},"width":240,"height":72,"data":{"kind":"plain-text","title":"child","text":""}}],"edges":[],"viewport":{"x":0,"y":0,"zoom":1}}` snapshots := map[string]canvasSnapshot{modelRootID: {BoardID: modelRootID, Version: 3, Document: json.RawMessage(root)}, modelChildID: {BoardID: modelChildID, Version: 7, Document: json.RawMessage(child)}} result, err := migrateCanvasModel(modelRootID, snapshots, []subgraphRelation{{Parent: modelRootID, Frame: "f", Child: modelChildID}}, modelTestAllocator()) if err != nil { t.Fatal(err) } ids := map[string]string{} for _, mapping := range result.Identities { ids[mapping.SourceCanvasID+"/"+mapping.Structure+"/"+mapping.SourceID] = mapping.ElementID } parentA, childA := ids[modelRootID+"/node/a"], ids[modelChildID+"/node/a"] if parentA == childA || parentA == "" || childA == "" { t.Fatal("canvas-local node IDs were merged") } edge := result.Model.Elements[ids[modelRootID+"/connection/e"]] if edge["source"].(map[string]any)["elementId"] != childA || edge["target"].(map[string]any)["elementId"] != parentA || edge["ownerCanvasId"] != modelRootID { t.Fatal("logical endpoint or common ancestor owner was lost") } if result.Model.Canvases[modelChildID].Parent.EntryElementID != ids[modelRootID+"/node/f"] || result.Model.Versions[modelRootID] != 4 || result.Model.Versions[modelChildID] != 8 { t.Fatal("migration did not publish an identity/version barrier") } if string(snapshots[modelRootID].Document) != root || string(snapshots[modelChildID].Document) != child { t.Fatal("migration mutated source snapshots") } projection, err := projectCanvasModel(result.Model) if err != nil { t.Fatal(err) } for _, raw := range projection { if _, valid := normalizeCanvas(raw); !valid { t.Fatalf("invalid compatibility projection: %s", raw) } } } func TestCanonicalMigrationTurnsImplicitFramesIntoOwningCanvases(t *testing.T) { root := `{"schema_version":1,"nodes":[` + modelLegacyFrame + `,{"id":"a","type":"plainText","position":{"x":150,"y":260},"width":240,"height":72,"data":{"kind":"plain-text","title":"a","text":"","frameId":"f"}}],"edges":[],"viewport":{"x":0,"y":0,"zoom":1}}` result, err := migrateCanvasModel(modelRootID, map[string]canvasSnapshot{modelRootID: {BoardID: modelRootID, Version: 1, Document: json.RawMessage(root)}}, nil, modelTestAllocator()) if err != nil { t.Fatal(err) } if len(result.NewCanvases) != 1 || len(result.Model.Canvases) != 2 { t.Fatal("legacy partition was not given a canvas") } var node map[string]any for _, mapping := range result.Identities { if mapping.SourceID == "a" { node = result.Model.Elements[mapping.ElementID] } } geometry := node["geometry"].(map[string]any) if node["ownerCanvasId"] != result.NewCanvases[0] || geometry["x"] != float64(26) || geometry["y"] != float64(24) { t.Fatal("membership or owner-local geometry changed") } if _, exists := node["data"].(map[string]any)["frameId"]; exists { t.Fatal("second ownership fact leaked into canonical data") } } func TestCanonicalOperationsPreserveOpaquePayloadAndStableMoveIdentity(t *testing.T) { model := modelTestDocument() frame := modelTestNode("f", modelRootID) frame["componentId"] = "frame" model.Elements["f"] = frame model.Canvases[modelChildID] = modelCanvas{ID: modelChildID, Parent: &modelParent{CanvasID: modelRootID, EntryElementID: "f"}} model.Versions[modelChildID] = 1 future := modelTestNode("future", modelRootID) future["componentId"], future["dataVersion"], future["data"], future["futureEnvelope"] = "plugin.future", float64(42), map[string]any{"nested": map[string]any{"list": []any{"x", float64(2)}}}, map[string]any{"retain": true} model.Elements["future"] = future op := modelOperation{ID: modelTestID(1), Changes: []modelChange{{Kind: "field", Target: modelTarget{ID: "a", Incarnation: 1}, Field: "ownerCanvasId", Before: modelJSON(modelRootID), After: modelJSON(modelChildID)}}} after, touched, err := applyModelOperations(model, []modelOperation{op}) if err != nil { t.Fatal(err) } if after.Elements["a"]["id"] != "a" || after.Elements["a"]["ownerCanvasId"] != modelChildID || !touched[modelRootID] || !touched[modelChildID] { t.Fatal("move changed identity or omitted a participant") } if !modelEqual(after.Elements["future"], future) || model.Elements["a"]["ownerCanvasId"] != modelRootID { t.Fatal("opaque payload or old snapshot was mutated") } } func TestCanonicalDeleteRestoreRejectsOldIncarnation(t *testing.T) { model := modelTestDocument() deleted, _, err := applyModelOperations(model, []modelOperation{{ID: modelTestID(1), Changes: []modelChange{{Kind: "element", ID: "a", Before: modelJSON(model.Elements["a"]), After: modelJSON(nil)}}}}) if err != nil { t.Fatal(err) } if deleted.Tombstones["a"] != 1 { t.Fatal("missing tombstone") } restored := modelTestNode("a", modelRootID) restored["incarnation"] = float64(2) live, _, err := applyModelOperations(deleted, []modelOperation{{ID: modelTestID(2), Changes: []modelChange{{Kind: "element", ID: "a", Before: modelJSON(nil), After: modelJSON(restored)}}}}) if err != nil { t.Fatal(err) } _, _, err = applyModelOperations(live, []modelOperation{{ID: modelTestID(3), Changes: []modelChange{{Kind: "field", Target: modelTarget{ID: "a", Incarnation: 1}, Field: "data.title", Before: modelJSON("a"), After: modelJSON("stale")}}}}) if err != errModelConflict || live.Elements["a"]["data"].(map[string]any)["title"] != "a" { t.Fatal("old callback claimed the restored incarnation") } } func TestCanonicalValidationRejectsPartialChangesAndWrongEdgeOwner(t *testing.T) { model := modelTestDocument() before := modelJSON(model) operations := []modelOperation{{ID: modelTestID(1), Changes: []modelChange{{Kind: "field", Target: modelTarget{ID: "a", Incarnation: 1}, Field: "data.title", Before: modelJSON("a"), After: modelJSON("changed")}, {Kind: "field", Target: modelTarget{ID: "missing", Incarnation: 1}, Field: "geometry", Before: modelJSON(nil), After: modelJSON(nil)}}}} if _, _, err := applyModelOperations(model, operations); err == nil { t.Fatal("invalid second change was accepted") } if string(modelJSON(model)) != string(before) { t.Fatal("failed batch changed the source") } frame := modelTestNode("f", modelRootID) frame["componentId"] = "frame" model.Elements["f"] = frame model.Canvases[modelChildID] = modelCanvas{ID: modelChildID, Parent: &modelParent{CanvasID: modelRootID, EntryElementID: "f"}} model.Versions[modelChildID] = 1 model.Elements["child"] = modelTestNode("child", modelChildID) model.Elements["e"] = map[string]any{"id": "e", "ownerCanvasId": modelChildID, "incarnation": float64(1), "componentId": "connection", "dataVersion": float64(1), "structure": "connection", "data": map[string]any{}, "source": map[string]any{"elementId": "a"}, "target": map[string]any{"elementId": "child"}, "path": nil} if validateCanvasModel(model) == nil { t.Fatal("cross-child connection acquired a child owner") } }