From ffeca157f454f1846103ab09e7752e62dc65b1aa Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Fri, 11 Sep 2026 00:14:29 +0500 Subject: [PATCH] fix(plan): admit the preserved identities the delete plan now carries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit path could rebuild the retirement evidence the approver signed. It did not teach the plan schema about them, and `repositoryProviderParameters` sets `additionalProperties: false`, so the first real `gds repository delete --plan` with `--preserve` failed as GDS_PLAN_INVALID. My own change, incomplete. Nothing caught it: the Go round-trip test asserted the field survives `Parameters`/`StepTransition`, and no test validated those parameters against the schema that governs them. This adds the test that does — it builds the actual delete plan and runs `plan.Validate`, and it fails without the schema change. The item pattern is the exact retirement vocabulary `core/repository/ retirement.go` emits: `commits:unpushed`, `review-threads`, and the `worktree:`, `ref:`, `branch:`, `pull-request:` and `issue:` prefixes. An unrecognized value was already harmless — it preserves nothing, so the plan fails closed as work-remaining — but a typo should be refused where it is written, not three steps later. Claude-Session: https://claude.ai/code/session_01CKXKXND4zAgWisTtatyTHX --- core/repository/lifecycle_test.go | 57 +++++++++++++++++++++++++++++++ schemas/v1/plan.schema.json | 13 ++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/core/repository/lifecycle_test.go b/core/repository/lifecycle_test.go index 2a97bbe..f24e374 100644 --- a/core/repository/lifecycle_test.go +++ b/core/repository/lifecycle_test.go @@ -2,10 +2,13 @@ package repository import ( "reflect" + "strings" "testing" + "time" "github.com/NDDev-OpenNetwork/github-device-sync/core/domain" "github.com/NDDev-OpenNetwork/github-device-sync/core/operations" + "github.com/NDDev-OpenNetwork/github-device-sync/core/validation" ) func TestValidateRenamePreservesStableIdentityAndAliasHistory(t *testing.T) { @@ -158,3 +161,57 @@ func TestDeleteParametersRejectANonStringAcceptedLoss(t *testing.T) { t.Fatal("a non-string accepted loss must not decode into a preservation declaration") } } + +// The plan schema refuses unknown keys under `repository_provider`, so a new +// transition field is only real once the schema admits it. A delete plan +// carrying accepted losses is exactly the shape that shipped broken: the Go +// round trip passed while `gds repository delete --plan` failed with +// GDS_PLAN_INVALID, because nothing validated the parameters against the +// schema that governs them. +func TestDeleteParametersValidateAgainstThePlanSchema(t *testing.T) { + current := lifecycleAnchor() + current.Repository.Lifecycle = "archived" + transition, findings := ValidateDelete(current) + if len(findings) != 0 { + t.Fatalf("findings=%#v", findings) + } + transition.MutationCapabilityID = "mutation:github-personal" + transition.ExpectedProviderDigest = "sha256:" + strings.Repeat("a", 64) + transition.AnalysisRoot = "/verified-estate" + transition.PreservedIdentities = []string{ + "commits:unpushed", "ref:refs/tags/v0.6.10", "review-threads", + "worktree:/verified-estate/example", "branch:task/one", + "pull-request:7", "issue:11", + } + plan, err := operations.NewPlan( + "plan_01ABCDEFGHJKMNPQRSTVWXYZ01", time.Unix(1_800_000_000, 0).UTC(), + time.Unix(1_800_000_900, 0).UTC(), + operations.PlanInput{ + Operation: "delete-repository", + Actor: operations.Actor{Type: "agent-session", SessionID: "schema-test-session"}, + Preconditions: []operations.Precondition{{ + RepositoryID: transition.RepositoryID, + HeadOID: strings.Repeat("b", 40), + ManifestDigest: "sha256:" + strings.Repeat("c", 64), + PolicyDigest: "sha256:" + strings.Repeat("d", 64), + }}, + Steps: []operations.Step{{ + StepID: "delete-provider-repository", RepositoryID: transition.RepositoryID, + Action: ProviderLifecycleAction, RequiresApproval: true, + Compensation: operations.Compensation{Mode: "manual"}, + Parameters: Parameters(transition), + }}, + ApprovalClass: "delete-github-repository", + }, + ) + if err != nil { + t.Fatalf("NewPlan: %v", err) + } + schemas, err := validation.NewSchemaSet() + if err != nil { + t.Fatalf("NewSchemaSet: %v", err) + } + if planFindings := plan.Validate(schemas); len(planFindings) != 0 { + t.Fatalf("plan findings = %#v", planFindings) + } +} diff --git a/schemas/v1/plan.schema.json b/schemas/v1/plan.schema.json index 798a598..e244cd3 100644 --- a/schemas/v1/plan.schema.json +++ b/schemas/v1/plan.schema.json @@ -730,7 +730,18 @@ "target_owner": {"$ref": "common.schema.json#/$defs/githubOwner"}, "target_name": {"$ref": "common.schema.json#/$defs/repositoryName"}, "target_lifecycle": {"$ref": "common.schema.json#/$defs/lifecycle"}, - "analysis_root": {"type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^/"} + "analysis_root": {"type": "string", "minLength": 1, "maxLength": 4096, "pattern": "^/"}, + "preserved_identities": { + "type": "array", + "uniqueItems": true, + "maxItems": 1000, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 512, + "pattern": "^(commits:unpushed|review-threads|(worktree|ref|branch|pull-request|issue):.+)$" + } + } }, "allOf": [ {