package platform import ( "bytes" _ "embed" "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"}}` // The schema runner mounts only the test binary, not the source tree. // //go:embed testdata/foundry-model-operation-v2.json var canonicalFrontendFixture []byte 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 TestCanonicalFramesRetainRotationAcrossOwnerChanges(t *testing.T) { model := modelTestDocument() frame := modelTestNode("f", modelRootID) frame["componentId"] = "frame" frame["geometry"].(map[string]any)["rotation"] = float64(30) model.Elements["f"] = frame model.Canvases[modelChildID] = modelCanvas{ID: modelChildID, Parent: &modelParent{CanvasID: modelRootID, EntryElementID: "f"}} model.Versions[modelChildID] = 1 member := modelTestNode("nested-frame", modelChildID) member["componentId"] = "frame" model.Elements["nested-frame"] = member if err := validateCanvasModel(model); err != nil { t.Fatalf("valid canonical frame rotation rejected: %v", err) } geometry := member["geometry"].(map[string]any) moved := map[string]any{"x": float64(100), "y": float64(80), "width": float64(520), "height": float64(400), "rotation": float64(30)} after, touched, err := applyModelOperations(model, []modelOperation{{ID: modelTestID(80), Changes: []modelChange{ {Kind: "field", Target: modelTarget{ID: "nested-frame", Incarnation: 1}, Field: "ownerCanvasId", Before: modelJSON(modelChildID), After: modelJSON(modelRootID)}, {Kind: "field", Target: modelTarget{ID: "nested-frame", Incarnation: 1}, Field: "geometry", Before: modelJSON(geometry), After: modelJSON(moved)}, }}}) if err != nil || !touched[modelRootID] || !touched[modelChildID] { t.Fatalf("rotated frame owner change failed: %v", err) } if after.Elements["nested-frame"]["ownerCanvasId"] != modelRootID || !modelEqual(after.Elements["nested-frame"]["geometry"], moved) || model.Elements["nested-frame"]["ownerCanvasId"] != modelChildID { t.Fatal("owner change lost geometry or changed the source model") } projection, err := projectCanvasModel(after) if err != nil { t.Fatal(err) } var document map[string]any if err := json.Unmarshal(projection[modelRootID], &document); err != nil { t.Fatal(err) } rotated := 0 for _, raw := range document["nodes"].([]any) { node := raw.(map[string]any) if node["id"] != "f" && node["id"] != "nested-frame" { continue } rotated++ if node["data"].(map[string]any)["rotation"] != float64(30) { t.Fatal("reader projection erased canonical rotation") } } if rotated != 2 { t.Fatal("reader projection omitted a rotated frame") } patched, err := prepareModelAIPatch(after, modelRootID, "a", response{"text": "new text"}) if err != nil || patched.Elements["f"]["geometry"].(map[string]any)["rotation"] != float64(30) { t.Fatalf("unrelated canonical AI patch rejected or rewound rotation: %v", err) } bad := cloneModel(after) bad.Elements["f"]["geometry"].(map[string]any)["rotation"] = float64(360) if validateCanvasModel(bad) == nil { t.Fatal("out-of-range canonical rotation accepted") } legacy := json.RawMessage(`{"schema_version":1,"nodes":[{"id":"f","type":"frame","position":{"x":0,"y":0},"width":640,"height":400,"data":{"kind":"frame","title":"F","text":"","rotation":30}}],"edges":[],"viewport":{"x":0,"y":0,"zoom":1}}`) if _, valid := normalizeCanvas(legacy); valid { t.Fatal("canonical support changed the legacy snapshot write contract") } } 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} future["data"].(map[string]any)["constructor"] = map[string]any{"retain": true} future["data"].(map[string]any)["__proto__"] = map[string]any{"ordinary": "JSON data"} 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") } } func TestCanonicalFrontendProtocolFixture(t *testing.T) { var input modelBatch decoder := json.NewDecoder(bytes.NewReader(canonicalFrontendFixture)) decoder.DisallowUnknownFields() if err := decoder.Decode(&input); err != nil { t.Fatal(err) } if input.ProtocolVersion != canvasModelProtocol || !uuidSlugPattern.MatchString(input.RequestID) || input.Expected[modelRootID] != 3 { t.Fatal("frontend fixture envelope is incompatible") } before := modelTestDocument() before.Versions[modelRootID] = 3 after, touched, err := applyModelOperations(before, input.Operations) if err != nil { t.Fatal(err) } if !touched[modelRootID] || after.Versions[modelRootID] != 4 || after.Elements["a"]["data"].(map[string]any)["title"] != "renamed" || after.Elements["a"]["data"].(map[string]any)["text"] != "preserved" { t.Fatal("frontend fixture did not apply exactly its intended field change") } }