From d00388504cda5ce267b61cc5deb919c71a9d5e25 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Wed, 11 Nov 2026 16:35:31 +0800 Subject: [PATCH] test(canvas): cover migration admission guards with valid legacy edges --- .../canvas_model_atomic_integration_test.go | 93 ++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/internal/platform/canvas_model_atomic_integration_test.go b/internal/platform/canvas_model_atomic_integration_test.go index be5aabb..4b42fd6 100644 --- a/internal/platform/canvas_model_atomic_integration_test.go +++ b/internal/platform/canvas_model_atomic_integration_test.go @@ -39,7 +39,7 @@ func newCanonicalAtomicFixture(t *testing.T) *canonicalAtomicFixture { f := &canonicalAtomicFixture{db: db, project: "canonical-atomic-" + hex.EncodeToString(entropy[:])} ctx := context.Background() t.Cleanup(func() { - for _, table := range []string{"canvas_model_operations", "canvas_model_identity_map", "canvas_model_requests", "canvas_model_projects", "canvas_asset_bindings", "canvas_subgraphs", "workspaces", "project_accounts"} { + for _, table := range []string{"canvas_ai_runs", "canvas_model_operations", "canvas_model_identity_map", "canvas_model_requests", "canvas_model_projects", "canvas_asset_bindings", "canvas_subgraphs", "workspaces", "project_accounts"} { _, _ = db.ExecContext(ctx, "DELETE FROM "+table+" WHERE project=$1", f.project) } }) @@ -59,7 +59,7 @@ func newCanonicalAtomicFixture(t *testing.T) *canonicalAtomicFixture { if _, err = db.ExecContext(ctx, `INSERT INTO workspace_members(project,workspace_id,account_id,role) VALUES($1,$2,$3,'admin')`, f.project, f.workspace, f.account); err != nil { t.Fatal(err) } - f.legacy = json.RawMessage(`{"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":"preserved","frameId":"f"}},{"id":"b","type":"plainText","position":{"x":180,"y":300},"width":240,"height":72,"data":{"kind":"plain-text","title":"b","text":"preserved","frameId":"f"}},{"id":"c","type":"plainText","position":{"x":0,"y":0},"width":240,"height":72,"data":{"kind":"plain-text","title":"c","text":"unrelated"}}],"edges":[{"id":"e","source":"a","target":"b"}],"viewport":{"x":0,"y":0,"zoom":1}}`) + f.legacy = json.RawMessage(`{"schema_version":1,"nodes":[` + modelLegacyFrame + `,{"id":"a","type":"material","position":{"x":150,"y":260},"width":240,"height":72,"data":{"kind":"text","title":"a","text":"preserved","frameId":"f"}},{"id":"b","type":"material","position":{"x":180,"y":300},"width":240,"height":72,"data":{"kind":"text","title":"b","text":"preserved","frameId":"f"}},{"id":"c","type":"material","position":{"x":0,"y":0},"width":240,"height":72,"data":{"kind":"text","title":"c","text":"unrelated"}}],"edges":[{"id":"e","source":"a","target":"b"}],"viewport":{"x":0,"y":0,"zoom":1}}`) if err = db.QueryRowContext(ctx, `INSERT INTO materials(project,workspace_id,kind,title,created_by,canvas_document) VALUES($1,$2,'board','Root',$3,$4) RETURNING id::text`, f.project, f.workspace, f.account, f.legacy).Scan(&f.root); err != nil { t.Fatal(err) } @@ -298,3 +298,92 @@ func TestCanonicalMigrationLateCancellationRollsBack(t *testing.T) { t.Fatal("retry after rollback duplicated identities or the implicit partition") } } + +func TestCanonicalMigrationPermissionVersionAndRunGuards(t *testing.T) { + f := newCanonicalAtomicFixture(t) + ctx := context.Background() + input := func(key string, version int64) response { + return response{"protocol_version": 2, "request_id": key, "expected_versions": map[string]int64{f.root: version}} + } + call := func(key string, version int64, status int) response { + t.Helper() + return canonicalAtomicResponse(t, f.request(ctx, "POST", f.path()+"/migrate", key, modelJSON(input(key, version))), status) + } + assertLegacy := func(version int64) { + t.Helper() + for _, table := range []string{"canvas_model_projects", "canvas_model_canvases", "canvas_model_elements", "canvas_model_identity_map", "canvas_subgraphs"} { + var count int + if err := f.db.QueryRowContext(ctx, "SELECT count(*) FROM "+table+" WHERE project=$1", f.project).Scan(&count); err != nil || count != 0 { + t.Fatalf("rejected migration left %s rows: %d %v", table, count, err) + } + } + var count int + var current int64 + var source json.RawMessage + if err := f.db.QueryRowContext(ctx, `SELECT count(*) FROM materials WHERE project=$1`, f.project).Scan(&count); err != nil || count != 1 { + t.Fatalf("rejected migration created a material: %d %v", count, err) + } + if err := f.db.QueryRowContext(ctx, `SELECT canvas_version,canvas_document FROM materials WHERE project=$1 AND id=$2`, f.project, f.root).Scan(¤t, &source); err != nil || current != version || !modelEqual(f.legacy, source) { + t.Fatalf("rejected migration changed legacy source/version: %d %v", current, err) + } + } + if _, err := f.db.ExecContext(ctx, `UPDATE workspace_members SET role='viewer' WHERE project=$1 AND workspace_id=$2 AND account_id=$3`, f.project, f.workspace, f.account); err != nil { + t.Fatal(err) + } + if call(modelTestID(8500), 1, 403)["error"] != "forbidden" { + t.Fatal("viewer crossed the identity migration barrier") + } + assertLegacy(1) + if _, err := f.db.ExecContext(ctx, `UPDATE workspace_members SET role='admin' WHERE project=$1 AND workspace_id=$2 AND account_id=$3`, f.project, f.workspace, f.account); err != nil { + t.Fatal(err) + } + staleKey := modelTestID(8501) + rejected := call(staleKey, 2, 412) + if rejected["status"] != "not-applied" || rejected["terminal"] != true || rejected["error"] != "migration_version_conflict" { + t.Fatal("stale source migration did not produce a zero-effect receipt") + } + assertLegacy(1) + // Advance the old writer to the formerly rejected expected version. That + // cannot turn the same frozen negative request into a migration later. + if _, err := f.db.ExecContext(ctx, `UPDATE materials SET canvas_version=2 WHERE project=$1 AND id=$2`, f.project, f.root); err != nil { + t.Fatal(err) + } + if !modelEqual(rejected, call(staleKey, 2, 412)) { + t.Fatal("a source version change reevaluated a rejected migration key") + } + assertLegacy(2) + run := modelTestID(8400) + if _, err := f.db.ExecContext(ctx, `INSERT INTO canvas_ai_runs(project,workspace_id,board_id,id,account_id,idempotency_key,payload_hash,status,plan,create_response) VALUES($1,$2,$3,$4,$5,$6,'fixture','queued','{}','{}')`, f.project, f.workspace, f.root, run, f.account, modelTestID(8401)); err != nil { + t.Fatal(err) + } + if _, err := f.db.ExecContext(ctx, `INSERT INTO canvas_ai_steps(project,workspace_id,run_id,id,board_id,node_id,ordinal,status,model_id,frozen_input,target_fingerprint) VALUES($1,$2,$3,'fixture',$4,'a',0,'queued','fixture','{}','fixture')`, f.project, f.workspace, run, f.root); err != nil { + t.Fatal(err) + } + for index, status := range []string{"queued", "running"} { + if _, err := f.db.ExecContext(ctx, `UPDATE canvas_ai_runs SET status=$4 WHERE project=$1 AND workspace_id=$2 AND id=$3`, f.project, f.workspace, run, status); err != nil { + t.Fatal(err) + } + if _, err := f.db.ExecContext(ctx, `UPDATE canvas_ai_steps SET status=$4 WHERE project=$1 AND workspace_id=$2 AND run_id=$3`, f.project, f.workspace, run, status); err != nil { + t.Fatal(err) + } + body := call(modelTestID(8600+index), 2, 409) + if body["status"] != "not-applied" || body["terminal"] != true || body["error"] != "migration_waiting_for_runs" { + t.Fatal("identity migration crossed queued or outcome-unknown execution") + } + assertLegacy(2) + } + if _, err := f.db.ExecContext(ctx, `UPDATE canvas_ai_steps SET status='cancelled' WHERE project=$1 AND workspace_id=$2 AND run_id=$3`, f.project, f.workspace, run); err != nil { + t.Fatal(err) + } + if _, err := f.db.ExecContext(ctx, `UPDATE canvas_ai_runs SET status='cancelled' WHERE project=$1 AND workspace_id=$2 AND id=$3`, f.project, f.workspace, run); err != nil { + t.Fatal(err) + } + if call(modelTestID(8601), 2, 409)["error"] != "migration_waiting_for_runs" { + t.Fatal("settling paid work changed the frozen rejection") + } + call(modelTestID(8602), 2, 200) + model := f.read(t) + if model.Versions[f.root] != 3 || len(model.Canvases) != 2 { + t.Fatal("explicit migration after execution settled lost its source version barrier") + } +}