feat(control-plane): idempotent deletes + command-level wiring tests · Entire
feat(control-plane): idempotent deletes + command-level wiring tests
ed45929·
toothbrush·3w ago·2 files·+105 added/-0 removed
- org/project/repo delete now treat a 404 from the delete call as the
desired end state (already gone), printing " - Add command-level tests driving each delete command through cobra
against a fake control plane: --force issues DELETE on the right path,
an already-gone resource is idempotent, and a non-interactive run
without --force refuses rather than deleting unprompted.
Addresses trail #642 findings.
Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Sessions
416181183067View transcript
Changes
2
cmd/entire/cli
Mcorecmd.go+7
Acorecmd_delete_test.go+98
89 unmodified lines
return nil
}
if err := del(ctx, c, id); err != nil {
// Idempotent delete: a resource that's already gone (a 404 from the
// delete call — e.g. a ULID passed straight through, or a concurrent
// delete) is the desired end state, not an error.
if isNotFound(err) {
cmd.Printf("%s not found; nothing to delete\n", label)
return nil
}
return err
}
cmd.Printf("Deleted %s\n", label)
Mcmd/entire/cli/corecmd.go+7
package cli
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
"github.com/entireio/cli/internal/coreapi"
)
// testDeleteULID is a syntactically valid ULID (26 Crockford base32 chars, no
// I/L/O/U) so it passes looksLikeULID and the delete commands skip the name
// lookup, addressing the resource by id directly.
const testDeleteULID = "01HZX7QABCDEFGHJKMNPQRSTVW"
// runDeleteCmd points the active-context client at srv via the activeCoreClient
// seam, runs newCmd() with args, and returns its stdout and error. The caller
// must not be parallel: the seam is package-global.
func runDeleteCmd(t *testing.T, newCmd func() *cobra.Command, srvURL string, args ...string) (string, error) {
t.Helper()
prev := activeCoreClient
activeCoreClient = func(context.Context) (*coreapi.Client, error) {
return coreapi.NewWithBearer(srvURL, "tok")
}
t.Cleanup(func() { activeCoreClient = prev })
cmd := newCmd()
var out bytes.Buffer
cmd.SetOut(&out)
cmd.SetErr(&bytes.Buffer{})
cmd.SetArgs(args)
err := cmd.ExecuteContext(t.Context())
return out.String(), err
}
// TestControlPlaneDelete_Wiring exercises the org/project/repo delete commands
// end-to-end through cobra: that --force bypasses the prompt and issues DELETE
// against the right path, that an already-gone resource (404) is idempotent,
// and that a non-interactive run without --force refuses rather than deleting
// unprompted.
//
// Not parallel: swaps the package-level activeCoreClient seam.
func TestControlPlaneDelete_Wiring(t *testing.T) {
cases := []struct {
noun string
newCmd func() *cobra.Command
wantPath string
}{
{"org", newOrgDeleteCmd, "/api/v1/orgs/" + testDeleteULID},
{"project", newProjectDeleteCmd, "/api/v1/projects/" + testDeleteULID},
{"repo", newRepoDeleteCmd, "/api/v1/repos/" + testDeleteULID},
}
for _, tc := range cases {
t.Run(tc.noun+"/force deletes via the right path", func(t *testing.T) {
var gotMethod, gotPath string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotMethod, gotPath = r.Method, r.URL.Path
w.WriteHeader(http.StatusNoContent)
}))
t.Cleanup(srv.Close)
out, err := runDeleteCmd(t, tc.newCmd, srv.URL, testDeleteULID, "--force")
require.NoError(t, err)
require.Equal(t, http.MethodDelete, gotMethod)
require.Equal(t, tc.wantPath, gotPath)
require.Contains(t, out, "Deleted "+tc.noun+" "+testDeleteULID)
})
t.Run(tc.noun+"/already-gone is idempotent", func(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
writeNotFoundProblem(t, w)
}))
t.Cleanup(srv.Close)
out, err := runDeleteCmd(t, tc.newCmd, srv.URL, testDeleteULID, "--force")
require.NoError(t, err)
require.Contains(t, out, "not found; nothing to delete")
})
t.Run(tc.noun+"/refuses without --force when non-interactive", func(t *testing.T) {
// A ULID needs no resolve, so the refusal lands before any request.
srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
}))
t.Cleanup(srv.Close)
_, err := runDeleteCmd(t, tc.newCmd, srv.URL, testDeleteULID)
require.Error(t, err)
require.Contains(t, err.Error(), "--force")
})
}
}