test(canvas): verify ordered batch receipts and rollback
Foundry Go checks / check (push) Successful in 6m1s
Foundry Go checks / check (pull_request) Successful in 5m52s

This commit is contained in:
2026-11-08 04:28:15 +08:00
parent 31258b995f
commit ca90b3fea3
@@ -301,4 +301,47 @@ func TestCanonicalModelMigrationAndIncrementalLifecycle(t *testing.T) {
if _, exists := model.Elements[innerFrame]["data"].(map[string]any)["presentation"].(map[string]any)["unitScale"]; exists || !modelEqual(model.Elements[memberID], member) || model.Canvases[innerCanvas].Parent.CanvasID != root {
t.Fatal("scaled move inverse did not restore absent unit metadata and the original child binding")
}
// Several logical edits share one atomic request while retaining every deduplication identity.
batchKey, firstLogical, secondLogical := modelTestID(1016), modelTestID(22001), modelTestID(22002)
batchVersion := model.Versions[innerCanvas]
batch := modelBatch{ProtocolVersion: 2, RequestID: batchKey, Expected: map[string]int64{innerCanvas: batchVersion}, Operations: []modelOperation{
{ID: firstLogical, Changes: []modelChange{{Kind: "field", Target: modelTarget{ID: memberID, Incarnation: 1}, Field: "data.title", Before: modelJSON(memberID), After: modelJSON("intermediate")}}},
{ID: secondLogical, Changes: []modelChange{{Kind: "field", Target: modelTarget{ID: memberID, Incarnation: 1}, Field: "data.title", Before: modelJSON("intermediate"), After: modelJSON("batched")}}},
}}
batched := call("POST", base+"/model/operations", batch, batchKey, 200)
if !modelEqual(batched["operation_ids"], []string{firstLogical, secondLogical}) {
t.Fatal("batch receipt did not cover every original logical ID")
}
model = read()
if model.Versions[innerCanvas] != batchVersion+1 || modelString(model.Elements[memberID]["data"].(map[string]any)["title"]) != "batched" {
t.Fatal("batch lost ordered conditions or advanced the canvas version more than once")
}
if !modelEqual(call("GET", base+"/model/operations/"+batchKey, nil, "", 200), batched) || !modelEqual(call("POST", base+"/model/operations", batch, batchKey, 200), batched) {
t.Fatal("batch query/retry did not return its original receipt")
}
batch.RequestID = modelTestID(1017)
batch.Expected[innerCanvas] = model.Versions[innerCanvas]
batch.Operations = batch.Operations[:1]
if call("POST", base+"/model/operations", batch, batch.RequestID, 409)["error"] != "operation_already_applied" {
t.Fatal("a covered logical ID was executable under a new request key")
}
// A later failed condition cannot publish the earlier edit or consume its logical identity.
failedKey := modelTestID(1018)
batch.RequestID = failedKey
batch.Operations = []modelOperation{
{ID: modelTestID(22003), Changes: []modelChange{{Kind: "field", Target: modelTarget{ID: memberID, Incarnation: 1}, Field: "data.title", Before: modelJSON("batched"), After: modelJSON("must roll back")}}},
{ID: modelTestID(22004), Changes: []modelChange{{Kind: "field", Target: modelTarget{ID: memberID, Incarnation: 1}, Field: "data.title", Before: modelJSON("wrong before"), After: modelJSON("invalid")}}},
}
if call("POST", base+"/model/operations", batch, failedKey, 409)["status"] != "not-applied" {
t.Fatal("failed batch did not produce a zero-write terminal receipt")
}
model = read()
if model.Versions[innerCanvas] != batchVersion+1 || modelString(model.Elements[memberID]["data"].(map[string]any)["title"]) != "batched" {
t.Fatal("a failed batch leaked its successful prefix")
}
var consumed int
if err = db.QueryRowContext(ctx, `SELECT count(*) FROM canvas_model_operations WHERE project=$1 AND operation_id=$2`, project, modelTestID(22003)).Scan(&consumed); err != nil || consumed != 0 {
t.Fatal("a rejected atomic prefix consumed a logical operation ID")
}
}