From b9cf57f7b9dacdc4f7d53b8a040e7b1ac2e5fcbb Mon Sep 17 00:00:00 2001 From: Elaine Vegeris Date: Tue, 28 Apr 2026 11:32:43 -0400 Subject: [PATCH 1/4] rm experiment flag --- cmd/sandbox/create.go | 3 -- cmd/sandbox/create_test.go | 66 ------------------------------- cmd/sandbox/delete.go | 3 -- cmd/sandbox/delete_test.go | 29 -------------- cmd/sandbox/list.go | 3 -- cmd/sandbox/list_test.go | 34 +++------------- cmd/sandbox/sandbox.go | 16 +------- cmd/sandbox/sandbox_test.go | 4 -- internal/experiment/experiment.go | 4 -- 9 files changed, 6 insertions(+), 156 deletions(-) diff --git a/cmd/sandbox/create.go b/cmd/sandbox/create.go index 36dc458f..f53db94b 100644 --- a/cmd/sandbox/create.go +++ b/cmd/sandbox/create.go @@ -60,9 +60,6 @@ func NewCreateCommand(clients *shared.ClientFactory) *cobra.Command { {Command: "sandbox create --name test-box --password mypass --domain test-box --archive-date 2025-12-31", Meaning: "Create a sandbox that will be archived on a specific date"}, }), Args: cobra.NoArgs, - PreRunE: func(cmd *cobra.Command, args []string) error { - return requireSandboxExperiment(clients) - }, RunE: func(cmd *cobra.Command, args []string) error { return runCreateCommand(cmd, clients) }, diff --git a/cmd/sandbox/create_test.go b/cmd/sandbox/create_test.go index 36d0619a..b7ddbfbe 100644 --- a/cmd/sandbox/create_test.go +++ b/cmd/sandbox/create_test.go @@ -20,7 +20,6 @@ import ( "testing" "time" - "github.com/slackapi/slack-cli/internal/experiment" "github.com/slackapi/slack-cli/internal/shared" "github.com/slackapi/slack-cli/internal/shared/types" "github.com/slackapi/slack-cli/test/testutil" @@ -36,7 +35,6 @@ func TestCreateCommand(t *testing.T) { testutil.TableTestCommand(t, testutil.CommandTests{ "create success": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "test-box", "--domain", "test-box", @@ -52,8 +50,6 @@ func TestCreateCommand(t *testing.T) { cm.API.On("UsersInfo", mock.Anything, mock.Anything, mock.Anything).Return(&types.UserInfo{Profile: types.UserProfile{}}, nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedStdoutOutputs: []string{"T123", "https://test-box.slack.com", "Sandbox Created"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -63,7 +59,6 @@ func TestCreateCommand(t *testing.T) { }, "create with derived domain": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "My Test Box", "--domain", "my-test-box", @@ -78,8 +73,6 @@ func TestCreateCommand(t *testing.T) { Return("T789", "https://my-test-box.slack.com", nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { cm.API.AssertCalled(t, "CreateSandbox", mock.Anything, "xoxb-test-token", "My Test Box", "my-test-box", "pass", "", "", 0, "", int64(0), false) @@ -87,7 +80,6 @@ func TestCreateCommand(t *testing.T) { }, "create with a relative time-to-live value": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "tmp-box", "--domain", "tmp-box", @@ -103,8 +95,6 @@ func TestCreateCommand(t *testing.T) { Return("T111", "https://tmp-box.slack.com", nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { cm.API.AssertCalled(t, "CreateSandbox", mock.Anything, "xoxb-test-token", "tmp-box", "tmp-box", "pass", "", "", 0, "", mock.MatchedBy(func(v int64) bool { return v > 0 }), false) @@ -112,7 +102,6 @@ func TestCreateCommand(t *testing.T) { }, "create API error": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "err-box", "--domain", "err-box", @@ -127,14 +116,11 @@ func TestCreateCommand(t *testing.T) { Return("", "", errors.New("api_error")) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedErrorStrings: []string{"api_error"}, }, "create with 'default' template": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "tpl-box", "--domain", "tpl-box", @@ -150,8 +136,6 @@ func TestCreateCommand(t *testing.T) { Return("T333", "https://tpl-box.slack.com", nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedStdoutOutputs: []string{"T333", "https://tpl-box.slack.com", "Sandbox Created"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -160,7 +144,6 @@ func TestCreateCommand(t *testing.T) { }, "create with partner flag": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "partner-box", "--domain", "partner-box", @@ -176,8 +159,6 @@ func TestCreateCommand(t *testing.T) { Return("T555", "https://partner-box.slack.com", nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedStdoutOutputs: []string{"T555", "https://partner-box.slack.com", "Sandbox Created"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -186,7 +167,6 @@ func TestCreateCommand(t *testing.T) { }, "create with invalid template fails": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "tmpl-box", "--domain", "tmpl-box", @@ -200,8 +180,6 @@ func TestCreateCommand(t *testing.T) { cm.Auth.On("ResolveLogstashHost", mock.Anything, mock.Anything).Return("https://slackb.com/events/cli") cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedErrorStrings: []string{"Invalid template"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -210,7 +188,6 @@ func TestCreateCommand(t *testing.T) { }, "create with archive-date": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "date-box", "--domain", "date-box", @@ -226,8 +203,6 @@ func TestCreateCommand(t *testing.T) { Return("T222", "https://date-box.slack.com", nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { cm.API.AssertCalled(t, "CreateSandbox", mock.Anything, "xoxb-test-token", "date-box", "date-box", "pass", "", "", 0, "", archiveEpoch, false) @@ -235,7 +210,6 @@ func TestCreateCommand(t *testing.T) { }, "create with both archive and archive-date fails": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "tmp-box", "--domain", "tmp-box", @@ -250,8 +224,6 @@ func TestCreateCommand(t *testing.T) { cm.Auth.On("ResolveLogstashHost", mock.Anything, mock.Anything).Return("https://slackb.com/events/cli") cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedErrorStrings: []string{"Cannot use both --archive-ttl and --archive-date"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -260,7 +232,6 @@ func TestCreateCommand(t *testing.T) { }, "create with invalid archive-ttl value fails": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "tmp-box", "--domain", "tmp-box", @@ -274,28 +245,12 @@ func TestCreateCommand(t *testing.T) { cm.Auth.On("ResolveLogstashHost", mock.Anything, mock.Anything).Return("https://slackb.com/events/cli") cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedErrorStrings: []string{"Invalid TTL"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { cm.API.AssertNotCalled(t, "CreateSandbox", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) }, }, - "experiment required": { - CmdArgs: []string{ - "--name", "test-box", - "--domain", "test-box", - "--password", "pass", - }, - Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { - cm.AddDefaultMocks() - }, - ExpectedErrorStrings: []string{"sandbox"}, - ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - cm.API.AssertNotCalled(t, "CreateSandbox", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything) - }, - }, }, func(cf *shared.ClientFactory) *cobra.Command { return NewCreateCommand(cf) }) @@ -310,8 +265,6 @@ func setupCreateMocks(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cm.API.On("CreateSandbox", mock.Anything, testToken, name, domain, password, "", "", 0, "", archiveEpoch, partner). Return("T222", "https://"+domain+".slack.com", nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) } func setupCreateAuthOnly(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -321,15 +274,12 @@ func setupCreateAuthOnly(t *testing.T, ctx context.Context, cm *shared.ClientsMo cm.Auth.On("ResolveAPIHost", mock.Anything, mock.Anything, mock.Anything).Return("https://api.slack.com") cm.Auth.On("ResolveLogstashHost", mock.Anything, mock.Anything).Return("https://slackb.com/events/cli") cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) } func Test_getEpochFromTTL(t *testing.T) { testutil.TableTestCommand(t, testutil.CommandTests{ "1d": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "ttl-box", "--domain", "ttl-box", @@ -346,7 +296,6 @@ func Test_getEpochFromTTL(t *testing.T) { }, "1w": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "ttl-box", "--domain", "ttl-box", @@ -363,7 +312,6 @@ func Test_getEpochFromTTL(t *testing.T) { }, "6mo": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "ttl-box", "--domain", "ttl-box", @@ -380,7 +328,6 @@ func Test_getEpochFromTTL(t *testing.T) { }, "invalid": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "ttl-box", "--domain", "ttl-box", @@ -408,7 +355,6 @@ func Test_getEpochFromDate(t *testing.T) { testutil.TableTestCommand(t, testutil.CommandTests{ "valid": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "date-box", "--domain", "date-box", @@ -425,7 +371,6 @@ func Test_getEpochFromDate(t *testing.T) { }, "invalid format": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "date-box", "--domain", "date-box", @@ -442,7 +387,6 @@ func Test_getEpochFromDate(t *testing.T) { }, "invalid date": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "date-box", "--domain", "date-box", @@ -459,7 +403,6 @@ func Test_getEpochFromDate(t *testing.T) { }, "date in past": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "date-box", "--domain", "date-box", @@ -483,7 +426,6 @@ func Test_getTemplateID(t *testing.T) { testutil.TableTestCommand(t, testutil.CommandTests{ "valid template name": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "tpl-box", "--domain", "tpl-box", @@ -498,8 +440,6 @@ func Test_getTemplateID(t *testing.T) { cm.API.On("CreateSandbox", mock.Anything, testToken, "tpl-box", "tpl-box", "pass", "", "", 1, "", int64(0), false). Return("T333", "https://tpl-box.slack.com", nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedStdoutOutputs: []string{"Sandbox Created"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -508,7 +448,6 @@ func Test_getTemplateID(t *testing.T) { }, "integer value also accepted": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "tpl-box", "--domain", "tpl-box", @@ -523,8 +462,6 @@ func Test_getTemplateID(t *testing.T) { cm.API.On("CreateSandbox", mock.Anything, testToken, "tpl-box", "tpl-box", "pass", "", "", 1, "", int64(0), false). Return("T333", "https://tpl-box.slack.com", nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedStdoutOutputs: []string{"Sandbox Created"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -533,7 +470,6 @@ func Test_getTemplateID(t *testing.T) { }, "invalid template name fails": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "tpl-box", "--domain", "tpl-box", @@ -557,7 +493,6 @@ func Test_domainFromName(t *testing.T) { testutil.TableTestCommand(t, testutil.CommandTests{ "handles invalid URL characters": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "-Hello_World 123-", "--password", "pass", @@ -572,7 +507,6 @@ func Test_domainFromName(t *testing.T) { }, "empty": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--name", "", "--password", "pass", diff --git a/cmd/sandbox/delete.go b/cmd/sandbox/delete.go index 6f38f740..1c78bce1 100644 --- a/cmd/sandbox/delete.go +++ b/cmd/sandbox/delete.go @@ -40,9 +40,6 @@ func NewDeleteCommand(clients *shared.ClientFactory) *cobra.Command { {Command: "sandbox delete --sandbox-id E0123456", Meaning: "Delete a sandbox identified by its team ID"}, }), Args: cobra.NoArgs, - PreRunE: func(cmd *cobra.Command, args []string) error { - return requireSandboxExperiment(clients) - }, RunE: func(cmd *cobra.Command, args []string) error { return runDeleteCommand(cmd, clients) }, diff --git a/cmd/sandbox/delete_test.go b/cmd/sandbox/delete_test.go index 454ba87f..d12d43a3 100644 --- a/cmd/sandbox/delete_test.go +++ b/cmd/sandbox/delete_test.go @@ -19,7 +19,6 @@ import ( "errors" "testing" - "github.com/slackapi/slack-cli/internal/experiment" "github.com/slackapi/slack-cli/internal/shared" "github.com/slackapi/slack-cli/internal/shared/types" "github.com/slackapi/slack-cli/test/testutil" @@ -31,7 +30,6 @@ func TestDeleteCommand(t *testing.T) { testutil.TableTestCommand(t, testutil.CommandTests{ "delete success": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--sandbox-id", "T123", "--force", @@ -46,8 +44,6 @@ func TestDeleteCommand(t *testing.T) { cm.API.On("UsersInfo", mock.Anything, mock.Anything, mock.Anything).Return(&types.UserInfo{Profile: types.UserProfile{}}, nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedStdoutOutputs: []string{"Sandbox Deleted", "T123", "No sandboxes found"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -58,7 +54,6 @@ func TestDeleteCommand(t *testing.T) { }, "delete with remaining sandboxes": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--sandbox-id", "T123", "--force", @@ -83,8 +78,6 @@ func TestDeleteCommand(t *testing.T) { cm.API.On("UsersInfo", mock.Anything, mock.Anything, mock.Anything).Return(&types.UserInfo{Profile: types.UserProfile{}}, nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedStdoutOutputs: []string{"Sandbox Deleted", "T123", "other-sandbox", "T456"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -94,7 +87,6 @@ func TestDeleteCommand(t *testing.T) { }, "deletion cancelled": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--sandbox-id", "T123", }, @@ -106,8 +98,6 @@ func TestDeleteCommand(t *testing.T) { cm.IO.On("ConfirmPrompt", mock.Anything, "Are you sure you want to delete the sandbox?", false).Return(false, nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedStdoutOutputs: []string{"Deletion cancelled"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -117,7 +107,6 @@ func TestDeleteCommand(t *testing.T) { }, "delete confirmation proceeds": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--sandbox-id", "E0123456", }, @@ -132,8 +121,6 @@ func TestDeleteCommand(t *testing.T) { cm.API.On("UsersInfo", mock.Anything, mock.Anything, mock.Anything).Return(&types.UserInfo{Profile: types.UserProfile{}}, nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedStdoutOutputs: []string{"Sandbox Deleted", "E0123456"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -143,7 +130,6 @@ func TestDeleteCommand(t *testing.T) { }, "delete API error": { CmdArgs: []string{ - "--experiment=sandboxes", "--token", "xoxb-test-token", "--sandbox-id", "T123", "--force", @@ -156,27 +142,12 @@ func TestDeleteCommand(t *testing.T) { cm.API.On("DeleteSandbox", mock.Anything, testToken, "T123").Return(errors.New("api_error")) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedErrorStrings: []string{"api_error"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { cm.API.AssertCalled(t, "DeleteSandbox", mock.Anything, "xoxb-test-token", "T123") }, }, - "experiment required": { - CmdArgs: []string{ - "--sandbox-id", "T123", - "--force", - }, - Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { - cm.AddDefaultMocks() - }, - ExpectedErrorStrings: []string{"sandbox"}, - ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - cm.API.AssertNotCalled(t, "DeleteSandbox", mock.Anything, mock.Anything, mock.Anything) - }, - }, }, func(cf *shared.ClientFactory) *cobra.Command { return NewDeleteCommand(cf) }) diff --git a/cmd/sandbox/list.go b/cmd/sandbox/list.go index efeb10a6..5809c88e 100644 --- a/cmd/sandbox/list.go +++ b/cmd/sandbox/list.go @@ -45,9 +45,6 @@ func NewListCommand(clients *shared.ClientFactory) *cobra.Command { {Command: "sandbox list", Meaning: "List developer sandboxes"}, {Command: "sandbox list --status active", Meaning: "List active sandboxes only"}, }), - PreRunE: func(cmd *cobra.Command, args []string) error { - return requireSandboxExperiment(clients) - }, RunE: func(cmd *cobra.Command, args []string) error { return runListCommand(cmd, clients) }, diff --git a/cmd/sandbox/list_test.go b/cmd/sandbox/list_test.go index 81c9f603..f7e3386d 100644 --- a/cmd/sandbox/list_test.go +++ b/cmd/sandbox/list_test.go @@ -19,7 +19,6 @@ import ( "errors" "testing" - "github.com/slackapi/slack-cli/internal/experiment" "github.com/slackapi/slack-cli/internal/shared" "github.com/slackapi/slack-cli/internal/shared/types" "github.com/slackapi/slack-cli/test/testutil" @@ -31,7 +30,7 @@ import ( func TestListCommand(t *testing.T) { testutil.TableTestCommand(t, testutil.CommandTests{ "empty list": { - CmdArgs: []string{"--experiment=sandboxes", "--token", "xoxb-test-token"}, + CmdArgs: []string{"--token", "xoxb-test-token"}, Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { testToken := "xoxb-test-token" cm.Auth.On("AuthWithToken", mock.Anything, testToken).Return(types.SlackAuth{Token: testToken}, nil) @@ -41,8 +40,6 @@ func TestListCommand(t *testing.T) { cm.API.On("UsersInfo", mock.Anything, mock.Anything, mock.Anything).Return(&types.UserInfo{Profile: types.UserProfile{}}, nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedStdoutOutputs: []string{"No sandboxes found"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -51,7 +48,7 @@ func TestListCommand(t *testing.T) { }, }, "with active sandboxes": { - CmdArgs: []string{"--experiment=sandboxes", "--token", "xoxb-test-token"}, + CmdArgs: []string{"--token", "xoxb-test-token"}, Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { testToken := "xoxb-test-token" cm.Auth.On("AuthWithToken", mock.Anything, testToken).Return(types.SlackAuth{Token: testToken}, nil) @@ -71,8 +68,6 @@ func TestListCommand(t *testing.T) { cm.API.On("UsersInfo", mock.Anything, mock.Anything, mock.Anything).Return(&types.UserInfo{Profile: types.UserProfile{}}, nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedStdoutOutputs: []string{"my-sandbox", "T123", "https://my-sandbox.slack.com", "Status: ACTIVE"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -81,7 +76,7 @@ func TestListCommand(t *testing.T) { }, }, "with archived sandbox": { - CmdArgs: []string{"--experiment=sandboxes", "--token", "xoxb-test-token"}, + CmdArgs: []string{"--token", "xoxb-test-token"}, Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { testToken := "xoxb-test-token" cm.Auth.On("AuthWithToken", mock.Anything, testToken).Return(types.SlackAuth{Token: testToken}, nil) @@ -101,8 +96,6 @@ func TestListCommand(t *testing.T) { cm.API.On("UsersInfo", mock.Anything, mock.Anything, mock.Anything).Return(&types.UserInfo{Profile: types.UserProfile{}}, nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedStdoutOutputs: []string{"old-sandbox", "T456", "Status: ARCHIVED"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -137,8 +130,6 @@ func TestListCommand(t *testing.T) { cm.API.On("UsersInfo", mock.Anything, mock.Anything, mock.Anything).Return(&types.UserInfo{Profile: types.UserProfile{}}, nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedStdoutOutputs: []string{"regular-sandbox", "partner-sandbox", "Type: Partner"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { @@ -146,7 +137,7 @@ func TestListCommand(t *testing.T) { }, }, "with status": { - CmdArgs: []string{"--experiment=sandboxes", "--token", "xoxb-test-token", "--status", "active"}, + CmdArgs: []string{"--token", "xoxb-test-token", "--status", "active"}, Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { testToken := "xoxb-test-token" cm.Auth.On("AuthWithToken", mock.Anything, testToken).Return(types.SlackAuth{Token: testToken}, nil) @@ -156,15 +147,13 @@ func TestListCommand(t *testing.T) { cm.API.On("UsersInfo", mock.Anything, mock.Anything, mock.Anything).Return(&types.UserInfo{Profile: types.UserProfile{}}, nil) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { cm.API.AssertCalled(t, "ListSandboxes", mock.Anything, "xoxb-test-token", "active") }, }, "list error": { - CmdArgs: []string{"--experiment=sandboxes", "--token", "xoxb-test-token"}, + CmdArgs: []string{"--token", "xoxb-test-token"}, Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { testToken := "xoxb-test-token" cm.Auth.On("AuthWithToken", mock.Anything, testToken).Return(types.SlackAuth{Token: testToken}, nil) @@ -174,22 +163,9 @@ func TestListCommand(t *testing.T) { Return([]types.Sandbox(nil), errors.New("api_error")) cm.AddDefaultMocks() - cm.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - cm.Config.LoadExperiments(ctx, cm.IO.PrintDebug) }, ExpectedErrorStrings: []string{"api_error"}, }, - "experiment required": { - CmdArgs: []string{}, - Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { - cm.AddDefaultMocks() - // Do NOT enable sandboxes experiment - }, - ExpectedErrorStrings: []string{"sandbox"}, - ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - cm.API.AssertNotCalled(t, "ListSandboxes", mock.Anything, mock.Anything, mock.Anything) - }, - }, }, func(cf *shared.ClientFactory) *cobra.Command { return NewListCommand(cf) }) diff --git a/cmd/sandbox/sandbox.go b/cmd/sandbox/sandbox.go index 27ed0cfb..fd3d42e9 100644 --- a/cmd/sandbox/sandbox.go +++ b/cmd/sandbox/sandbox.go @@ -17,18 +17,16 @@ package sandbox import ( "context" - "github.com/slackapi/slack-cli/internal/experiment" "github.com/slackapi/slack-cli/internal/prompts" "github.com/slackapi/slack-cli/internal/shared" "github.com/slackapi/slack-cli/internal/shared/types" - "github.com/slackapi/slack-cli/internal/slackerror" "github.com/slackapi/slack-cli/internal/style" "github.com/spf13/cobra" ) func NewCommand(clients *shared.ClientFactory) *cobra.Command { cmd := &cobra.Command{ - Use: "sandbox [flags] --experiment=sandboxes", + Use: "sandbox [flags]", Short: "Manage developer sandboxes", Long: `Manage Slack developer sandboxes without leaving your terminal. Use the --team flag to select the authentication to use for these commands. @@ -40,9 +38,6 @@ New to the Developer Program? Sign up at {{LinkText "https://api.slack.com/developer-program/join"}}`, Example: style.ExampleCommandsf([]style.ExampleCommand{}), Aliases: []string{"sandboxes"}, - PreRunE: func(cmd *cobra.Command, args []string) error { - return requireSandboxExperiment(clients) - }, RunE: func(cmd *cobra.Command, args []string) error { return cmd.Help() }, @@ -55,15 +50,6 @@ New to the Developer Program? Sign up at return cmd } -func requireSandboxExperiment(clients *shared.ClientFactory) error { - if !clients.Config.WithExperimentOn(experiment.Sandboxes) { - return slackerror.New(slackerror.ErrMissingExperiment). - WithMessage("%sThe sandbox management commands are under construction", style.Emoji("construction")). - WithRemediation("To try them out, just add the --experiment=sandboxes flag to your command!") - } - return nil -} - // getSandboxAuth returns the auth to be used for sandbox management. // Uses the global --token or --team flag if present, otherwise prompts the user to select a team. func getSandboxAuth(ctx context.Context, clients *shared.ClientFactory) (*types.SlackAuth, error) { diff --git a/cmd/sandbox/sandbox_test.go b/cmd/sandbox/sandbox_test.go index cbbcf004..16015cd6 100644 --- a/cmd/sandbox/sandbox_test.go +++ b/cmd/sandbox/sandbox_test.go @@ -17,7 +17,6 @@ package sandbox import ( "testing" - "github.com/slackapi/slack-cli/internal/experiment" "github.com/slackapi/slack-cli/internal/shared" "github.com/slackapi/slack-cli/internal/slackcontext" "github.com/slackapi/slack-cli/test/testutil" @@ -29,9 +28,6 @@ func TestSandboxCommand(t *testing.T) { clientsMock := shared.NewClientsMock() clientsMock.AddDefaultMocks() - clientsMock.Config.ExperimentsFlag = []string{string(experiment.Sandboxes)} - clientsMock.Config.LoadExperiments(ctx, clientsMock.IO.PrintDebug) - clients := shared.NewClientFactory(clientsMock.MockClientFactory()) cmd := NewCommand(clients) testutil.MockCmdIO(clients.IO, cmd) diff --git a/internal/experiment/experiment.go b/internal/experiment/experiment.go index 80c661f9..3c029c5b 100644 --- a/internal/experiment/experiment.go +++ b/internal/experiment/experiment.go @@ -36,9 +36,6 @@ const ( // Placeholder experiment is a placeholder for testing and does nothing... or does it? Placeholder Experiment = "placeholder" - // Sandboxes experiment lets users who have joined the Slack Developer Program use the CLI to manage their sandboxes. - Sandboxes Experiment = "sandboxes" - // SetIcon experiment enables icon upload for non-hosted apps. SetIcon Experiment = "set-icon" ) @@ -48,7 +45,6 @@ const ( var AllExperiments = []Experiment{ Lipgloss, Placeholder, - Sandboxes, SetIcon, } From ce63d4909413b1d3fe467fcfe3755239260e026f Mon Sep 17 00:00:00 2001 From: Elaine Vegeris Date: Wed, 29 Apr 2026 10:20:14 -0400 Subject: [PATCH 2/4] incl partner template names, new err code msg --- cmd/sandbox/create.go | 10 +++++++++- internal/slackerror/errors.go | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/cmd/sandbox/create.go b/cmd/sandbox/create.go index f53db94b..b2d89a75 100644 --- a/cmd/sandbox/create.go +++ b/cmd/sandbox/create.go @@ -45,8 +45,16 @@ var createCmdFlags createFlags // templateNameToID maps user-friendly template names to integer IDs var templateNameToID = map[string]int{ - "default": 1, // The default template "empty": 0, // The sandbox will be empty if the template param is not set + "default": 1, // The default template + + // Additional partner sandbox template options + "finance": 2, + "hr": 3, + "it-incident-management": 4, + "customer-support": 5, + "sales": 6, + "marketing": 7, } func NewCreateCommand(clients *shared.ClientFactory) *cobra.Command { diff --git a/internal/slackerror/errors.go b/internal/slackerror/errors.go index 86de955a..b1ba024c 100644 --- a/internal/slackerror/errors.go +++ b/internal/slackerror/errors.go @@ -100,6 +100,7 @@ const ( ErrDotEnvFileRead = "dotenv_file_read_error" ErrDotEnvFileWrite = "dotenv_file_write_error" ErrDotEnvVarMarshal = "dotenv_var_marshal_error" + ErrDomainLong = "domain_long" ErrEnterpriseNotFound = "enterprise_not_found" ErrFailedAddingCollaborator = "failed_adding_collaborator" ErrFailedCreatingApp = "failed_creating_app" @@ -709,6 +710,11 @@ Otherwise start your app for local development with: %s`, Message: "Failed to marshal the .env variable", }, + ErrDomainLong: { + Code: ErrDomainLong, + Message: "Sandbox name or domain is too long", + }, + ErrEnterpriseNotFound: { Code: ErrEnterpriseNotFound, Message: "The `enterprise` was not found", From 9c99351c3e735d87451564359736575252d36289 Mon Sep 17 00:00:00 2001 From: Elaine Vegeris Date: Thu, 30 Apr 2026 09:25:16 -0400 Subject: [PATCH 3/4] alphabetize sandbox errors --- internal/slackerror/errors.go | 64 +++++++++++++++++------------------ 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/internal/slackerror/errors.go b/internal/slackerror/errors.go index b1ba024c..e612d79d 100644 --- a/internal/slackerror/errors.go +++ b/internal/slackerror/errors.go @@ -55,6 +55,7 @@ const ( ErrAppNotHosted = "app_not_hosted" ErrAppRemove = "app_remove_error" ErrAppRenameApp = "app_rename_app" + ErrAtActiveSandboxLimit = "at_active_sandbox_limit" ErrAuthProdTokenNotFound = "auth_prod_token_not_found" ErrAuthTimeout = "auth_timeout_error" ErrAuthToken = "auth_token_error" @@ -100,7 +101,6 @@ const ( ErrDotEnvFileRead = "dotenv_file_read_error" ErrDotEnvFileWrite = "dotenv_file_write_error" ErrDotEnvVarMarshal = "dotenv_var_marshal_error" - ErrDomainLong = "domain_long" ErrEnterpriseNotFound = "enterprise_not_found" ErrFailedAddingCollaborator = "failed_adding_collaborator" ErrFailedCreatingApp = "failed_creating_app" @@ -153,6 +153,7 @@ const ( ErrInvalidResourceID = "invalid_resource_id" ErrInvalidResourceType = "invalid_resource_type" ErrInvalidS3Key = "invalid_s3_key" + ErrInvalidSandboxArchiveTTL = "invalid_archive_ttl" ErrInvalidScopes = "invalid_scopes" ErrInvalidSemVer = "invalid_semver" ErrInvalidSlackProjectDirectory = "invalid_slack_project_directory" @@ -166,6 +167,7 @@ const ( ErrInvalidTriggerInputs = "invalid_trigger_inputs" ErrInvalidTriggerType = "invalid_trigger_type" ErrInvalidSandboxTemplateID = "invalid_template_id" + ErrInvalidSandboxTeamID = "invalid_sandbox_team_id" ErrInvalidUserID = "invalid_user_id" ErrInvalidWebhookConfig = "invalid_webhook_config" ErrInvalidWebhookSchemaRef = "invalid_webhook_schema_ref" @@ -219,6 +221,8 @@ const ( ErrRestrictedPlanLevel = "restricted_plan_level" ErrRuntimeNotFound = "runtime_not_found" ErrRuntimeNotSupported = "runtime_not_supported" + ErrSandboxDomainTaken = "domain_taken" + ErrSandboxDomainTooLong = "domain_long" ErrSDKConfigLoad = "sdk_config_load_error" ErrSDKHookInvocationFailed = "sdk_hook_invocation_failed" ErrSDKHookNotFound = "sdk_hook_not_found" @@ -273,10 +277,6 @@ const ( ErrUserRemovedFromTeam = "user_removed_from_team" ErrWorkflowNotFound = "workflow_not_found" ErrYaml = "yaml_error" - ErrSandboxDomainTaken = "domain_taken" - ErrAtActiveSandboxLimit = "at_active_sandbox_limit" - ErrInvalidSandboxTeamID = "invalid_sandbox_team_id" - ErrInvalidSandboxArchiveTTL = "invalid_archive_ttl" ) var ErrorCodeMap = map[string]Error{ @@ -464,6 +464,11 @@ Otherwise start your app for local development with: %s`, Message: "Couldn't rename your app", }, + ErrAtActiveSandboxLimit: { + Code: ErrAtActiveSandboxLimit, + Message: "You've reached the maximum number of active sandboxes", + }, + ErrAuthProdTokenNotFound: { Code: ErrAuthProdTokenNotFound, Message: "Couldn't find a valid auth token for the Slack API", @@ -710,11 +715,6 @@ Otherwise start your app for local development with: %s`, Message: "Failed to marshal the .env variable", }, - ErrDomainLong: { - Code: ErrDomainLong, - Message: "Sandbox name or domain is too long", - }, - ErrEnterpriseNotFound: { Code: ErrEnterpriseNotFound, Message: "The `enterprise` was not found", @@ -1006,6 +1006,12 @@ Otherwise start your app for local development with: %s`, Remediation: "Please reach out to feedback@slack.com if the problem persists.", }, + ErrInvalidSandboxArchiveTTL: { + Code: ErrInvalidSandboxArchiveTTL, + Message: "Invalid TTL", + Remediation: "Use days (1d), weeks (2w), or months (3mo); min 1d, max 6mo", + }, + ErrInvalidScopes: { Code: ErrInvalidScopes, Message: "Some of the provided scopes do not exist", @@ -1076,6 +1082,12 @@ Otherwise start your app for local development with: %s`, Message: "The provided sandbox template value is invalid", }, + ErrInvalidSandboxTeamID: { + Code: ErrInvalidSandboxTeamID, + Message: "The provided sandbox team ID is invalid", + Remediation: fmt.Sprintf("List your sandboxes with the %s command to find the ID", style.Commandf("sandbox list", false)), + }, + ErrInvalidUserID: { Code: ErrInvalidUserID, Message: "A value passed as a user_id is invalid", @@ -1349,6 +1361,16 @@ Otherwise start your app for local development with: %s`, Message: "The SDK runtime is not supported by the CLI", }, + ErrSandboxDomainTaken: { + Code: ErrSandboxDomainTaken, + Message: "This domain has been claimed by another sandbox", + }, + + ErrSandboxDomainTooLong: { + Code: ErrSandboxDomainTooLong, + Message: "Sandbox name or domain is too long", + }, + ErrSampleCreate: { Code: ErrSampleCreate, Message: "Couldn't create app from sample", @@ -1647,26 +1669,4 @@ Otherwise start your app for local development with: %s`, Code: ErrYaml, Message: "An error occurred while parsing the app manifest YAML file", }, - - ErrSandboxDomainTaken: { - Code: ErrSandboxDomainTaken, - Message: "This domain has been claimed by another sandbox", - }, - - ErrAtActiveSandboxLimit: { - Code: ErrAtActiveSandboxLimit, - Message: "You've reached the maximum number of active sandboxes", - }, - - ErrInvalidSandboxTeamID: { - Code: ErrInvalidSandboxTeamID, - Message: "The provided sandbox team ID is invalid", - Remediation: fmt.Sprintf("List your sandboxes with the %s command to find the ID", style.Commandf("sandbox list", false)), - }, - - ErrInvalidSandboxArchiveTTL: { - Code: ErrInvalidSandboxArchiveTTL, - Message: "Invalid TTL", - Remediation: "Use days (1d), weeks (2w), or months (3mo); min 1d, max 6mo", - }, } From 508df39b3dc1d509d85f6a32176c01384b7dd4fe Mon Sep 17 00:00:00 2001 From: Elaine Vegeris Date: Thu, 30 Apr 2026 09:54:05 -0400 Subject: [PATCH 4/4] update info log --- cmd/app/link.go | 2 +- cmd/sandbox/sandbox.go | 4 +--- internal/prompts/team_select.go | 19 ++++++++++++++----- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/cmd/app/link.go b/cmd/app/link.go index a1f3945c..044f56c3 100644 --- a/cmd/app/link.go +++ b/cmd/app/link.go @@ -210,7 +210,7 @@ func LinkAppFooterSection(ctx context.Context, clients *shared.ClientFactory, ap // promptExistingApp gathers details to represent app information func promptExistingApp(ctx context.Context, clients *shared.ClientFactory) (types.App, *types.SlackAuth, error) { - slackAuth, err := prompts.PromptTeamSlackAuth(ctx, clients, "Select the existing app team") + slackAuth, err := prompts.PromptTeamSlackAuth(ctx, clients, "Select the existing app team", nil) if err != nil { return types.App{}, &types.SlackAuth{}, err } diff --git a/cmd/sandbox/sandbox.go b/cmd/sandbox/sandbox.go index fd3d42e9..fe8dd38a 100644 --- a/cmd/sandbox/sandbox.go +++ b/cmd/sandbox/sandbox.go @@ -63,9 +63,7 @@ func getSandboxAuth(ctx context.Context, clients *shared.ClientFactory) (*types. } // Prompt the user to select a team to use for authentication - // TODO(experiment:charm): Change this to prompt "help" message once charm is stable - clients.IO.PrintInfo(ctx, false, "%s", style.Secondary("Choose a Slack team where your email address matches your Slack developer account")) - auth, err := prompts.PromptTeamSlackAuth(ctx, clients, "Select a team for authentication") + auth, err := prompts.PromptTeamSlackAuth(ctx, clients, "Select a team for authentication", &prompts.PromptTeamSlackAuthConfig{HelpText: "Your email address on the selected team should match your Slack developer account"}) if err != nil { return nil, err } diff --git a/internal/prompts/team_select.go b/internal/prompts/team_select.go index f1cdbccf..4c6240d8 100644 --- a/internal/prompts/team_select.go +++ b/internal/prompts/team_select.go @@ -26,9 +26,13 @@ import ( "github.com/slackapi/slack-cli/internal/style" ) +type PromptTeamSlackAuthConfig struct { + HelpText string +} + // PromptTeamSlackAuth prompts the user to select a team that they're logged in to and returns the auth information. // If the user is only logged in to one team, we return it by default. -func PromptTeamSlackAuth(ctx context.Context, clients *shared.ClientFactory, promptText string) (*types.SlackAuth, error) { +func PromptTeamSlackAuth(ctx context.Context, clients *shared.ClientFactory, promptText string, promptConfig *PromptTeamSlackAuthConfig) (*types.SlackAuth, error) { allAuths, err := clients.Auth().Auths(ctx) if err != nil { @@ -54,14 +58,19 @@ func PromptTeamSlackAuth(ctx context.Context, clients *shared.ClientFactory, pro ) } + selectPromptConfig := iostreams.SelectPromptConfig{ + Required: true, + Flag: clients.Config.Flags.Lookup("team"), + } + if promptConfig != nil && promptConfig.HelpText != "" { + selectPromptConfig.Help = promptConfig.HelpText + } + selection, err := clients.IO.SelectPrompt( ctx, promptText, teamLabels, - iostreams.SelectPromptConfig{ - Required: true, - Flag: clients.Config.Flags.Lookup("team"), - }, + selectPromptConfig, ) if err != nil { return &types.SlackAuth{}, err