diff --git a/internal/platform/canvas_model_integration_test.go b/internal/platform/canvas_model_integration_test.go index c376f8f..469b3cc 100644 --- a/internal/platform/canvas_model_integration_test.go +++ b/internal/platform/canvas_model_integration_test.go @@ -236,4 +236,25 @@ func TestCanonicalModelMigrationAndIncrementalLifecycle(t *testing.T) { if writer.Code != 409 { t.Fatal("expected-account barrier was ignored") } + // A new partition's entry, owning canvas and versions publish atomically. + newCanvas, newFrame := modelTestID(5000), modelTestID(5001) + frame := modelTestNode(newFrame, root) + frame["componentId"] = "frame" + canvas := modelCanvas{ID: newCanvas, Parent: &modelParent{CanvasID: root, EntryElementID: newFrame}} + edit(1010, map[string]int64{root: 8, newCanvas: 0}, modelChange{Kind: "canvas", ID: newCanvas, Before: modelJSON(nil), After: modelJSON(canvas)}, modelChange{Kind: "element", ID: newFrame, Before: modelJSON(nil), After: modelJSON(frame)}) + model = read() + if model.Versions[newCanvas] != 1 || model.Canvases[newCanvas].Parent.EntryElementID != newFrame { + t.Fatal("partition creation was not atomic") + } + edit(1011, map[string]int64{root: 9, newCanvas: 1}, modelChange{Kind: "canvas", ID: newCanvas, Before: modelJSON(canvas), After: modelJSON(nil)}, modelChange{Kind: "element", ID: newFrame, Before: modelJSON(frame), After: modelJSON(nil)}) + model = read() + if _, exists := model.Canvases[newCanvas]; exists || model.Versions[newCanvas] != 2 { + t.Fatal("partition archive lost its version fence") + } + frame["incarnation"] = float64(2) + edit(1012, map[string]int64{root: 10, newCanvas: 2}, modelChange{Kind: "element", ID: newFrame, Before: modelJSON(nil), After: modelJSON(frame)}, modelChange{Kind: "canvas", ID: newCanvas, Before: modelJSON(nil), After: modelJSON(canvas)}) + model = read() + if model.Versions[newCanvas] != 3 || modelInt(model.Elements[newFrame]["incarnation"]) != 2 { + t.Fatal("partition restore reused a stale lifecycle or version") + } } diff --git a/internal/platform/canvas_model_test.go b/internal/platform/canvas_model_test.go index aeedd12..4c905be 100644 --- a/internal/platform/canvas_model_test.go +++ b/internal/platform/canvas_model_test.go @@ -1,8 +1,10 @@ package platform import ( + "bytes" "encoding/json" "fmt" + "os" "testing" ) @@ -149,3 +151,28 @@ func TestCanonicalValidationRejectsPartialChangesAndWrongEdgeOwner(t *testing.T) 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") + } +} diff --git a/internal/platform/testdata/foundry-model-operation-v2.json b/internal/platform/testdata/foundry-model-operation-v2.json new file mode 100644 index 0000000..165b8ea --- /dev/null +++ b/internal/platform/testdata/foundry-model-operation-v2.json @@ -0,0 +1,26 @@ +{ + "protocol_version": 2, + "request_id": "90000000-0000-4000-8000-000000000002", + "expected_versions": { + "10000000-0000-4000-8000-000000000001": 3 + }, + "operations": [ + { + "id": "90000000-0000-4000-8000-000000000001", + "changes": [ + { + "kind": "field", + "target": { + "id": "a", + "incarnation": 1 + }, + "field": "data.title", + "before": "a", + "after": "renamed", + "beforePresent": true, + "afterPresent": true + } + ] + } + ] +}