diff --git a/docs/superpowers/plans/2026-08-12-instance-rename.md b/docs/superpowers/plans/2026-08-12-instance-rename.md index 981a4f6..37f6363 100644 --- a/docs/superpowers/plans/2026-08-12-instance-rename.md +++ b/docs/superpowers/plans/2026-08-12-instance-rename.md @@ -6,6 +6,8 @@ **Architecture:** Slug derivation stays in `shared/provision`, beside the create path that already owns it. Admin reaches the control plane only through `cloudprov`, writing `instances` — a collection it already writes. Admin's own row (`admin_instances`) is updated second and carries the 24h cooldown timestamp, because the cooldown is admin's policy and the control plane has no opinion about it. The portal shows the new host and asks the customer to click through; it does not redirect. +**Note:** This repo has no automated test suite and the user has ruled out adding test files. Every task verifies by build, vet and (Task 8) manual exercise. + **Tech Stack:** Go 1.x (gin, mongo-driver v2), Next.js 16 App Router + TanStack Query + Tailwind 3 (`adminsite`). **Spec:** `docs/superpowers/specs/2026-08-12-instance-rename-design.md` @@ -27,8 +29,6 @@ **Files:** - Modify: `shared/provision/instance.go` -- Create: `shared/provision/slug_test.go` - **Interfaces:** - Consumes: `BaseSlug(name string) (string, error)`, `ErrNameRejected` — both already in `shared/provision`. - Produces: @@ -37,75 +37,20 @@ - `provision.RenameInstance(ctx context.Context, db *mongo.Database, instanceID, name string) (*models.Instance, error)` - `provision.RestoreInstanceIdentity(ctx context.Context, db *mongo.Database, instanceID, name, slug string) error` -- [ ] **Step 1: Write the failing test** +Behaviour `RenameSlug` must have, verified by reading rather than by test (this +repo has no Go test suite and the user has ruled out adding one): -Create `shared/provision/slug_test.go`: +| Input name | Current slug | Result | +|---|---|---| +| `Acme Ltd` | `acme` | `acme-ltd` | +| `ACME!` | `acme` | `acme` — still derives to the current slug, so not a move | +| `Acme` | `acme-2` | `acme` — a creation-time collision suffix derives from no name, so moving off it is a real move | +| `ab` | any | `ErrNameRejected` | +| `Admin` | any | `ErrNameRejected` (reserved) | +| `!!!` | any | `ErrNameRejected` | +| 50 `a`s | any | truncated to `MaxSlugLength`, exactly as `BaseSlug` truncates on create | -```go -package provision - -import ( - "errors" - "testing" -) - -func TestRenameSlug(t *testing.T) { - cases := []struct { - name string - newName string - currentSlug string - want string - wantErr error - }{ - {"derives a new slug", "Acme Ltd", "acme", "acme-ltd", nil}, - {"unchanged when the name still derives to the current slug", "ACME!", "acme", "acme", nil}, - // A creation-time collision suffix does not derive from any name, so a - // rename off it is a real move even when the name is untouched. - {"moves off a collision suffix", "Acme", "acme-2", "acme", nil}, - {"too short is rejected", "ab", "acme", "", ErrNameRejected}, - {"reserved is rejected", "Admin", "acme", "", ErrNameRejected}, - {"punctuation only is rejected", "!!!", "acme", "", ErrNameRejected}, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - got, err := RenameSlug(tc.newName, tc.currentSlug) - if tc.wantErr != nil { - if !errors.Is(err, tc.wantErr) { - t.Fatalf("RenameSlug(%q, %q) error = %v, want %v", tc.newName, tc.currentSlug, err, tc.wantErr) - } - return - } - if err != nil { - t.Fatalf("RenameSlug(%q, %q) unexpected error: %v", tc.newName, tc.currentSlug, err) - } - if got != tc.want { - t.Fatalf("RenameSlug(%q, %q) = %q, want %q", tc.newName, tc.currentSlug, got, tc.want) - } - }) - } -} - -// A name longer than MaxSlugLength is truncated rather than refused, exactly as -// the create path truncates it — the two must not disagree about what is legal. -func TestRenameSlugTruncatesLongNames(t *testing.T) { - long := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" // 50 chars - got, err := RenameSlug(long, "old") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if len(got) != MaxSlugLength { - t.Fatalf("slug length = %d, want %d", len(got), MaxSlugLength) - } -} -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `cd /go-projects/vantage && go test ./shared/provision/ -run TestRenameSlug -v` -Expected: FAIL — `undefined: RenameSlug` - -- [ ] **Step 3: Write the implementation** +- [ ] **Step 1: Write the implementation** Append to `shared/provision/instance.go`: @@ -197,15 +142,15 @@ func RestoreInstanceIdentity(ctx context.Context, db *mongo.Database, instanceID } ``` -- [ ] **Step 4: Run the tests** +- [ ] **Step 2: Build and vet** -Run: `cd /go-projects/vantage && go test ./shared/provision/ -v && go build ./shared/...` -Expected: PASS on every subtest; build clean. +Run: `cd /go-projects/vantage && go build ./shared/... && go vet ./shared/provision/` +Expected: clean. -- [ ] **Step 5: Commit** +- [ ] **Step 3: Commit** ```bash -git add shared/provision/instance.go shared/provision/slug_test.go +git add shared/provision/instance.go git commit -m "feat: Add instance rename to shared provisioning" ``` @@ -958,11 +903,11 @@ screen with no explanation. The 24h cooldown lives on `admin_instances.renamed_a because it is admin's policy; staff bypass it and must not write the field. ``` -- [ ] **Step 3: Full build and test** +- [ ] **Step 3: Full build** Run: ```bash -cd /go-projects/vantage && go build ./... && go test ./shared/... && (cd adminsite && npm run build) +cd /go-projects/vantage && go build ./... && go vet ./admin/... ./shared/... && (cd adminsite && npm run build) ``` Expected: all clean.