Files
foundry-core/internal/platform/canvas_model_test.go
T
codex-bot b7499e3815
Foundry Go checks / check (push) Failing after 3m6s
Foundry Go checks / check (pull_request) Failing after 2m36s
Verify canonical partition restoration and frontend protocol fixture
2026-11-07 22:40:39 +08:00

179 lines
9.8 KiB
Go

package platform
import (
"bytes"
"encoding/json"
"fmt"
"os"
"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")
}
}
func TestCanonicalFrontendProtocolFixture(t *testing.T) {
raw, err := os.ReadFile("testdata/foundry-model-operation-v2.json")
if err != nil {
t.Fatal(err)
}
var input modelBatch
decoder := json.NewDecoder(bytes.NewReader(raw))
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")
}
}