test(canvas): verify ordered batch receipts and rollback
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user