From f248043adfbb2dda53ae353567a7e0ffc61c5cd6 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Mon, 23 Mar 2026 18:42:37 -0700 Subject: [PATCH 01/24] go --- cmd/docs/docs.go | 9 +- cmd/docs/search.go | 140 +++++++++++++++++++++++++ cmd/docs/search_test.go | 223 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 cmd/docs/search.go create mode 100644 cmd/docs/search_test.go diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index 9b47c3e8..d351d454 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -47,12 +47,18 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command { Command: "docs --search", }, }), + Args: cobra.ArbitraryArgs, // Allow any arguments RunE: func(cmd *cobra.Command, args []string) error { return runDocsCommand(clients, cmd, args) }, + // Disable automatic suggestions for unknown commands + DisableSuggestions: true, } - cmd.Flags().BoolVar(&searchMode, "search", false, "open Slack docs search page or search with query") + cmd.Flags().BoolVar(&searchMode, "search", false, "[DEPRECATED] open Slack docs search page or search with query (use 'docs search' subcommand instead)") + + // Add the experimental search subcommand + cmd.AddCommand(NewSearchCommand(clients)) return cmd } @@ -74,6 +80,7 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st } if cmd.Flags().Changed("search") { + clients.IO.PrintWarning(ctx, "The `--search` flag is deprecated. Use 'docs search' subcommand instead.") if len(args) > 0 { // --search "query" (space-separated) - join all args as the query query := strings.Join(args, " ") diff --git a/cmd/docs/search.go b/cmd/docs/search.go new file mode 100644 index 00000000..d70ba4bd --- /dev/null +++ b/cmd/docs/search.go @@ -0,0 +1,140 @@ +// Copyright 2022-2026 Salesforce, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package docs + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/slackapi/slack-cli/internal/shared" + "github.com/slackapi/slack-cli/internal/slacktrace" + "github.com/slackapi/slack-cli/internal/style" + "github.com/spf13/cobra" +) + +var searchOutputFlag string +var searchLimitFlag int + +// response from the Slack docs search API +type DocsSearchResponse struct { + TotalResults int `json:"total_results"` + Results []DocsSearchResult `json:"results"` + Limit int `json:"limit"` +} + +// single search result +type DocsSearchResult struct { + URL string `json:"url"` + Title string `json:"title"` +} + +func NewSearchCommand(clients *shared.ClientFactory) *cobra.Command { + cmd := &cobra.Command{ + Use: "search ", + Short: "Search Slack developer docs (experimental)", + Long: "Search the Slack developer docs and return results in browser or JSON format", + Example: style.ExampleCommandsf([]style.ExampleCommand{ + { + Meaning: "Search docs and open results in browser", + Command: "docs search \"Block Kit\"", + }, + { + Meaning: "Search docs and return JSON results", + Command: "docs search \"webhooks\" --output=json", + }, + { + Meaning: "Search docs with limited JSON results", + Command: "docs search \"api\" --output=json --limit=5", + }, + }), + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runDocsSearchCommand(clients, cmd, args, http.DefaultClient) + }, + } + + cmd.Flags().StringVar(&searchOutputFlag, "output", "json", "output format: browser, json") + cmd.Flags().IntVar(&searchLimitFlag, "limit", 20, "maximum number of search results to return (only applies with --output=json)") + + return cmd +} + +// handles the docs search subcommand +func runDocsSearchCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []string, httpClient *http.Client) error { + ctx := cmd.Context() + + query := strings.Join(args, " ") + + if searchOutputFlag == "json" { + return fetchAndOutputSearchResults(ctx, clients, query, searchLimitFlag, httpClient) + } + + // Browser output - open search results in browser + encodedQuery := url.QueryEscape(query) + docsURL := fmt.Sprintf("https://docs.slack.dev/search/?q=%s", encodedQuery) + + clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ + Emoji: "books", + Text: "Docs Search", + Secondary: []string{ + docsURL, + }, + })) + + clients.Browser().OpenURL(docsURL) + clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, query) + + return nil +} + +// fetches search results from the docs API and outputs as JSON +func fetchAndOutputSearchResults(ctx context.Context, clients *shared.ClientFactory, query string, limit int, httpClient *http.Client) error { + // Build API URL with limit parameter + apiURL := fmt.Sprintf("https://docs-slack-d-search-api-duu9zr.herokuapp.com/api/search?q=%s&limit=%d", url.QueryEscape(query), limit) + + // Make HTTP request + resp, err := httpClient.Get(apiURL) + if err != nil { + return fmt.Errorf("failed to fetch search results: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("API returned status %d", resp.StatusCode) + } + + // Parse JSON response + var searchResponse DocsSearchResponse + if err := json.NewDecoder(resp.Body).Decode(&searchResponse); err != nil { + return fmt.Errorf("failed to parse search results: %w", err) + } + + // Output as JSON + output, err := json.MarshalIndent(searchResponse, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON output: %w", err) + } + + fmt.Println(string(output)) + + // Trace the successful API call + clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, query) + + return nil +} diff --git a/cmd/docs/search_test.go b/cmd/docs/search_test.go new file mode 100644 index 00000000..fa8c4e8c --- /dev/null +++ b/cmd/docs/search_test.go @@ -0,0 +1,223 @@ +// Copyright 2022-2026 Salesforce, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package docs + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "testing" + + "github.com/slackapi/slack-cli/internal/shared" + "github.com/slackapi/slack-cli/internal/slackcontext" + "github.com/slackapi/slack-cli/test/testutil" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// mockRoundTripper implements http.RoundTripper to mock HTTP responses for testing. +// It allows tests to control the response status code and body without making real network calls. +// It also captures the request URL for assertion purposes. +type mockRoundTripper struct { + response string + status int + capturedURL string +} + +// RoundTrip executes a mocked HTTP request and returns a controlled response. +func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + m.capturedURL = req.URL.String() + return &http.Response{ + StatusCode: m.status, + Body: io.NopCloser(bytes.NewBufferString(m.response)), + Header: make(http.Header), + }, nil +} + +// setupJSONOutputTest creates a mock client and clients factory for JSON output tests. +func setupJSONOutputTest(t *testing.T, response string, status int) (*http.Client, *shared.ClientFactory, *mockRoundTripper) { + clientsMock := shared.NewClientsMock() + clientsMock.AddDefaultMocks() + clients := shared.NewClientFactory(clientsMock.MockClientFactory()) + + mockTransport := &mockRoundTripper{ + response: response, + status: status, + } + mockClient := &http.Client{ + Transport: mockTransport, + } + + return mockClient, clients, mockTransport +} + +// JSON Output Tests + +// Test_Docs_SearchCommand_JSONOutput_APIError verifies that HTTP errors from the API +// (e.g., 404 Not Found) are properly caught and returned as errors. +func Test_Docs_SearchCommand_JSONOutput_APIError(t *testing.T) { + mockClient, clients, _ := setupJSONOutputTest(t, `{"error": "not found"}`, http.StatusNotFound) + err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "nonexistent", 20, mockClient) + assert.Error(t, err) + assert.Contains(t, err.Error(), "API returned status 404") +} + +// Test_Docs_SearchCommand_JSONOutput_InvalidJSON verifies that malformed JSON responses +// from the API are caught during parsing and returned as errors. +func Test_Docs_SearchCommand_JSONOutput_InvalidJSON(t *testing.T) { + mockClient, clients, _ := setupJSONOutputTest(t, `{invalid json}`, http.StatusOK) + err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "test", 20, mockClient) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to parse search results") +} + +// Test_Docs_SearchCommand_JSONOutput_EmptyResults verifies that valid JSON responses with no results +// are correctly parsed and output without errors. +func Test_Docs_SearchCommand_JSONOutput_EmptyResults(t *testing.T) { + mockResponse := `{ + "total_results": 0, + "limit": 20, + "results": [] + }` + + mockClient, clients, _ := setupJSONOutputTest(t, mockResponse, http.StatusOK) + err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "nonexistent query", 20, mockClient) + require.NoError(t, err) +} + +// Test_Docs_SearchCommand_JSONOutput_QueryFormats tests JSON output with various query formats +// to ensure proper URL encoding, API parameter handling, and response parsing. +func Test_Docs_SearchCommand_JSONOutput_QueryFormats(t *testing.T) { + mockResponse := `{ + "total_results": 2, + "limit": 20, + "results": [ + { + "title": "Block Kit", + "url": "https://docs.slack.dev/block-kit" + }, + { + "title": "Block Kit Elements", + "url": "https://docs.slack.dev/block-kit/elements" + } + ] + }` + + tests := map[string]struct { + query string + limit int + expected string + }{ + "single word query": { + query: "messaging", + limit: 20, + expected: "messaging", + }, + "multiple words": { + query: "socket mode", + limit: 20, + expected: "socket+mode", + }, + "special characters": { + query: "messages & webhooks", + limit: 20, + expected: "messages+%26+webhooks", + }, + "custom limit": { + query: "Block Kit", + limit: 5, + expected: "Block+Kit", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + mockClient, clients, mockTransport := setupJSONOutputTest(t, mockResponse, http.StatusOK) + err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, tc.query, tc.limit, mockClient) + require.NoError(t, err) + assert.Contains(t, mockTransport.capturedURL, "q="+tc.expected) + assert.Contains(t, mockTransport.capturedURL, "limit="+fmt.Sprint(tc.limit)) + }) + } +} + +// Browser Output Tests + +// Test_Docs_SearchCommand_BrowserOutput tests the browser output mode with various query formats +// to ensure proper URL encoding and command execution. +func Test_Docs_SearchCommand_BrowserOutput(t *testing.T) { + testutil.TableTestCommand(t, testutil.CommandTests{ + "opens browser with search query using space syntax": { + CmdArgs: []string{"search", "messaging", "--output=browser"}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + expectedURL := "https://docs.slack.dev/search/?q=messaging" + cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + }, + ExpectedOutputs: []string{ + "Docs Search", + "https://docs.slack.dev/search/?q=messaging", + }, + }, + "handles search with multiple arguments": { + CmdArgs: []string{"search", "Block", "Kit", "Element", "--output=browser"}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + expectedURL := "https://docs.slack.dev/search/?q=Block+Kit+Element" + cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + }, + ExpectedOutputs: []string{ + "Docs Search", + "https://docs.slack.dev/search/?q=Block+Kit+Element", + }, + }, + "handles search query with multiple words": { + CmdArgs: []string{"search", "socket mode", "--output=browser"}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + expectedURL := "https://docs.slack.dev/search/?q=socket+mode" + cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + }, + ExpectedOutputs: []string{ + "Docs Search", + "https://docs.slack.dev/search/?q=socket+mode", + }, + }, + "handles special characters in search query": { + CmdArgs: []string{"search", "messages & webhooks", "--output=browser"}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + expectedURL := "https://docs.slack.dev/search/?q=messages+%26+webhooks" + cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + }, + ExpectedOutputs: []string{ + "Docs Search", + "https://docs.slack.dev/search/?q=messages+%26+webhooks", + }, + }, + "handles search query with quotes": { + CmdArgs: []string{"search", "webhook \"send message\"", "--output=browser"}, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + expectedURL := "https://docs.slack.dev/search/?q=webhook+%22send+message%22" + cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + }, + ExpectedOutputs: []string{ + "Docs Search", + "https://docs.slack.dev/search/?q=webhook+%22send+message%22", + }, + }, + }, func(cf *shared.ClientFactory) *cobra.Command { + return NewCommand(cf) + }) +} From 94c8a90491e4e61f77ec6c24199c618f2b627e8f Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 24 Mar 2026 09:38:33 -0700 Subject: [PATCH 02/24] refactoring --- cmd/docs/docs.go | 2 +- cmd/docs/search.go | 46 +++++++++++++++++++---------------------- cmd/docs/search_test.go | 42 +++++++++++++++++++++++-------------- 3 files changed, 48 insertions(+), 42 deletions(-) diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index d351d454..37b0f0c4 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -57,7 +57,7 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command { cmd.Flags().BoolVar(&searchMode, "search", false, "[DEPRECATED] open Slack docs search page or search with query (use 'docs search' subcommand instead)") - // Add the experimental search subcommand + // Add the search subcommand cmd.AddCommand(NewSearchCommand(clients)) return cmd diff --git a/cmd/docs/search.go b/cmd/docs/search.go index d70ba4bd..3063ff8e 100644 --- a/cmd/docs/search.go +++ b/cmd/docs/search.go @@ -20,6 +20,7 @@ import ( "fmt" "net/http" "net/url" + "os" "strings" "github.com/slackapi/slack-cli/internal/shared" @@ -28,26 +29,30 @@ import ( "github.com/spf13/cobra" ) -var searchOutputFlag string -var searchLimitFlag int +const docsSearchAPIURL = "https://docs-slack-d-search-api-duu9zr.herokuapp.com/api/search" + +type searchConfig struct { + output string + limit int +} -// response from the Slack docs search API type DocsSearchResponse struct { TotalResults int `json:"total_results"` Results []DocsSearchResult `json:"results"` Limit int `json:"limit"` } -// single search result type DocsSearchResult struct { URL string `json:"url"` Title string `json:"title"` } func NewSearchCommand(clients *shared.ClientFactory) *cobra.Command { + cfg := &searchConfig{} + cmd := &cobra.Command{ Use: "search ", - Short: "Search Slack developer docs (experimental)", + Short: "Search Slack developer docs", Long: "Search the Slack developer docs and return results in browser or JSON format", Example: style.ExampleCommandsf([]style.ExampleCommand{ { @@ -65,27 +70,25 @@ func NewSearchCommand(clients *shared.ClientFactory) *cobra.Command { }), Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runDocsSearchCommand(clients, cmd, args, http.DefaultClient) + return runDocsSearchCommand(clients, cmd, args, cfg, http.DefaultClient) }, } - cmd.Flags().StringVar(&searchOutputFlag, "output", "json", "output format: browser, json") - cmd.Flags().IntVar(&searchLimitFlag, "limit", 20, "maximum number of search results to return (only applies with --output=json)") + cmd.Flags().StringVar(&cfg.output, "output", "json", "output format: browser, json") + cmd.Flags().IntVar(&cfg.limit, "limit", 20, "maximum number of search results to return (only applies with --output=json)") return cmd } -// handles the docs search subcommand -func runDocsSearchCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []string, httpClient *http.Client) error { +func runDocsSearchCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []string, cfg *searchConfig, httpClient *http.Client) error { ctx := cmd.Context() query := strings.Join(args, " ") - if searchOutputFlag == "json" { - return fetchAndOutputSearchResults(ctx, clients, query, searchLimitFlag, httpClient) + if cfg.output == "json" { + return fetchAndOutputSearchResults(ctx, clients, query, cfg.limit, httpClient) } - // Browser output - open search results in browser encodedQuery := url.QueryEscape(query) docsURL := fmt.Sprintf("https://docs.slack.dev/search/?q=%s", encodedQuery) @@ -103,12 +106,9 @@ func runDocsSearchCommand(clients *shared.ClientFactory, cmd *cobra.Command, arg return nil } -// fetches search results from the docs API and outputs as JSON func fetchAndOutputSearchResults(ctx context.Context, clients *shared.ClientFactory, query string, limit int, httpClient *http.Client) error { - // Build API URL with limit parameter - apiURL := fmt.Sprintf("https://docs-slack-d-search-api-duu9zr.herokuapp.com/api/search?q=%s&limit=%d", url.QueryEscape(query), limit) + apiURL := fmt.Sprintf("%s?q=%s&limit=%d", docsSearchAPIURL, url.QueryEscape(query), limit) - // Make HTTP request resp, err := httpClient.Get(apiURL) if err != nil { return fmt.Errorf("failed to fetch search results: %w", err) @@ -119,21 +119,17 @@ func fetchAndOutputSearchResults(ctx context.Context, clients *shared.ClientFact return fmt.Errorf("API returned status %d", resp.StatusCode) } - // Parse JSON response var searchResponse DocsSearchResponse if err := json.NewDecoder(resp.Body).Decode(&searchResponse); err != nil { return fmt.Errorf("failed to parse search results: %w", err) } - // Output as JSON - output, err := json.MarshalIndent(searchResponse, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal JSON output: %w", err) + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent("", " ") + if err := encoder.Encode(searchResponse); err != nil { + return fmt.Errorf("failed to output search results: %w", err) } - fmt.Println(string(output)) - - // Trace the successful API call clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, query) return nil diff --git a/cmd/docs/search_test.go b/cmd/docs/search_test.go index fa8c4e8c..5d39156d 100644 --- a/cmd/docs/search_test.go +++ b/cmd/docs/search_test.go @@ -49,8 +49,23 @@ func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) }, nil } -// setupJSONOutputTest creates a mock client and clients factory for JSON output tests. -func setupJSONOutputTest(t *testing.T, response string, status int) (*http.Client, *shared.ClientFactory, *mockRoundTripper) { +func setupJSONOutputTest(t *testing.T, response string, status int) (*http.Client, *shared.ClientFactory) { + clientsMock := shared.NewClientsMock() + clientsMock.AddDefaultMocks() + clients := shared.NewClientFactory(clientsMock.MockClientFactory()) + + mockTransport := &mockRoundTripper{ + response: response, + status: status, + } + mockClient := &http.Client{ + Transport: mockTransport, + } + + return mockClient, clients +} + +func setupJSONOutputTestWithCapture(t *testing.T, response string, status int) (*http.Client, *shared.ClientFactory, *mockRoundTripper) { clientsMock := shared.NewClientsMock() clientsMock.AddDefaultMocks() clients := shared.NewClientFactory(clientsMock.MockClientFactory()) @@ -68,26 +83,23 @@ func setupJSONOutputTest(t *testing.T, response string, status int) (*http.Clien // JSON Output Tests -// Test_Docs_SearchCommand_JSONOutput_APIError verifies that HTTP errors from the API -// (e.g., 404 Not Found) are properly caught and returned as errors. +// Verifies that HTTP errors from the API are properly caught and returned as errors. func Test_Docs_SearchCommand_JSONOutput_APIError(t *testing.T) { - mockClient, clients, _ := setupJSONOutputTest(t, `{"error": "not found"}`, http.StatusNotFound) + mockClient, clients := setupJSONOutputTest(t, `{"error": "not found"}`, http.StatusNotFound) err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "nonexistent", 20, mockClient) assert.Error(t, err) assert.Contains(t, err.Error(), "API returned status 404") } -// Test_Docs_SearchCommand_JSONOutput_InvalidJSON verifies that malformed JSON responses -// from the API are caught during parsing and returned as errors. +// Verifies that malformed JSON responses are caught during parsing and returned as errors. func Test_Docs_SearchCommand_JSONOutput_InvalidJSON(t *testing.T) { - mockClient, clients, _ := setupJSONOutputTest(t, `{invalid json}`, http.StatusOK) + mockClient, clients := setupJSONOutputTest(t, `{invalid json}`, http.StatusOK) err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "test", 20, mockClient) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to parse search results") } -// Test_Docs_SearchCommand_JSONOutput_EmptyResults verifies that valid JSON responses with no results -// are correctly parsed and output without errors. +// Verifies that valid JSON responses with no results are correctly parsed and output without errors. func Test_Docs_SearchCommand_JSONOutput_EmptyResults(t *testing.T) { mockResponse := `{ "total_results": 0, @@ -95,13 +107,12 @@ func Test_Docs_SearchCommand_JSONOutput_EmptyResults(t *testing.T) { "results": [] }` - mockClient, clients, _ := setupJSONOutputTest(t, mockResponse, http.StatusOK) + mockClient, clients := setupJSONOutputTest(t, mockResponse, http.StatusOK) err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "nonexistent query", 20, mockClient) require.NoError(t, err) } -// Test_Docs_SearchCommand_JSONOutput_QueryFormats tests JSON output with various query formats -// to ensure proper URL encoding, API parameter handling, and response parsing. +// Verifies that various query formats are properly URL encoded and API parameters are correctly passed. func Test_Docs_SearchCommand_JSONOutput_QueryFormats(t *testing.T) { mockResponse := `{ "total_results": 2, @@ -147,7 +158,7 @@ func Test_Docs_SearchCommand_JSONOutput_QueryFormats(t *testing.T) { for name, tc := range tests { t.Run(name, func(t *testing.T) { - mockClient, clients, mockTransport := setupJSONOutputTest(t, mockResponse, http.StatusOK) + mockClient, clients, mockTransport := setupJSONOutputTestWithCapture(t, mockResponse, http.StatusOK) err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, tc.query, tc.limit, mockClient) require.NoError(t, err) assert.Contains(t, mockTransport.capturedURL, "q="+tc.expected) @@ -158,8 +169,7 @@ func Test_Docs_SearchCommand_JSONOutput_QueryFormats(t *testing.T) { // Browser Output Tests -// Test_Docs_SearchCommand_BrowserOutput tests the browser output mode with various query formats -// to ensure proper URL encoding and command execution. +// Verifies that browser output mode correctly handles various query formats and opens the correct URLs. func Test_Docs_SearchCommand_BrowserOutput(t *testing.T) { testutil.TableTestCommand(t, testutil.CommandTests{ "opens browser with search query using space syntax": { From d7bbb84c1d44437960ac404c2915355008fb99c1 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 24 Mar 2026 09:46:45 -0700 Subject: [PATCH 03/24] go --- cmd/docs/docs.go | 8 ++++---- cmd/docs/search.go | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index 37b0f0c4..51159d56 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -32,7 +32,7 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command { cmd := &cobra.Command{ Use: "docs", Short: "Open Slack developer docs", - Long: "Open the Slack developer docs in your browser, with optional search functionality", + Long: "Open the Slack developer docs in your browser or search them using the search subcommand", Example: style.ExampleCommandsf([]style.ExampleCommand{ { Meaning: "Open Slack developer docs homepage", @@ -40,11 +40,11 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command { }, { Meaning: "Search Slack developer docs for Block Kit", - Command: "docs --search \"Block Kit\"", + Command: "docs search \"Block Kit\"", }, { - Meaning: "Open Slack docs search page", - Command: "docs --search", + Meaning: "Search docs and open results in browser", + Command: "docs search \"Block Kit\" --output=browser", }, }), Args: cobra.ArbitraryArgs, // Allow any arguments diff --git a/cmd/docs/search.go b/cmd/docs/search.go index 3063ff8e..a6257084 100644 --- a/cmd/docs/search.go +++ b/cmd/docs/search.go @@ -56,12 +56,12 @@ func NewSearchCommand(clients *shared.ClientFactory) *cobra.Command { Long: "Search the Slack developer docs and return results in browser or JSON format", Example: style.ExampleCommandsf([]style.ExampleCommand{ { - Meaning: "Search docs and open results in browser", + Meaning: "Search docs and return JSON results", Command: "docs search \"Block Kit\"", }, { - Meaning: "Search docs and return JSON results", - Command: "docs search \"webhooks\" --output=json", + Meaning: "Search docs and open results in browser", + Command: "docs search \"webhooks\" --output=browser", }, { Meaning: "Search docs with limited JSON results", From 727d7cec00a32354ab6295ef3aec49f9cc7fb5f1 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 24 Mar 2026 09:51:10 -0700 Subject: [PATCH 04/24] more test coverage --- cmd/docs/search_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/cmd/docs/search_test.go b/cmd/docs/search_test.go index 5d39156d..fb073433 100644 --- a/cmd/docs/search_test.go +++ b/cmd/docs/search_test.go @@ -37,10 +37,14 @@ type mockRoundTripper struct { response string status int capturedURL string + returnError bool } // RoundTrip executes a mocked HTTP request and returns a controlled response. func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + if m.returnError { + return nil, fmt.Errorf("mock network error") + } m.capturedURL = req.URL.String() return &http.Response{ StatusCode: m.status, @@ -83,6 +87,29 @@ func setupJSONOutputTestWithCapture(t *testing.T, response string, status int) ( // JSON Output Tests +// Verifies that HTTP request errors are properly caught and returned as errors. +func Test_Docs_SearchCommand_JSONOutput_HTTPError(t *testing.T) { + ctx := slackcontext.MockContext(context.Background()) + clientsMock := shared.NewClientsMock() + clientsMock.AddDefaultMocks() + clients := shared.NewClientFactory(clientsMock.MockClientFactory()) + + // Create a mock transport that returns an error + mockTransport := &mockRoundTripper{ + response: "", + status: 0, + } + mockTransport.returnError = true + + mockClient := &http.Client{ + Transport: mockTransport, + } + + err := fetchAndOutputSearchResults(ctx, clients, "test", 20, mockClient) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to fetch search results") +} + // Verifies that HTTP errors from the API are properly caught and returned as errors. func Test_Docs_SearchCommand_JSONOutput_APIError(t *testing.T) { mockClient, clients := setupJSONOutputTest(t, `{"error": "not found"}`, http.StatusNotFound) From 49857131f3f76a89c193c1da30bf626ce5783f47 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 26 Mar 2026 14:37:45 -0700 Subject: [PATCH 05/24] refactor --- cmd/docs/docs.go | 21 +++-- cmd/docs/search.go | 110 +++++++++++++----------- cmd/docs/search_test.go | 180 ++++++++++++++++----------------------- internal/api/api_mock.go | 10 +++ internal/api/docs.go | 76 +++++++++++++++++ internal/api/types.go | 1 + 6 files changed, 231 insertions(+), 167 deletions(-) create mode 100644 internal/api/docs.go diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index 51159d56..30f5d92c 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -26,8 +26,15 @@ import ( "github.com/spf13/cobra" ) +const docsURL = "https://docs.slack.dev" + var searchMode bool +func buildDocsSearchURL(query string) string { + encodedQuery := url.QueryEscape(query) + return fmt.Sprintf("%s/search/?q=%s", docsURL, encodedQuery) +} + func NewCommand(clients *shared.ClientFactory) *cobra.Command { cmd := &cobra.Command{ Use: "docs", @@ -67,7 +74,7 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command { func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []string) error { ctx := cmd.Context() - var docsURL string + var finalURL string var sectionText string // Validate: if there are arguments, --search flag must be used @@ -80,21 +87,19 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st } if cmd.Flags().Changed("search") { - clients.IO.PrintWarning(ctx, "The `--search` flag is deprecated. Use 'docs search' subcommand instead.") if len(args) > 0 { // --search "query" (space-separated) - join all args as the query query := strings.Join(args, " ") - encodedQuery := url.QueryEscape(query) - docsURL = fmt.Sprintf("https://docs.slack.dev/search/?q=%s", encodedQuery) + finalURL = buildDocsSearchURL(query) sectionText = "Docs Search" } else { // --search (no argument) - open search page - docsURL = "https://docs.slack.dev/search/" + finalURL = fmt.Sprintf("%s/search/", docsURL) sectionText = "Docs Search" } } else { // No search flag: default homepage - docsURL = "https://docs.slack.dev" + finalURL = docsURL sectionText = "Docs Open" } @@ -102,11 +107,11 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st Emoji: "books", Text: sectionText, Secondary: []string{ - docsURL, + finalURL, }, })) - clients.Browser().OpenURL(docsURL) + clients.Browser().OpenURL(finalURL) if cmd.Flags().Changed("search") { traceValue := "" diff --git a/cmd/docs/search.go b/cmd/docs/search.go index a6257084..f6944c31 100644 --- a/cmd/docs/search.go +++ b/cmd/docs/search.go @@ -18,33 +18,25 @@ import ( "context" "encoding/json" "fmt" - "net/http" - "net/url" - "os" "strings" "github.com/slackapi/slack-cli/internal/shared" + "github.com/slackapi/slack-cli/internal/slackerror" "github.com/slackapi/slack-cli/internal/slacktrace" "github.com/slackapi/slack-cli/internal/style" "github.com/spf13/cobra" ) -const docsSearchAPIURL = "https://docs-slack-d-search-api-duu9zr.herokuapp.com/api/search" - type searchConfig struct { output string limit int } -type DocsSearchResponse struct { - TotalResults int `json:"total_results"` - Results []DocsSearchResult `json:"results"` - Limit int `json:"limit"` -} - -type DocsSearchResult struct { - URL string `json:"url"` - Title string `json:"title"` +func makeAbsoluteURL(relativeURL string) string { + if strings.HasPrefix(relativeURL, "http") { + return relativeURL + } + return docsURL + relativeURL } func NewSearchCommand(clients *shared.ClientFactory) *cobra.Command { @@ -53,10 +45,10 @@ func NewSearchCommand(clients *shared.ClientFactory) *cobra.Command { cmd := &cobra.Command{ Use: "search ", Short: "Search Slack developer docs", - Long: "Search the Slack developer docs and return results in browser or JSON format", + Long: "Search the Slack developer docs and return results in text, JSON, or browser format", Example: style.ExampleCommandsf([]style.ExampleCommand{ { - Meaning: "Search docs and return JSON results", + Meaning: "Search docs and return text results", Command: "docs search \"Block Kit\"", }, { @@ -70,64 +62,80 @@ func NewSearchCommand(clients *shared.ClientFactory) *cobra.Command { }), Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runDocsSearchCommand(clients, cmd, args, cfg, http.DefaultClient) + return runDocsSearchCommand(clients, cmd, args, cfg) }, } - cmd.Flags().StringVar(&cfg.output, "output", "json", "output format: browser, json") - cmd.Flags().IntVar(&cfg.limit, "limit", 20, "maximum number of search results to return (only applies with --output=json)") + cmd.Flags().StringVar(&cfg.output, "output", "text", "output format: text, json, browser") + cmd.Flags().IntVar(&cfg.limit, "limit", 20, "maximum number of search results to return (only applies with --output=json and --output=text)") return cmd } -func runDocsSearchCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []string, cfg *searchConfig, httpClient *http.Client) error { +func runDocsSearchCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []string, cfg *searchConfig) error { ctx := cmd.Context() query := strings.Join(args, " ") - if cfg.output == "json" { - return fetchAndOutputSearchResults(ctx, clients, query, cfg.limit, httpClient) + switch cfg.output { + case "json": + return fetchAndOutputSearchResults(ctx, clients, query, cfg.limit) + case "text": + return fetchAndOutputTextResults(ctx, clients, query, cfg.limit) + case "browser": + docsSearchURL := buildDocsSearchURL(query) + + clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ + Emoji: "books", + Text: "Docs Search", + Secondary: []string{ + docsSearchURL, + }, + })) + + clients.Browser().OpenURL(docsSearchURL) + clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, query) + + return nil + default: + return slackerror.New(slackerror.ErrInvalidFlag).WithMessage( + "Invalid output format: %s", cfg.output, + ).WithRemediation( + "Use one of: text, json, browser", + ) } +} - encodedQuery := url.QueryEscape(query) - docsURL := fmt.Sprintf("https://docs.slack.dev/search/?q=%s", encodedQuery) +func fetchAndOutputSearchResults(ctx context.Context, clients *shared.ClientFactory, query string, limit int) error { + searchResponse, err := clients.API().DocsSearch(ctx, query, limit) + if err != nil { + return err + } - clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ - Emoji: "books", - Text: "Docs Search", - Secondary: []string{ - docsURL, - }, - })) + for i := range searchResponse.Results { + searchResponse.Results[i].URL = makeAbsoluteURL(searchResponse.Results[i].URL) + } + + encoder := json.NewEncoder(clients.IO.WriteOut()) + encoder.SetIndent("", " ") + if err := encoder.Encode(searchResponse); err != nil { + return slackerror.New(slackerror.ErrUnableToParseJSON).WithRootCause(err) + } - clients.Browser().OpenURL(docsURL) clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, query) return nil } -func fetchAndOutputSearchResults(ctx context.Context, clients *shared.ClientFactory, query string, limit int, httpClient *http.Client) error { - apiURL := fmt.Sprintf("%s?q=%s&limit=%d", docsSearchAPIURL, url.QueryEscape(query), limit) - - resp, err := httpClient.Get(apiURL) +func fetchAndOutputTextResults(ctx context.Context, clients *shared.ClientFactory, query string, limit int) error { + searchResponse, err := clients.API().DocsSearch(ctx, query, limit) if err != nil { - return fmt.Errorf("failed to fetch search results: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("API returned status %d", resp.StatusCode) - } - - var searchResponse DocsSearchResponse - if err := json.NewDecoder(resp.Body).Decode(&searchResponse); err != nil { - return fmt.Errorf("failed to parse search results: %w", err) + return err } - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - if err := encoder.Encode(searchResponse); err != nil { - return fmt.Errorf("failed to output search results: %w", err) + for _, result := range searchResponse.Results { + absoluteURL := makeAbsoluteURL(result.URL) + fmt.Fprintf(clients.IO.WriteOut(), "%s\n%s\n\n", result.Title, absoluteURL) } clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, query) diff --git a/cmd/docs/search_test.go b/cmd/docs/search_test.go index fb073433..8fbc6af2 100644 --- a/cmd/docs/search_test.go +++ b/cmd/docs/search_test.go @@ -15,181 +15,145 @@ package docs import ( - "bytes" "context" - "fmt" - "io" - "net/http" "testing" + "github.com/slackapi/slack-cli/internal/api" "github.com/slackapi/slack-cli/internal/shared" "github.com/slackapi/slack-cli/internal/slackcontext" + "github.com/slackapi/slack-cli/internal/slackerror" "github.com/slackapi/slack-cli/test/testutil" "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -// mockRoundTripper implements http.RoundTripper to mock HTTP responses for testing. -// It allows tests to control the response status code and body without making real network calls. -// It also captures the request URL for assertion purposes. -type mockRoundTripper struct { - response string - status int - capturedURL string - returnError bool +// mockDocsAPI implements api.DocsClient for testing +type mockDocsAPI struct { + searchResponse *api.DocsSearchResponse + searchError error } -// RoundTrip executes a mocked HTTP request and returns a controlled response. -func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - if m.returnError { - return nil, fmt.Errorf("mock network error") - } - m.capturedURL = req.URL.String() - return &http.Response{ - StatusCode: m.status, - Body: io.NopCloser(bytes.NewBufferString(m.response)), - Header: make(http.Header), - }, nil +func (m *mockDocsAPI) DocsSearch(ctx context.Context, query string, limit int) (*api.DocsSearchResponse, error) { + return m.searchResponse, m.searchError } -func setupJSONOutputTest(t *testing.T, response string, status int) (*http.Client, *shared.ClientFactory) { +func setupDocsAPITest(t *testing.T, response *api.DocsSearchResponse, err error) *shared.ClientFactory { clientsMock := shared.NewClientsMock() clientsMock.AddDefaultMocks() clients := shared.NewClientFactory(clientsMock.MockClientFactory()) - mockTransport := &mockRoundTripper{ - response: response, - status: status, + mockDocsAPI := &mockDocsAPI{ + searchResponse: response, + searchError: err, } - mockClient := &http.Client{ - Transport: mockTransport, + + // Override the API to return our mock for DocsSearch + originalAPI := clients.API + clients.API = func() api.APIInterface { + realAPI := originalAPI() + // Return a wrapper that intercepts DocsSearch calls + return &docsAPIWrapper{ + APIInterface: realAPI, + mock: mockDocsAPI, + } } - return mockClient, clients + return clients } -func setupJSONOutputTestWithCapture(t *testing.T, response string, status int) (*http.Client, *shared.ClientFactory, *mockRoundTripper) { - clientsMock := shared.NewClientsMock() - clientsMock.AddDefaultMocks() - clients := shared.NewClientFactory(clientsMock.MockClientFactory()) - - mockTransport := &mockRoundTripper{ - response: response, - status: status, - } - mockClient := &http.Client{ - Transport: mockTransport, - } +// docsAPIWrapper wraps APIInterface to mock DocsSearch while delegating other methods +type docsAPIWrapper struct { + api.APIInterface + mock *mockDocsAPI +} - return mockClient, clients, mockTransport +func (w *docsAPIWrapper) DocsSearch(ctx context.Context, query string, limit int) (*api.DocsSearchResponse, error) { + return w.mock.DocsSearch(ctx, query, limit) } -// JSON Output Tests +// Text and JSON Output Tests // Verifies that HTTP request errors are properly caught and returned as errors. -func Test_Docs_SearchCommand_JSONOutput_HTTPError(t *testing.T) { +func Test_Docs_SearchCommand_TextJSONOutput_HTTPError(t *testing.T) { ctx := slackcontext.MockContext(context.Background()) - clientsMock := shared.NewClientsMock() - clientsMock.AddDefaultMocks() - clients := shared.NewClientFactory(clientsMock.MockClientFactory()) - - // Create a mock transport that returns an error - mockTransport := &mockRoundTripper{ - response: "", - status: 0, - } - mockTransport.returnError = true - - mockClient := &http.Client{ - Transport: mockTransport, - } + clients := setupDocsAPITest(t, nil, slackerror.New(slackerror.ErrHTTPRequestFailed)) - err := fetchAndOutputSearchResults(ctx, clients, "test", 20, mockClient) + err := fetchAndOutputSearchResults(ctx, clients, "test", 20) assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to fetch search results") } // Verifies that HTTP errors from the API are properly caught and returned as errors. -func Test_Docs_SearchCommand_JSONOutput_APIError(t *testing.T) { - mockClient, clients := setupJSONOutputTest(t, `{"error": "not found"}`, http.StatusNotFound) - err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "nonexistent", 20, mockClient) +func Test_Docs_SearchCommand_TextJSONOutput_APIError(t *testing.T) { + clients := setupDocsAPITest(t, nil, slackerror.New(slackerror.ErrHTTPRequestFailed)) + err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "nonexistent", 20) assert.Error(t, err) - assert.Contains(t, err.Error(), "API returned status 404") } // Verifies that malformed JSON responses are caught during parsing and returned as errors. -func Test_Docs_SearchCommand_JSONOutput_InvalidJSON(t *testing.T) { - mockClient, clients := setupJSONOutputTest(t, `{invalid json}`, http.StatusOK) - err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "test", 20, mockClient) +func Test_Docs_SearchCommand_TextJSONOutput_InvalidJSON(t *testing.T) { + clients := setupDocsAPITest(t, nil, slackerror.New(slackerror.ErrHTTPResponseInvalid)) + err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "test", 20) assert.Error(t, err) - assert.Contains(t, err.Error(), "failed to parse search results") } // Verifies that valid JSON responses with no results are correctly parsed and output without errors. -func Test_Docs_SearchCommand_JSONOutput_EmptyResults(t *testing.T) { - mockResponse := `{ - "total_results": 0, - "limit": 20, - "results": [] - }` +func Test_Docs_SearchCommand_TextJSONOutput_EmptyResults(t *testing.T) { + response := &api.DocsSearchResponse{ + TotalResults: 0, + Results: []api.DocsSearchItem{}, + Limit: 20, + } - mockClient, clients := setupJSONOutputTest(t, mockResponse, http.StatusOK) - err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "nonexistent query", 20, mockClient) + clients := setupDocsAPITest(t, response, nil) + err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "nonexistent query", 20) require.NoError(t, err) } // Verifies that various query formats are properly URL encoded and API parameters are correctly passed. -func Test_Docs_SearchCommand_JSONOutput_QueryFormats(t *testing.T) { - mockResponse := `{ - "total_results": 2, - "limit": 20, - "results": [ +func Test_Docs_SearchCommand_TextJSONOutput_QueryFormats(t *testing.T) { + response := &api.DocsSearchResponse{ + TotalResults: 2, + Limit: 20, + Results: []api.DocsSearchItem{ { - "title": "Block Kit", - "url": "https://docs.slack.dev/block-kit" + Title: "Block Kit", + URL: "/block-kit", }, { - "title": "Block Kit Elements", - "url": "https://docs.slack.dev/block-kit/elements" - } - ] - }` + Title: "Block Kit Elements", + URL: "/block-kit/elements", + }, + }, + } tests := map[string]struct { - query string - limit int - expected string + query string + limit int }{ "single word query": { - query: "messaging", - limit: 20, - expected: "messaging", + query: "messaging", + limit: 20, }, "multiple words": { - query: "socket mode", - limit: 20, - expected: "socket+mode", + query: "socket mode", + limit: 20, }, "special characters": { - query: "messages & webhooks", - limit: 20, - expected: "messages+%26+webhooks", + query: "messages & webhooks", + limit: 20, }, "custom limit": { - query: "Block Kit", - limit: 5, - expected: "Block+Kit", + query: "Block Kit", + limit: 5, }, } for name, tc := range tests { t.Run(name, func(t *testing.T) { - mockClient, clients, mockTransport := setupJSONOutputTestWithCapture(t, mockResponse, http.StatusOK) - err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, tc.query, tc.limit, mockClient) + clients := setupDocsAPITest(t, response, nil) + err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, tc.query, tc.limit) require.NoError(t, err) - assert.Contains(t, mockTransport.capturedURL, "q="+tc.expected) - assert.Contains(t, mockTransport.capturedURL, "limit="+fmt.Sprint(tc.limit)) }) } } diff --git a/internal/api/api_mock.go b/internal/api/api_mock.go index c68b8953..951b60bb 100644 --- a/internal/api/api_mock.go +++ b/internal/api/api_mock.go @@ -381,3 +381,13 @@ func (m *APIMock) DeveloperAppInstall(ctx context.Context, IO iostreams.IOStream args := m.Called(ctx, IO, token, app, botScopes, outgoingDomains, orgGrantWorkspaceID, autoAAARequest) return args.Get(0).(DeveloperAppInstallResult), args.Get(1).(types.InstallState), args.Error(2) } + +// DocsClient + +func (m *APIMock) DocsSearch(ctx context.Context, query string, limit int) (*DocsSearchResponse, error) { + args := m.Called(ctx, query, limit) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*DocsSearchResponse), args.Error(1) +} diff --git a/internal/api/docs.go b/internal/api/docs.go new file mode 100644 index 00000000..cf18e844 --- /dev/null +++ b/internal/api/docs.go @@ -0,0 +1,76 @@ +// Copyright 2022-2026 Salesforce, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/opentracing/opentracing-go" +) + +const docsSearchAPIURL = "https://docs-slack-d-search-api-duu9zr.herokuapp.com" + +type DocsClient interface { + DocsSearch(ctx context.Context, query string, limit int) (*DocsSearchResponse, error) +} + +type DocsSearchResponse struct { + TotalResults int `json:"total_results"` + Results []DocsSearchItem `json:"results"` + Limit int `json:"limit"` +} + +type DocsSearchItem struct { + URL string `json:"url"` + Title string `json:"title"` +} + +// DocsSearch searches the Slack developer docs API +func (c *Client) DocsSearch(ctx context.Context, query string, limit int) (*DocsSearchResponse, error) { + var span opentracing.Span + span, _ = opentracing.StartSpanFromContext(ctx, "apiclient.DocsSearch") + defer span.Finish() + + endpoint := fmt.Sprintf("api/search?q=%s&limit=%d", url.QueryEscape(query), limit) + sURL := docsSearchAPIURL + "/" + endpoint + + span.SetTag("request_url", sURL) + + req, err := http.NewRequestWithContext(ctx, "GET", sURL, nil) + if err != nil { + return nil, errHTTPRequestFailed.WithRootCause(err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, errHTTPRequestFailed.WithRootCause(err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, errHTTPRequestFailed.WithMessage(fmt.Sprintf("API returned status %d", resp.StatusCode)) + } + + var searchResponse DocsSearchResponse + if err := json.NewDecoder(resp.Body).Decode(&searchResponse); err != nil { + return nil, errHTTPResponseInvalid.WithRootCause(err) + } + + return &searchResponse, nil +} diff --git a/internal/api/types.go b/internal/api/types.go index 22f0431c..df5e570c 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -26,6 +26,7 @@ type APIInterface interface { ChannelClient CollaboratorsClient DatastoresClient + DocsClient ExternalAuthClient FunctionDistributionClient SandboxClient From 91b5ac501948416198d412d98a21c26d48812086 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 26 Mar 2026 14:39:41 -0700 Subject: [PATCH 06/24] remove deprecation warning --- cmd/docs/docs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index 30f5d92c..ac11cc6c 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -62,7 +62,7 @@ func NewCommand(clients *shared.ClientFactory) *cobra.Command { DisableSuggestions: true, } - cmd.Flags().BoolVar(&searchMode, "search", false, "[DEPRECATED] open Slack docs search page or search with query (use 'docs search' subcommand instead)") + cmd.Flags().BoolVar(&searchMode, "search", false, "open Slack docs search page or search with query") // Add the search subcommand cmd.AddCommand(NewSearchCommand(clients)) From 086b9e5661fe7355784d4a6f3462c7bf59b5ec5c Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 26 Mar 2026 14:48:14 -0700 Subject: [PATCH 07/24] go --- internal/api/docs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/docs.go b/internal/api/docs.go index cf18e844..66465644 100644 --- a/internal/api/docs.go +++ b/internal/api/docs.go @@ -64,7 +64,7 @@ func (c *Client) DocsSearch(ctx context.Context, query string, limit int) (*Docs defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, errHTTPRequestFailed.WithMessage(fmt.Sprintf("API returned status %d", resp.StatusCode)) + return nil, errHTTPRequestFailed.WithMessage("API returned status %d", resp.StatusCode) } var searchResponse DocsSearchResponse From 47613d012d7716c04154bba04640264726206193 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 26 Mar 2026 15:36:47 -0700 Subject: [PATCH 08/24] test --- cmd/docs/search_test.go | 57 ++++++++++ internal/api/docs_test.go | 219 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 internal/api/docs_test.go diff --git a/cmd/docs/search_test.go b/cmd/docs/search_test.go index 8fbc6af2..5736aa68 100644 --- a/cmd/docs/search_test.go +++ b/cmd/docs/search_test.go @@ -158,6 +158,63 @@ func Test_Docs_SearchCommand_TextJSONOutput_QueryFormats(t *testing.T) { } } +// JSON Output Tests + +// Verifies that JSON output mode correctly formats and outputs search results. +func Test_Docs_SearchCommand_JSONOutput(t *testing.T) { + response := &api.DocsSearchResponse{ + TotalResults: 1, + Limit: 20, + Results: []api.DocsSearchItem{ + { + Title: "Block Kit", + URL: "/block-kit", + }, + }, + } + + clients := setupDocsAPITest(t, response, nil) + err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "Block Kit", 20) + require.NoError(t, err) +} + +// Text Output Tests + +// Verifies that text output mode correctly formats and outputs search results. +func Test_Docs_SearchCommand_TextOutput(t *testing.T) { + response := &api.DocsSearchResponse{ + TotalResults: 1, + Limit: 20, + Results: []api.DocsSearchItem{ + { + Title: "Block Kit", + URL: "/block-kit", + }, + }, + } + + clients := setupDocsAPITest(t, response, nil) + err := fetchAndOutputTextResults(slackcontext.MockContext(context.Background()), clients, "Block Kit", 20) + require.NoError(t, err) +} + +// Invalid Output Format Tests + +// Verifies that invalid output format returns an error with helpful remediation. +func Test_Docs_SearchCommand_InvalidOutputFormat(t *testing.T) { + testutil.TableTestCommand(t, testutil.CommandTests{ + "rejects invalid output format": { + CmdArgs: []string{"search", "test", "--output=invalid"}, + ExpectedErrorStrings: []string{ + "Invalid output format", + "Use one of: text, json, browser", + }, + }, + }, func(cf *shared.ClientFactory) *cobra.Command { + return NewCommand(cf) + }) +} + // Browser Output Tests // Verifies that browser output mode correctly handles various query formats and opens the correct URLs. diff --git a/internal/api/docs_test.go b/internal/api/docs_test.go new file mode 100644 index 00000000..b4706756 --- /dev/null +++ b/internal/api/docs_test.go @@ -0,0 +1,219 @@ +// Copyright 2022-2026 Salesforce, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// mockRoundTripper implements http.RoundTripper for testing +type mockRoundTripper struct { + response *http.Response + err error + capturedURL string +} + +func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + m.capturedURL = req.URL.String() + return m.response, m.err +} + +// Test_DocsSearch_Success verifies successful API response parsing +func Test_DocsSearch_Success(t *testing.T) { + responseBody := DocsSearchResponse{ + TotalResults: 2, + Limit: 20, + Results: []DocsSearchItem{ + { + Title: "Block Kit", + URL: "/block-kit", + }, + { + Title: "Block Kit Elements", + URL: "/block-kit/elements", + }, + }, + } + + bodyBytes, err := json.Marshal(responseBody) + require.NoError(t, err) + + mockTransport := &mockRoundTripper{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(bodyBytes)), + Header: make(http.Header), + }, + } + + httpClient := &http.Client{ + Transport: mockTransport, + } + + client := &Client{ + httpClient: httpClient, + } + + result, err := client.DocsSearch(context.Background(), "Block Kit", 20) + require.NoError(t, err) + assert.Equal(t, 2, result.TotalResults) + assert.Equal(t, 20, result.Limit) + assert.Len(t, result.Results, 2) + assert.Equal(t, "Block Kit", result.Results[0].Title) + assert.Equal(t, "/block-kit", result.Results[0].URL) +} + +// Test_DocsSearch_EmptyResults verifies handling of empty results +func Test_DocsSearch_EmptyResults(t *testing.T) { + responseBody := DocsSearchResponse{ + TotalResults: 0, + Limit: 20, + Results: []DocsSearchItem{}, + } + + bodyBytes, err := json.Marshal(responseBody) + require.NoError(t, err) + + mockTransport := &mockRoundTripper{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(bodyBytes)), + Header: make(http.Header), + }, + } + + httpClient := &http.Client{ + Transport: mockTransport, + } + + client := &Client{ + httpClient: httpClient, + } + + result, err := client.DocsSearch(context.Background(), "nonexistent", 20) + require.NoError(t, err) + assert.Equal(t, 0, result.TotalResults) + assert.Len(t, result.Results, 0) +} + +// Test_DocsSearch_QueryEncoding verifies query parameters are properly encoded +func Test_DocsSearch_QueryEncoding(t *testing.T) { + responseBody := DocsSearchResponse{ + TotalResults: 0, + Limit: 20, + Results: []DocsSearchItem{}, + } + + bodyBytes, err := json.Marshal(responseBody) + require.NoError(t, err) + + mockTransport := &mockRoundTripper{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewReader(bodyBytes)), + Header: make(http.Header), + }, + } + + httpClient := &http.Client{ + Transport: mockTransport, + } + + client := &Client{ + httpClient: httpClient, + } + + _, err = client.DocsSearch(context.Background(), "messages & webhooks", 5) + require.NoError(t, err) + + // Verify URL encoding + assert.Contains(t, mockTransport.capturedURL, "q=messages+%26+webhooks") + assert.Contains(t, mockTransport.capturedURL, "limit=5") +} + +// Test_DocsSearch_HTTPError verifies HTTP request errors are handled +func Test_DocsSearch_HTTPError(t *testing.T) { + mockTransport := &mockRoundTripper{ + err: fmt.Errorf("network error"), + } + + httpClient := &http.Client{ + Transport: mockTransport, + } + + client := &Client{ + httpClient: httpClient, + } + + result, err := client.DocsSearch(context.Background(), "test", 20) + assert.Error(t, err) + assert.Nil(t, result) +} + +// Test_DocsSearch_NonOKStatus verifies non-200 status codes are handled +func Test_DocsSearch_NonOKStatus(t *testing.T) { + mockTransport := &mockRoundTripper{ + response: &http.Response{ + StatusCode: http.StatusNotFound, + Body: io.NopCloser(bytes.NewBufferString("")), + Header: make(http.Header), + }, + } + + httpClient := &http.Client{ + Transport: mockTransport, + } + + client := &Client{ + httpClient: httpClient, + } + + result, err := client.DocsSearch(context.Background(), "test", 20) + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "404") +} + +// Test_DocsSearch_InvalidJSON verifies invalid JSON responses are handled +func Test_DocsSearch_InvalidJSON(t *testing.T) { + mockTransport := &mockRoundTripper{ + response: &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(bytes.NewBufferString("{invalid json}")), + Header: make(http.Header), + }, + } + + httpClient := &http.Client{ + Transport: mockTransport, + } + + client := &Client{ + httpClient: httpClient, + } + + result, err := client.DocsSearch(context.Background(), "test", 20) + assert.Error(t, err) + assert.Nil(t, result) +} From 5650feb0f95155bb028e5e2cabc9f54ff998e0d9 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Tue, 31 Mar 2026 09:27:30 -0700 Subject: [PATCH 09/24] resty api --- internal/api/docs.go | 2 +- internal/api/docs_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/api/docs.go b/internal/api/docs.go index 66465644..f48d71ca 100644 --- a/internal/api/docs.go +++ b/internal/api/docs.go @@ -47,7 +47,7 @@ func (c *Client) DocsSearch(ctx context.Context, query string, limit int) (*Docs span, _ = opentracing.StartSpanFromContext(ctx, "apiclient.DocsSearch") defer span.Finish() - endpoint := fmt.Sprintf("api/search?q=%s&limit=%d", url.QueryEscape(query), limit) + endpoint := fmt.Sprintf("api/v1/search?query=%s&limit=%d", url.QueryEscape(query), limit) sURL := docsSearchAPIURL + "/" + endpoint span.SetTag("request_url", sURL) diff --git a/internal/api/docs_test.go b/internal/api/docs_test.go index b4706756..258717e8 100644 --- a/internal/api/docs_test.go +++ b/internal/api/docs_test.go @@ -148,7 +148,7 @@ func Test_DocsSearch_QueryEncoding(t *testing.T) { require.NoError(t, err) // Verify URL encoding - assert.Contains(t, mockTransport.capturedURL, "q=messages+%26+webhooks") + assert.Contains(t, mockTransport.capturedURL, "query=messages+%26+webhooks") assert.Contains(t, mockTransport.capturedURL, "limit=5") } From 0baf9486e64f2321c26454f57596d70d5ffce5f8 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 1 Apr 2026 18:04:08 -0700 Subject: [PATCH 10/24] test: replace docs base url for fake client runner --- internal/api/docs.go | 8 +- internal/api/docs_test.go | 271 ++++++++++++-------------------------- 2 files changed, 88 insertions(+), 191 deletions(-) diff --git a/internal/api/docs.go b/internal/api/docs.go index f48d71ca..77d98b3f 100644 --- a/internal/api/docs.go +++ b/internal/api/docs.go @@ -24,7 +24,9 @@ import ( "github.com/opentracing/opentracing-go" ) -const docsSearchAPIURL = "https://docs-slack-d-search-api-duu9zr.herokuapp.com" +var docsBaseURL = "https://docs-slack-d-search-api-duu9zr.herokuapp.com" + +const docsSearchMethod = "api/v1/search" type DocsClient interface { DocsSearch(ctx context.Context, query string, limit int) (*DocsSearchResponse, error) @@ -47,8 +49,8 @@ func (c *Client) DocsSearch(ctx context.Context, query string, limit int) (*Docs span, _ = opentracing.StartSpanFromContext(ctx, "apiclient.DocsSearch") defer span.Finish() - endpoint := fmt.Sprintf("api/v1/search?query=%s&limit=%d", url.QueryEscape(query), limit) - sURL := docsSearchAPIURL + "/" + endpoint + endpoint := fmt.Sprintf("%s?query=%s&limit=%d", docsSearchMethod, url.QueryEscape(query), limit) + sURL := docsBaseURL + "/" + endpoint span.SetTag("request_url", sURL) diff --git a/internal/api/docs_test.go b/internal/api/docs_test.go index 258717e8..b5a6d372 100644 --- a/internal/api/docs_test.go +++ b/internal/api/docs_test.go @@ -15,205 +15,100 @@ package api import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" "testing" - "github.com/stretchr/testify/assert" + "github.com/slackapi/slack-cli/internal/slackcontext" + "github.com/slackapi/slack-cli/internal/slackerror" "github.com/stretchr/testify/require" ) -// mockRoundTripper implements http.RoundTripper for testing -type mockRoundTripper struct { - response *http.Response - err error - capturedURL string -} - -func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - m.capturedURL = req.URL.String() - return m.response, m.err -} - -// Test_DocsSearch_Success verifies successful API response parsing -func Test_DocsSearch_Success(t *testing.T) { - responseBody := DocsSearchResponse{ - TotalResults: 2, - Limit: 20, - Results: []DocsSearchItem{ - { - Title: "Block Kit", - URL: "/block-kit", +func Test_Client_DocsSearch(t *testing.T) { + tests := map[string]struct { + query string + limit int + response string + statusCode int + expectedQuerystring string + expectedResponse *DocsSearchResponse + expectedErrorContains string + }{ + "returns search results": { + query: "test", + limit: 20, + response: `{"total_results":2,"limit":20,"results":[{"title":"Block Kit","url":"/block-kit"},{"title":"Block Kit Elements","url":"/block-kit/elements"}]}`, + expectedResponse: &DocsSearchResponse{ + TotalResults: 2, + Limit: 20, + Results: []DocsSearchItem{ + { + Title: "Block Kit", + URL: "/block-kit", + }, + { + Title: "Block Kit Elements", + URL: "/block-kit/elements", + }, + }, }, - { - Title: "Block Kit Elements", - URL: "/block-kit/elements", - }, - }, - } - - bodyBytes, err := json.Marshal(responseBody) - require.NoError(t, err) - - mockTransport := &mockRoundTripper{ - response: &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader(bodyBytes)), - Header: make(http.Header), }, - } - - httpClient := &http.Client{ - Transport: mockTransport, - } - - client := &Client{ - httpClient: httpClient, - } - - result, err := client.DocsSearch(context.Background(), "Block Kit", 20) - require.NoError(t, err) - assert.Equal(t, 2, result.TotalResults) - assert.Equal(t, 20, result.Limit) - assert.Len(t, result.Results, 2) - assert.Equal(t, "Block Kit", result.Results[0].Title) - assert.Equal(t, "/block-kit", result.Results[0].URL) -} - -// Test_DocsSearch_EmptyResults verifies handling of empty results -func Test_DocsSearch_EmptyResults(t *testing.T) { - responseBody := DocsSearchResponse{ - TotalResults: 0, - Limit: 20, - Results: []DocsSearchItem{}, - } - - bodyBytes, err := json.Marshal(responseBody) - require.NoError(t, err) - - mockTransport := &mockRoundTripper{ - response: &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader(bodyBytes)), - Header: make(http.Header), + "returns empty results": { + query: "nonexistent", + limit: 20, + response: `{"total_results":0,"limit":20,"results":[]}`, + expectedResponse: &DocsSearchResponse{ + TotalResults: 0, + Limit: 20, + Results: []DocsSearchItem{}, + }, }, - } - - httpClient := &http.Client{ - Transport: mockTransport, - } - - client := &Client{ - httpClient: httpClient, - } - - result, err := client.DocsSearch(context.Background(), "nonexistent", 20) - require.NoError(t, err) - assert.Equal(t, 0, result.TotalResults) - assert.Len(t, result.Results, 0) -} - -// Test_DocsSearch_QueryEncoding verifies query parameters are properly encoded -func Test_DocsSearch_QueryEncoding(t *testing.T) { - responseBody := DocsSearchResponse{ - TotalResults: 0, - Limit: 20, - Results: []DocsSearchItem{}, - } - - bodyBytes, err := json.Marshal(responseBody) - require.NoError(t, err) - - mockTransport := &mockRoundTripper{ - response: &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewReader(bodyBytes)), - Header: make(http.Header), + "encodes query parameters": { + query: "messages & webhooks", + limit: 5, + response: `{"total_results":0,"limit":5,"results":[]}`, + expectedQuerystring: "query=messages+%26+webhooks&limit=5", + expectedResponse: &DocsSearchResponse{ + TotalResults: 0, + Limit: 5, + Results: []DocsSearchItem{}, + }, }, - } - - httpClient := &http.Client{ - Transport: mockTransport, - } - - client := &Client{ - httpClient: httpClient, - } - - _, err = client.DocsSearch(context.Background(), "messages & webhooks", 5) - require.NoError(t, err) - - // Verify URL encoding - assert.Contains(t, mockTransport.capturedURL, "query=messages+%26+webhooks") - assert.Contains(t, mockTransport.capturedURL, "limit=5") -} - -// Test_DocsSearch_HTTPError verifies HTTP request errors are handled -func Test_DocsSearch_HTTPError(t *testing.T) { - mockTransport := &mockRoundTripper{ - err: fmt.Errorf("network error"), - } - - httpClient := &http.Client{ - Transport: mockTransport, - } - - client := &Client{ - httpClient: httpClient, - } - - result, err := client.DocsSearch(context.Background(), "test", 20) - assert.Error(t, err) - assert.Nil(t, result) -} - -// Test_DocsSearch_NonOKStatus verifies non-200 status codes are handled -func Test_DocsSearch_NonOKStatus(t *testing.T) { - mockTransport := &mockRoundTripper{ - response: &http.Response{ - StatusCode: http.StatusNotFound, - Body: io.NopCloser(bytes.NewBufferString("")), - Header: make(http.Header), + "returns error for non-OK status": { + query: "test", + limit: 20, + statusCode: 404, + expectedErrorContains: slackerror.ErrHTTPRequestFailed, }, - } - - httpClient := &http.Client{ - Transport: mockTransport, - } - - client := &Client{ - httpClient: httpClient, - } - - result, err := client.DocsSearch(context.Background(), "test", 20) - assert.Error(t, err) - assert.Nil(t, result) - assert.Contains(t, err.Error(), "404") -} - -// Test_DocsSearch_InvalidJSON verifies invalid JSON responses are handled -func Test_DocsSearch_InvalidJSON(t *testing.T) { - mockTransport := &mockRoundTripper{ - response: &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(bytes.NewBufferString("{invalid json}")), - Header: make(http.Header), + "returns error for invalid JSON": { + query: "test", + limit: 20, + response: `{invalid json}`, + expectedErrorContains: slackerror.ErrHTTPResponseInvalid, }, } - - httpClient := &http.Client{ - Transport: mockTransport, + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctx := slackcontext.MockContext(t.Context()) + c, teardown := NewFakeClient(t, FakeClientParams{ + ExpectedMethod: docsSearchMethod, + ExpectedQuerystring: tc.expectedQuerystring, + Response: tc.response, + StatusCode: tc.statusCode, + }) + defer teardown() + + originalURL := docsBaseURL + docsBaseURL = c.Host() + defer func() { docsBaseURL = originalURL }() + + result, err := c.DocsSearch(ctx, tc.query, tc.limit) + + if tc.expectedErrorContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErrorContains) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedResponse, result) + } + }) } - - client := &Client{ - httpClient: httpClient, - } - - result, err := client.DocsSearch(context.Background(), "test", 20) - assert.Error(t, err) - assert.Nil(t, result) } From 6c6875ec028e86becedeb631f1694f282b45bc37 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 1 Apr 2026 18:29:45 -0700 Subject: [PATCH 11/24] feat: output request and response details in verbose --- internal/api/docs.go | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/internal/api/docs.go b/internal/api/docs.go index 77d98b3f..cd2ae18d 100644 --- a/internal/api/docs.go +++ b/internal/api/docs.go @@ -20,8 +20,10 @@ import ( "fmt" "net/http" "net/url" + "runtime" "github.com/opentracing/opentracing-go" + "github.com/slackapi/slack-cli/internal/slackcontext" ) var docsBaseURL = "https://docs-slack-d-search-api-duu9zr.herokuapp.com" @@ -50,27 +52,33 @@ func (c *Client) DocsSearch(ctx context.Context, query string, limit int) (*Docs defer span.Finish() endpoint := fmt.Sprintf("%s?query=%s&limit=%d", docsSearchMethod, url.QueryEscape(query), limit) - sURL := docsBaseURL + "/" + endpoint + sURL, err := url.Parse(docsBaseURL + "/" + endpoint) + if err != nil { + return nil, errHTTPRequestFailed.WithRootCause(err) + } span.SetTag("request_url", sURL) - req, err := http.NewRequestWithContext(ctx, "GET", sURL, nil) + req, err := http.NewRequestWithContext(ctx, "GET", sURL.String(), nil) if err != nil { return nil, errHTTPRequestFailed.WithRootCause(err) } - resp, err := c.httpClient.Do(req) + cliVersion, err := slackcontext.Version(ctx) if err != nil { - return nil, errHTTPRequestFailed.WithRootCause(err) + return nil, err } - defer resp.Body.Close() + req.Header.Add("User-Agent", fmt.Sprintf("slack-cli/%s (os: %s)", cliVersion, runtime.GOOS)) - if resp.StatusCode != http.StatusOK { - return nil, errHTTPRequestFailed.WithMessage("API returned status %d", resp.StatusCode) + c.printRequest(ctx, req, false) + + respBytes, err := c.DoWithRetry(ctx, req, span, false, sURL) + if err != nil { + return nil, errHTTPRequestFailed.WithRootCause(err) } var searchResponse DocsSearchResponse - if err := json.NewDecoder(resp.Body).Decode(&searchResponse); err != nil { + if err := json.Unmarshal(respBytes, &searchResponse); err != nil { return nil, errHTTPResponseInvalid.WithRootCause(err) } From 3317e1171b8340d637600f788823f4754deae2d1 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 1 Apr 2026 18:44:22 -0700 Subject: [PATCH 12/24] test: confirm query string keeps capitals --- internal/api/docs_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/api/docs_test.go b/internal/api/docs_test.go index b5a6d372..7f19e716 100644 --- a/internal/api/docs_test.go +++ b/internal/api/docs_test.go @@ -33,9 +33,10 @@ func Test_Client_DocsSearch(t *testing.T) { expectedErrorContains string }{ "returns search results": { - query: "test", - limit: 20, - response: `{"total_results":2,"limit":20,"results":[{"title":"Block Kit","url":"/block-kit"},{"title":"Block Kit Elements","url":"/block-kit/elements"}]}`, + query: "Block Kit", + limit: 20, + response: `{"total_results":2,"limit":20,"results":[{"title":"Block Kit","url":"/block-kit"},{"title":"Block Kit Elements","url":"/block-kit/elements"}]}`, + expectedQuerystring: "query=Block+Kit&limit=20", expectedResponse: &DocsSearchResponse{ TotalResults: 2, Limit: 20, From d815e69c39fc580f72452a632d2f37cb22add054 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Wed, 1 Apr 2026 18:44:43 -0700 Subject: [PATCH 13/24] test: command test cases in a single table --- cmd/docs/search_test.go | 313 +++++++++++++--------------------------- 1 file changed, 102 insertions(+), 211 deletions(-) diff --git a/cmd/docs/search_test.go b/cmd/docs/search_test.go index 5736aa68..9537cbd8 100644 --- a/cmd/docs/search_test.go +++ b/cmd/docs/search_test.go @@ -20,189 +20,114 @@ import ( "github.com/slackapi/slack-cli/internal/api" "github.com/slackapi/slack-cli/internal/shared" - "github.com/slackapi/slack-cli/internal/slackcontext" "github.com/slackapi/slack-cli/internal/slackerror" + "github.com/slackapi/slack-cli/internal/slacktrace" "github.com/slackapi/slack-cli/test/testutil" "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/mock" ) -// mockDocsAPI implements api.DocsClient for testing -type mockDocsAPI struct { - searchResponse *api.DocsSearchResponse - searchError error -} - -func (m *mockDocsAPI) DocsSearch(ctx context.Context, query string, limit int) (*api.DocsSearchResponse, error) { - return m.searchResponse, m.searchError -} - -func setupDocsAPITest(t *testing.T, response *api.DocsSearchResponse, err error) *shared.ClientFactory { - clientsMock := shared.NewClientsMock() - clientsMock.AddDefaultMocks() - clients := shared.NewClientFactory(clientsMock.MockClientFactory()) - - mockDocsAPI := &mockDocsAPI{ - searchResponse: response, - searchError: err, - } - - // Override the API to return our mock for DocsSearch - originalAPI := clients.API - clients.API = func() api.APIInterface { - realAPI := originalAPI() - // Return a wrapper that intercepts DocsSearch calls - return &docsAPIWrapper{ - APIInterface: realAPI, - mock: mockDocsAPI, - } - } - - return clients -} - -// docsAPIWrapper wraps APIInterface to mock DocsSearch while delegating other methods -type docsAPIWrapper struct { - api.APIInterface - mock *mockDocsAPI -} - -func (w *docsAPIWrapper) DocsSearch(ctx context.Context, query string, limit int) (*api.DocsSearchResponse, error) { - return w.mock.DocsSearch(ctx, query, limit) -} - -// Text and JSON Output Tests - -// Verifies that HTTP request errors are properly caught and returned as errors. -func Test_Docs_SearchCommand_TextJSONOutput_HTTPError(t *testing.T) { - ctx := slackcontext.MockContext(context.Background()) - clients := setupDocsAPITest(t, nil, slackerror.New(slackerror.ErrHTTPRequestFailed)) - - err := fetchAndOutputSearchResults(ctx, clients, "test", 20) - assert.Error(t, err) -} - -// Verifies that HTTP errors from the API are properly caught and returned as errors. -func Test_Docs_SearchCommand_TextJSONOutput_APIError(t *testing.T) { - clients := setupDocsAPITest(t, nil, slackerror.New(slackerror.ErrHTTPRequestFailed)) - err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "nonexistent", 20) - assert.Error(t, err) -} - -// Verifies that malformed JSON responses are caught during parsing and returned as errors. -func Test_Docs_SearchCommand_TextJSONOutput_InvalidJSON(t *testing.T) { - clients := setupDocsAPITest(t, nil, slackerror.New(slackerror.ErrHTTPResponseInvalid)) - err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "test", 20) - assert.Error(t, err) -} - -// Verifies that valid JSON responses with no results are correctly parsed and output without errors. -func Test_Docs_SearchCommand_TextJSONOutput_EmptyResults(t *testing.T) { - response := &api.DocsSearchResponse{ - TotalResults: 0, - Results: []api.DocsSearchItem{}, - Limit: 20, - } - - clients := setupDocsAPITest(t, response, nil) - err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "nonexistent query", 20) - require.NoError(t, err) -} - -// Verifies that various query formats are properly URL encoded and API parameters are correctly passed. -func Test_Docs_SearchCommand_TextJSONOutput_QueryFormats(t *testing.T) { - response := &api.DocsSearchResponse{ - TotalResults: 2, - Limit: 20, - Results: []api.DocsSearchItem{ - { - Title: "Block Kit", - URL: "/block-kit", +func Test_Docs_SearchCommand(t *testing.T) { + testutil.TableTestCommand(t, testutil.CommandTests{ + "returns text results": { + CmdArgs: []string{"search", "Block Kit"}, + Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { + cm.API.On("DocsSearch", mock.Anything, "Block Kit", 20).Return(&api.DocsSearchResponse{ + TotalResults: 2, + Limit: 20, + Results: []api.DocsSearchItem{ + {Title: "Block Kit", URL: "/block-kit"}, + {Title: "Block Kit Elements", URL: "/block-kit/elements"}, + }, + }, nil) }, - { - Title: "Block Kit Elements", - URL: "/block-kit/elements", + ExpectedStdoutOutputs: []string{ + "Block Kit", + "https://docs.slack.dev/block-kit", + }, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) }, }, - } - - tests := map[string]struct { - query string - limit int - }{ - "single word query": { - query: "messaging", - limit: 20, - }, - "multiple words": { - query: "socket mode", - limit: 20, + "returns JSON results": { + CmdArgs: []string{"search", "Block Kit", "--output=json"}, + Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { + cm.API.On("DocsSearch", mock.Anything, "Block Kit", 20).Return(&api.DocsSearchResponse{ + TotalResults: 2, + Limit: 20, + Results: []api.DocsSearchItem{ + {Title: "Block Kit", URL: "/block-kit"}, + {Title: "Block Kit Elements", URL: "/block-kit/elements"}, + }, + }, nil) + }, + ExpectedStdoutOutputs: []string{ + `{ + "total_results": 2, + "results": [ + { + "url": "https://docs.slack.dev/block-kit", + "title": "Block Kit" + }, + { + "url": "https://docs.slack.dev/block-kit/elements", + "title": "Block Kit Elements" + } + ], + "limit": 20 +} +`, + }, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) + }, }, - "special characters": { - query: "messages & webhooks", - limit: 20, + "returns empty results": { + CmdArgs: []string{"search", "nonexistent"}, + Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { + cm.API.On("DocsSearch", mock.Anything, "nonexistent", 20).Return(&api.DocsSearchResponse{ + TotalResults: 0, + Results: []api.DocsSearchItem{}, + Limit: 20, + }, nil) + }, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) + }, }, - "custom limit": { - query: "Block Kit", - limit: 5, + "returns error on API failure": { + CmdArgs: []string{"search", "test"}, + Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { + cm.API.On("DocsSearch", mock.Anything, "test", 20).Return(nil, slackerror.New(slackerror.ErrHTTPRequestFailed)) + }, + ExpectedErrorStrings: []string{slackerror.ErrHTTPRequestFailed}, }, - } - - for name, tc := range tests { - t.Run(name, func(t *testing.T) { - clients := setupDocsAPITest(t, response, nil) - err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, tc.query, tc.limit) - require.NoError(t, err) - }) - } -} - -// JSON Output Tests - -// Verifies that JSON output mode correctly formats and outputs search results. -func Test_Docs_SearchCommand_JSONOutput(t *testing.T) { - response := &api.DocsSearchResponse{ - TotalResults: 1, - Limit: 20, - Results: []api.DocsSearchItem{ - { - Title: "Block Kit", - URL: "/block-kit", + "passes custom limit": { + CmdArgs: []string{"search", "test", "--limit=5"}, + Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { + cm.API.On("DocsSearch", mock.Anything, "test", 5).Return(&api.DocsSearchResponse{ + TotalResults: 0, + Results: []api.DocsSearchItem{}, + Limit: 5, + }, nil) + }, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + cm.API.AssertCalled(t, "DocsSearch", mock.Anything, "test", 5) }, }, - } - - clients := setupDocsAPITest(t, response, nil) - err := fetchAndOutputSearchResults(slackcontext.MockContext(context.Background()), clients, "Block Kit", 20) - require.NoError(t, err) -} - -// Text Output Tests - -// Verifies that text output mode correctly formats and outputs search results. -func Test_Docs_SearchCommand_TextOutput(t *testing.T) { - response := &api.DocsSearchResponse{ - TotalResults: 1, - Limit: 20, - Results: []api.DocsSearchItem{ - { - Title: "Block Kit", - URL: "/block-kit", + "joins multiple arguments into query": { + CmdArgs: []string{"search", "Block", "Kit", "Element"}, + Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { + cm.API.On("DocsSearch", mock.Anything, "Block Kit Element", 20).Return(&api.DocsSearchResponse{ + TotalResults: 0, + Results: []api.DocsSearchItem{}, + Limit: 20, + }, nil) + }, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + cm.API.AssertCalled(t, "DocsSearch", mock.Anything, "Block Kit Element", 20) }, }, - } - - clients := setupDocsAPITest(t, response, nil) - err := fetchAndOutputTextResults(slackcontext.MockContext(context.Background()), clients, "Block Kit", 20) - require.NoError(t, err) -} - -// Invalid Output Format Tests - -// Verifies that invalid output format returns an error with helpful remediation. -func Test_Docs_SearchCommand_InvalidOutputFormat(t *testing.T) { - testutil.TableTestCommand(t, testutil.CommandTests{ "rejects invalid output format": { CmdArgs: []string{"search", "test", "--output=invalid"}, ExpectedErrorStrings: []string{ @@ -210,69 +135,35 @@ func Test_Docs_SearchCommand_InvalidOutputFormat(t *testing.T) { "Use one of: text, json, browser", }, }, - }, func(cf *shared.ClientFactory) *cobra.Command { - return NewCommand(cf) - }) -} - -// Browser Output Tests - -// Verifies that browser output mode correctly handles various query formats and opens the correct URLs. -func Test_Docs_SearchCommand_BrowserOutput(t *testing.T) { - testutil.TableTestCommand(t, testutil.CommandTests{ - "opens browser with search query using space syntax": { + "opens browser with search query": { CmdArgs: []string{"search", "messaging", "--output=browser"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - expectedURL := "https://docs.slack.dev/search/?q=messaging" - cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + cm.Browser.AssertCalled(t, "OpenURL", "https://docs.slack.dev/search/?q=messaging") + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) }, ExpectedOutputs: []string{ "Docs Search", "https://docs.slack.dev/search/?q=messaging", }, }, - "handles search with multiple arguments": { - CmdArgs: []string{"search", "Block", "Kit", "Element", "--output=browser"}, - ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - expectedURL := "https://docs.slack.dev/search/?q=Block+Kit+Element" - cm.Browser.AssertCalled(t, "OpenURL", expectedURL) - }, - ExpectedOutputs: []string{ - "Docs Search", - "https://docs.slack.dev/search/?q=Block+Kit+Element", - }, - }, - "handles search query with multiple words": { - CmdArgs: []string{"search", "socket mode", "--output=browser"}, - ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - expectedURL := "https://docs.slack.dev/search/?q=socket+mode" - cm.Browser.AssertCalled(t, "OpenURL", expectedURL) - }, - ExpectedOutputs: []string{ - "Docs Search", - "https://docs.slack.dev/search/?q=socket+mode", - }, - }, - "handles special characters in search query": { + "opens browser with special characters": { CmdArgs: []string{"search", "messages & webhooks", "--output=browser"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - expectedURL := "https://docs.slack.dev/search/?q=messages+%26+webhooks" - cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + cm.Browser.AssertCalled(t, "OpenURL", "https://docs.slack.dev/search/?q=messages+%26+webhooks") }, ExpectedOutputs: []string{ "Docs Search", "https://docs.slack.dev/search/?q=messages+%26+webhooks", }, }, - "handles search query with quotes": { - CmdArgs: []string{"search", "webhook \"send message\"", "--output=browser"}, + "opens browser with multiple arguments": { + CmdArgs: []string{"search", "Block", "Kit", "Element", "--output=browser"}, ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { - expectedURL := "https://docs.slack.dev/search/?q=webhook+%22send+message%22" - cm.Browser.AssertCalled(t, "OpenURL", expectedURL) + cm.Browser.AssertCalled(t, "OpenURL", "https://docs.slack.dev/search/?q=Block+Kit+Element") }, ExpectedOutputs: []string{ "Docs Search", - "https://docs.slack.dev/search/?q=webhook+%22send+message%22", + "https://docs.slack.dev/search/?q=Block+Kit+Element", }, }, }, func(cf *shared.ClientFactory) *cobra.Command { From 1bb09490ee86b5f7e80aac3f88450fb641b3984d Mon Sep 17 00:00:00 2001 From: Luke Russell <31357343+lukegalbraithrussell@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:56:33 -0700 Subject: [PATCH 14/24] Apply suggestions from code review Co-authored-by: Eden Zimbelman --- cmd/docs/search.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/cmd/docs/search.go b/cmd/docs/search.go index f6944c31..d66cb119 100644 --- a/cmd/docs/search.go +++ b/cmd/docs/search.go @@ -43,9 +43,12 @@ func NewSearchCommand(clients *shared.ClientFactory) *cobra.Command { cfg := &searchConfig{} cmd := &cobra.Command{ - Use: "search ", + Use: "search [query]", Short: "Search Slack developer docs", - Long: "Search the Slack developer docs and return results in text, JSON, or browser format", + Long: strings.Join([]string{ + "Search the Slack developer docs and return results in text, JSON, or browser", + "format.", + }, "\n"), Example: style.ExampleCommandsf([]style.ExampleCommand{ { Meaning: "Search docs and return text results", @@ -67,7 +70,7 @@ func NewSearchCommand(clients *shared.ClientFactory) *cobra.Command { } cmd.Flags().StringVar(&cfg.output, "output", "text", "output format: text, json, browser") - cmd.Flags().IntVar(&cfg.limit, "limit", 20, "maximum number of search results to return (only applies with --output=json and --output=text)") + cmd.Flags().IntVar(&cfg.limit, "limit", 20, "maximum number of text or json search results to return") return cmd } @@ -135,7 +138,11 @@ func fetchAndOutputTextResults(ctx context.Context, clients *shared.ClientFactor for _, result := range searchResponse.Results { absoluteURL := makeAbsoluteURL(result.URL) - fmt.Fprintf(clients.IO.WriteOut(), "%s\n%s\n\n", result.Title, absoluteURL) + clients.IO.PrintInfo(ctx, false, style.Sectionf(style.TextSection{ + Emoji: "books", + Text: result.Title, + Secondary: []string{absoluteURL}, + })) } clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, query) From 2fdee9bbf3e0298d35f0ada2289d2b7a5e13d677 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 2 Apr 2026 09:25:45 -0700 Subject: [PATCH 15/24] feedback --- cmd/docs/docs.go | 10 +++-- cmd/docs/search.go | 101 +++++++++++++++++++++++++-------------------- 2 files changed, 62 insertions(+), 49 deletions(-) diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index ac11cc6c..3834731f 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -77,12 +77,14 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st var finalURL string var sectionText string - // Validate: if there are arguments, --search flag must be used + // Validate: if there are arguments, search subcommand must be used if len(args) > 0 && !cmd.Flags().Changed("search") { query := strings.Join(args, " ") - return slackerror.New(slackerror.ErrDocsSearchFlagRequired).WithRemediation( - "Use --search flag: %s", - style.Commandf(fmt.Sprintf("docs --search \"%s\"", query), false), + return slackerror.New(slackerror.ErrDocsSearchFlagRequired).WithMessage( + "Did you mean to search?", + ).WithRemediation( + "Use search subcommand: %s", + style.Commandf(fmt.Sprintf("docs search \"%s\"", query), false), ) } diff --git a/cmd/docs/search.go b/cmd/docs/search.go index d66cb119..c4c9a2c7 100644 --- a/cmd/docs/search.go +++ b/cmd/docs/search.go @@ -15,7 +15,6 @@ package docs import ( - "context" "encoding/json" "fmt" "strings" @@ -45,7 +44,7 @@ func NewSearchCommand(clients *shared.ClientFactory) *cobra.Command { cmd := &cobra.Command{ Use: "search [query]", Short: "Search Slack developer docs", - Long: strings.Join([]string{ + Long: strings.Join([]string{ "Search the Slack developer docs and return results in text, JSON, or browser", "format.", }, "\n"), @@ -82,9 +81,62 @@ func runDocsSearchCommand(clients *shared.ClientFactory, cmd *cobra.Command, arg switch cfg.output { case "json": - return fetchAndOutputSearchResults(ctx, clients, query, cfg.limit) + searchResponse, err := clients.API().DocsSearch(ctx, query, cfg.limit) + if err != nil { + return err + } + + for i := range searchResponse.Results { + searchResponse.Results[i].URL = makeAbsoluteURL(searchResponse.Results[i].URL) + } + + encoder := json.NewEncoder(clients.IO.WriteOut()) + encoder.SetIndent("", " ") + if err := encoder.Encode(searchResponse); err != nil { + return slackerror.New(slackerror.ErrUnableToParseJSON).WithRootCause(err) + } + + clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, query) + + return nil case "text": - return fetchAndOutputTextResults(ctx, clients, query, cfg.limit) + searchResponse, err := clients.API().DocsSearch(ctx, query, cfg.limit) + if err != nil { + return err + } + + if len(searchResponse.Results) == 0 { + clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ + Emoji: "books", + Text: fmt.Sprintf("Docs Search: %s", query), + Secondary: []string{ + fmt.Sprintf("Found zero results"), + }, + })) + clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, query) + return nil + } + + clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ + Emoji: "books", + Text: fmt.Sprintf("Docs Search: %s", query), + Secondary: []string{ + fmt.Sprintf("Found %d result%s. Displaying first %d", searchResponse.TotalResults, style.Pluralize("", "s", searchResponse.TotalResults), len(searchResponse.Results)), + }, + })) + + for _, result := range searchResponse.Results { + absoluteURL := makeAbsoluteURL(result.URL) + clients.IO.PrintInfo(ctx, false, "%s", style.Sectionf(style.TextSection{ + Emoji: "book", + Text: result.Title, + Secondary: []string{absoluteURL}, + })) + } + + clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, query) + + return nil case "browser": docsSearchURL := buildDocsSearchURL(query) @@ -108,44 +160,3 @@ func runDocsSearchCommand(clients *shared.ClientFactory, cmd *cobra.Command, arg ) } } - -func fetchAndOutputSearchResults(ctx context.Context, clients *shared.ClientFactory, query string, limit int) error { - searchResponse, err := clients.API().DocsSearch(ctx, query, limit) - if err != nil { - return err - } - - for i := range searchResponse.Results { - searchResponse.Results[i].URL = makeAbsoluteURL(searchResponse.Results[i].URL) - } - - encoder := json.NewEncoder(clients.IO.WriteOut()) - encoder.SetIndent("", " ") - if err := encoder.Encode(searchResponse); err != nil { - return slackerror.New(slackerror.ErrUnableToParseJSON).WithRootCause(err) - } - - clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, query) - - return nil -} - -func fetchAndOutputTextResults(ctx context.Context, clients *shared.ClientFactory, query string, limit int) error { - searchResponse, err := clients.API().DocsSearch(ctx, query, limit) - if err != nil { - return err - } - - for _, result := range searchResponse.Results { - absoluteURL := makeAbsoluteURL(result.URL) - clients.IO.PrintInfo(ctx, false, style.Sectionf(style.TextSection{ - Emoji: "books", - Text: result.Title, - Secondary: []string{absoluteURL}, - })) - } - - clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, query) - - return nil -} From 8fd1999fd39390fdbde67b3f60c9229efa90fc64 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 2 Apr 2026 09:40:50 -0700 Subject: [PATCH 16/24] go --- cmd/docs/search.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/docs/search.go b/cmd/docs/search.go index c4c9a2c7..4af76d39 100644 --- a/cmd/docs/search.go +++ b/cmd/docs/search.go @@ -110,7 +110,7 @@ func runDocsSearchCommand(clients *shared.ClientFactory, cmd *cobra.Command, arg Emoji: "books", Text: fmt.Sprintf("Docs Search: %s", query), Secondary: []string{ - fmt.Sprintf("Found zero results"), + "Found zero results", }, })) clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, query) @@ -119,9 +119,9 @@ func runDocsSearchCommand(clients *shared.ClientFactory, cmd *cobra.Command, arg clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ Emoji: "books", - Text: fmt.Sprintf("Docs Search: %s", query), + Text: "Docs Search", Secondary: []string{ - fmt.Sprintf("Found %d result%s. Displaying first %d", searchResponse.TotalResults, style.Pluralize("", "s", searchResponse.TotalResults), len(searchResponse.Results)), + fmt.Sprintf("Displaying first %d of %d results for \"%s\"", len(searchResponse.Results), searchResponse.TotalResults, query), }, })) From 962d75dbdc19e0d5af36665549f9f8051c9ed454 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 2 Apr 2026 09:42:49 -0700 Subject: [PATCH 17/24] go --- cmd/docs/search.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/docs/search.go b/cmd/docs/search.go index 4af76d39..17b68d3e 100644 --- a/cmd/docs/search.go +++ b/cmd/docs/search.go @@ -108,9 +108,9 @@ func runDocsSearchCommand(clients *shared.ClientFactory, cmd *cobra.Command, arg if len(searchResponse.Results) == 0 { clients.IO.PrintInfo(ctx, false, "\n%s", style.Sectionf(style.TextSection{ Emoji: "books", - Text: fmt.Sprintf("Docs Search: %s", query), + Text: "Docs Search", Secondary: []string{ - "Found zero results", + fmt.Sprintf("Found zero results for \"%s\"", query), }, })) clients.IO.PrintTrace(ctx, slacktrace.DocsSearchSuccess, query) From fb1c29d5e1995379bb0e72acefbdd3e71fd192f4 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Thu, 2 Apr 2026 09:53:23 -0700 Subject: [PATCH 18/24] tests --- cmd/docs/docs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index 3834731f..5f1972d3 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -81,7 +81,7 @@ func runDocsCommand(clients *shared.ClientFactory, cmd *cobra.Command, args []st if len(args) > 0 && !cmd.Flags().Changed("search") { query := strings.Join(args, " ") return slackerror.New(slackerror.ErrDocsSearchFlagRequired).WithMessage( - "Did you mean to search?", + "Invalid docs command. Did you mean to search?", ).WithRemediation( "Use search subcommand: %s", style.Commandf(fmt.Sprintf("docs search \"%s\"", query), false), From 086e69e5fce260aeb851f657f3cb847a8656099b Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 3 Apr 2026 08:56:31 -0700 Subject: [PATCH 19/24] real docs URL nowgit add . --- internal/api/docs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/docs.go b/internal/api/docs.go index cd2ae18d..c4fea45a 100644 --- a/internal/api/docs.go +++ b/internal/api/docs.go @@ -26,7 +26,7 @@ import ( "github.com/slackapi/slack-cli/internal/slackcontext" ) -var docsBaseURL = "https://docs-slack-d-search-api-duu9zr.herokuapp.com" +var docsBaseURL = "https://docs.slack.dev" const docsSearchMethod = "api/v1/search" From b1216f96a4da934eaf110c5fea12f4a674eed718 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 3 Apr 2026 11:49:18 -0700 Subject: [PATCH 20/24] moves search stuff into search per request --- cmd/docs/docs.go | 6 ------ cmd/docs/search.go | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/docs/docs.go b/cmd/docs/docs.go index 5f1972d3..31ab091f 100644 --- a/cmd/docs/docs.go +++ b/cmd/docs/docs.go @@ -16,7 +16,6 @@ package docs import ( "fmt" - "net/url" "strings" "github.com/slackapi/slack-cli/internal/shared" @@ -30,11 +29,6 @@ const docsURL = "https://docs.slack.dev" var searchMode bool -func buildDocsSearchURL(query string) string { - encodedQuery := url.QueryEscape(query) - return fmt.Sprintf("%s/search/?q=%s", docsURL, encodedQuery) -} - func NewCommand(clients *shared.ClientFactory) *cobra.Command { cmd := &cobra.Command{ Use: "docs", diff --git a/cmd/docs/search.go b/cmd/docs/search.go index 17b68d3e..52672d92 100644 --- a/cmd/docs/search.go +++ b/cmd/docs/search.go @@ -17,6 +17,7 @@ package docs import ( "encoding/json" "fmt" + "net/url" "strings" "github.com/slackapi/slack-cli/internal/shared" @@ -26,6 +27,11 @@ import ( "github.com/spf13/cobra" ) +func buildDocsSearchURL(query string) string { + encodedQuery := url.QueryEscape(query) + return fmt.Sprintf("%s/search/?q=%s", docsURL, encodedQuery) +} + type searchConfig struct { output string limit int From e26eab41b7ca52d972ada99371098b1724371ad0 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 3 Apr 2026 11:58:54 -0700 Subject: [PATCH 21/24] error --- docs/errors.md | 1692 +++++++++++++++++++++++++++++++++ internal/slackerror/errors.go | 2 +- 2 files changed, 1693 insertions(+), 1 deletion(-) create mode 100644 docs/errors.md diff --git a/docs/errors.md b/docs/errors.md new file mode 100644 index 00000000..efb51c2e --- /dev/null +++ b/docs/errors.md @@ -0,0 +1,1692 @@ +# Slack CLI errors reference + +Troubleshooting errors can be tricky between your development environment, the +Slack CLI, and those encountered when running your code. Below are some common +ones, as well as a list of the errors the Slack CLI may raise, what they mean, +and some ways to remediate them. + +## Slack CLI errors list + +### access_denied {#access_denied} + +**Message**: You don't have the permission to access the specified resource + +**Remediation**: Check with your Slack admin to make sure that you have permission to access the resource. + +--- + +### add_app_to_project_error {#add_app_to_project_error} + +**Message**: Couldn't save your app's info to this project + +--- + +### already_logged_out {#already_logged_out} + +**Message**: You're already logged out + +--- + +### already_resolved {#already_resolved} + +**Message**: The app already has a resolution and cannot be requested + +--- + +### app_add_error {#app_add_error} + +**Message**: Couldn't create a new app + +--- + +### app_add_exists {#app_add_exists} + +**Message**: App already exists belonging to the team + +--- + +### app_approval_request_denied {#app_approval_request_denied} + +**Message**: This app is currently denied for installation + +**Remediation**: Reach out to an admin for additional information, or try requesting again with different scopes and outgoing domains + +--- + +### app_approval_request_eligible {#app_approval_request_eligible} + +**Message**: This app requires permissions that must be reviewed by an admin before you can install it + +--- + +### app_approval_request_pending {#app_approval_request_pending} + +**Message**: This app has requested admin approval to install and is awaiting review + +**Remediation**: Reach out to an admin for additional information + +--- + +### app_auth_team_mismatch {#app_auth_team_mismatch} + +**Message**: Specified app and team are mismatched + +**Remediation**: Try a different combination of `--app` and `--team` flags + +--- + +### app_create_error {#app_create_error} + +**Message**: Couldn't create your app + +--- + +### app_delete_error {#app_delete_error} + +**Message**: Couldn't delete your app + +--- + +### app_deploy_error {#app_deploy_error} + +**Message**: Couldn't deploy your app + +--- + +### app_deploy_function_runtime_not_slack {#app_deploy_function_runtime_not_slack} + +**Message**: Deployment to Slack is not currently supported for apps with `runOnSlack` set as false + +**Remediation**: Learn about building apps with the Deno Slack SDK: + +https://docs.slack.dev/tools/deno-slack-sdk + +If you are using a Bolt framework, add a deploy hook then run: `slack-cli deploy` + +Otherwise start your app for local development with: `slack-cli run` + +--- + +### app_dir_only_fail {#app_dir_only_fail} + +**Message**: The app was neither in the app directory nor created on this team/org, and cannot be requested + +--- + +### app_directory_access_error {#app_directory_access_error} + +**Message**: Couldn't access app directory + +--- + +### app_flag_required {#app_flag_required} + +**Message**: The --app flag must be provided + +**Remediation**: Choose a specific app with `--app ` + +--- + +### app_found {#app_found} + +**Message**: An app was found + +--- + +### app_hosted {#app_hosted} + +**Message**: App is configured for Run on Slack infrastructure + +--- + +### app_install_error {#app_install_error} + +**Message**: Couldn't install your app to a workspace + +--- + +### app_manifest_access_error {#app_manifest_access_error} + +**Message**: Couldn't access your app manifest + +--- + +### app_manifest_create_error {#app_manifest_create_error} + +**Message**: Couldn't create your app manifest + +--- + +### app_manifest_generate_error {#app_manifest_generate_error} + +**Message**: Couldn't generate an app manifest from this project + +**Remediation**: Check to make sure you are in a valid Slack project directory and that your project has no compilation errors. + +--- + +### app_manifest_update_error {#app_manifest_update_error} + +**Message**: The app manifest was not updated + +--- + +### app_manifest_validate_error {#app_manifest_validate_error} + +**Message**: Your app manifest is invalid + +--- + +### app_not_eligible {#app_not_eligible} + +**Message**: The specified app is not eligible for this API + +--- + +### app_not_found {#app_not_found} + +**Message**: The app was not found + +--- + +### app_not_hosted {#app_not_hosted} + +**Message**: App is not configured to be deployed to the Slack platform + +**Remediation**: Deploy an app containing workflow automations to Slack managed infrastructure +Read about ROSI: https://docs.slack.dev/workflows/run-on-slack-infrastructure + +--- + +### app_not_installed {#app_not_installed} + +**Message**: The provided app must be installed on this team + +--- + +### app_remove_error {#app_remove_error} + +**Message**: Couldn't remove your app + +--- + +### app_rename_app {#app_rename_app} + +**Message**: Couldn't rename your app + +--- + +### apps_list_error {#apps_list_error} + +**Message**: Couldn't get a list of your apps + +--- + +### at_active_sandbox_limit {#at_active_sandbox_limit} + +**Message**: You've reached the maximum number of active sandboxes + +--- + +### auth_prod_token_not_found {#auth_prod_token_not_found} + +**Message**: Couldn't find a valid auth token for the Slack API + +**Remediation**: You need to be logged in to at least 1 production (slack.com) team to use this command. Log into one with the `slack-cli login` command and try again. + +--- + +### auth_timeout_error {#auth_timeout_error} + +**Message**: Couldn't receive authorization in the time allowed + +**Remediation**: Ensure you have pasted the command in a Slack workspace and accepted the permissions. + +--- + +### auth_token_error {#auth_token_error} + +**Message**: Couldn't get a token with an active session + +--- + +### auth_verification_error {#auth_verification_error} + +**Message**: Couldn't verify your authorization + +--- + +### bot_invite_required {#bot_invite_required} + +**Message**: Your app must be invited to the channel + +**Remediation**: Try to find the channel declared the source code of a workflow or function. + +Open Slack, join the channel, invite your app, and try the command again. +Learn more: https://slack.com/help/articles/201980108-Add-people-to-a-channel + +--- + +### cannot_abandon_app {#cannot_abandon_app} + +**Message**: The last owner cannot be removed + +--- + +### cannot_add_owner {#cannot_add_owner} + +**Message**: Unable to add the given user as owner + +--- + +### cannot_count_owners {#cannot_count_owners} + +**Message**: Unable to retrieve current app collaborators + +--- + +### cannot_delete_app {#cannot_delete_app} + +**Message**: Unable to delete app + +--- + +### cannot_list_collaborators {#cannot_list_collaborators} + +**Message**: Calling user is unable to list collaborators + +--- + +### cannot_list_owners {#cannot_list_owners} + +**Message**: Calling user is unable to list owners + +--- + +### cannot_remove_collaborators {#cannot_remove_collaborators} + +**Message**: User is unable to remove collaborators + +--- + +### cannot_remove_owner {#cannot_remove_owner} + +**Message**: Unable to remove the given user + +--- + +### cannot_revoke_org_bot_token {#cannot_revoke_org_bot_token} + +**Message**: Revoking org-level bot token is not supported + +--- + +### channel_not_found {#channel_not_found} + +**Message**: Couldn't find the specified Slack channel + +**Remediation**: Try adding your app as a member to the channel. + +--- + +### cli_autoupdate_error {#cli_autoupdate_error} + +**Message**: Couldn't auto-update this command-line tool + +**Remediation**: You can manually install the latest version from: +https://docs.slack.dev/tools/slack-cli + +--- + +### cli_config_invalid {#cli_config_invalid} + +**Message**: Configuration invalid + +**Remediation**: Check your config.json file. + +--- + +### cli_config_location_error {#cli_config_location_error} + +**Message**: The .slack/cli.json configuration file is not supported + +**Remediation**: This version of the CLI no longer supports this configuration file. +Move the .slack/cli.json file to .slack/hooks.json and try again. + +--- + +### cli_read_error {#cli_read_error} + +**Message**: There was an error reading configuration + +**Remediation**: Check your config.json file. + +--- + +### cli_update_required {#cli_update_required} + +**Message**: Slack API requires the latest version of the Slack CLI + +**Remediation**: You can upgrade to the latest version of the Slack CLI using the command: `slack-cli upgrade` + +--- + +### comment_required {#comment_required} + +**Message**: Your admin is requesting a reason to approve installation of this app + +--- + +### connected_org_denied {#connected_org_denied} + +**Message**: The admin does not allow connected organizations to be named_entities + +--- + +### connected_team_denied {#connected_team_denied} + +**Message**: The admin does not allow connected teams to be named_entities + +--- + +### connector_approval_pending {#connector_approval_pending} + +**Message**: A connector requires admin approval before it can be installed +Approval is pending review + +**Remediation**: Contact your Slack admin about the status of your request + +--- + +### connector_approval_required {#connector_approval_required} + +**Message**: A connector requires admin approval before it can be installed + +**Remediation**: Request approval for the given connector from your Slack admin + +--- + +### connector_denied {#connector_denied} + +**Message**: A connector has been denied for use by an admin + +**Remediation**: Contact your Slack admin + +--- + +### connector_not_installed {#connector_not_installed} + +**Message**: A connector requires installation before it can be used + +**Remediation**: Request installation for the given connector + +--- + +### context_value_not_found {#context_value_not_found} + +**Message**: The context value could not be found + +--- + +### credentials_not_found {#credentials_not_found} + +**Message**: No authentication found for this team + +**Remediation**: Use the command `slack-cli login` to login to this workspace + +--- + +### customizable_input_missing_matching_workflow_input {#customizable_input_missing_matching_workflow_input} + +**Message**: Customizable input on the trigger must map to a workflow input of the same name + +--- + +### customizable_input_unsupported_type {#customizable_input_unsupported_type} + +**Message**: Customizable input has been mapped to a workflow input of an unsupported type. Only `UserID`, `ChannelId`, and `String` are supported for customizable inputs + +--- + +### customizable_inputs_not_allowed_on_optional_inputs {#customizable_inputs_not_allowed_on_optional_inputs} + +**Message**: Customizable trigger inputs must map to required workflow inputs + +--- + +### customizable_inputs_only_allowed_on_link_triggers {#customizable_inputs_only_allowed_on_link_triggers} + +**Message**: Customizable inputs are only allowed on link triggers + +--- + +### datastore_error {#datastore_error} + +**Message**: An error occurred while accessing your datastore + +--- + +### datastore_missing_primary_key {#datastore_missing_primary_key} + +**Message**: The primary key for the datastore is missing + +--- + +### datastore_not_found {#datastore_not_found} + +**Message**: The specified datastore could not be found + +--- + +### default_app_access_error {#default_app_access_error} + +**Message**: Couldn't access the default app + +--- + +### default_app_setting_error {#default_app_setting_error} + +**Message**: Couldn't set this app as the default + +--- + +### deno_not_found {#deno_not_found} + +**Message**: Couldn't find the 'deno' language runtime installed on this system + +**Remediation**: To install Deno, visit https://deno.land/#installation. + +--- + +### deployed_app_not_supported {#deployed_app_not_supported} + +**Message**: A deployed app cannot be used by this command + +--- + +### docs_search_flag_required {#docs_search_flag_required} + +**Message**: Invalid docs command. Did you mean to search? + +**Remediation**: Use --search flag: `slack-cli docs --search ""` + +--- + +### documentation_generation_failed {#documentation_generation_failed} + +**Message**: Failed to generate documentation + +--- + +### domain_taken {#domain_taken} + +**Message**: This domain has been claimed by another sandbox + +--- + +### dotenv_file_parse_error {#dotenv_file_parse_error} + +**Message**: Failed to parse the .env file + +--- + +### dotenv_file_read_error {#dotenv_file_read_error} + +**Message**: Failed to read the .env file + +--- + +### dotenv_file_write_error {#dotenv_file_write_error} + +**Message**: Failed to write the .env file + +--- + +### dotenv_var_marshal_error {#dotenv_var_marshal_error} + +**Message**: Failed to marshal the .env variable + +--- + +### enterprise_not_found {#enterprise_not_found} + +**Message**: The `enterprise` was not found + +--- + +### fail_to_get_teams_for_restricted_user {#fail_to_get_teams_for_restricted_user} + +**Message**: Failed to get teams for restricted user + +--- + +### failed_adding_collaborator {#failed_adding_collaborator} + +**Message**: Failed writing a collaborator record for this new app + +--- + +### failed_creating_app {#failed_creating_app} + +**Message**: Failed to create the app model + +--- + +### failed_datastore_operation {#failed_datastore_operation} + +**Message**: Failed while managing datastore infrastructure + +**Remediation**: Please try again and reach out to feedback@slack.com if the problem persists. + +--- + +### failed_export {#failed_export} + +**Message**: Couldn't export the app manifest + +--- + +### failed_for_some_requests {#failed_for_some_requests} + +**Message**: At least one request was not cancelled + +--- + +### failed_to_get_user {#failed_to_get_user} + +**Message**: Couldn't find the user to install the app + +--- + +### failed_to_save_extension_logs {#failed_to_save_extension_logs} + +**Message**: Couldn't save the logs + +--- + +### feedback_name_invalid {#feedback_name_invalid} + +**Message**: The name of the feedback is invalid + +**Remediation**: View the feedback options with `slack-cli feedback --help` + +--- + +### feedback_name_required {#feedback_name_required} + +**Message**: The name of the feedback is required + +**Remediation**: Please provide a `--name ` flag or remove the `--no-prompt` flag +View feedback options with `slack-cli feedback --help` + +--- + +### file_rejected {#file_rejected} + +**Message**: Not an acceptable S3 file + +--- + +### forbidden_team {#forbidden_team} + +**Message**: The authenticated team cannot use this API + +--- + +### free_team_not_allowed {#free_team_not_allowed} + +**Message**: Free workspaces do not support the Slack platform's low-code automation for workflows and functions + +**Remediation**: You can install this app if you upgrade your workspace: https://slack.com/pricing. + +--- + +### function_belongs_to_another_app {#function_belongs_to_another_app} + +**Message**: The provided function_id does not belong to this app_id + +--- + +### function_not_found {#function_not_found} + +**Message**: The specified function couldn't be found + +--- + +### git_clone_error {#git_clone_error} + +**Message**: Git failed to clone repository + +--- + +### git_not_found {#git_not_found} + +**Message**: Couldn't find Git installed on this system + +**Remediation**: To install Git, visit https://github.com/git-guides/install-git. + +--- + +### git_zip_download_error {#git_zip_download_error} + +**Message**: Cannot download Git repository as a .zip archive + +--- + +### home_directory_access_failed {#home_directory_access_failed} + +**Message**: Failed to read/create .slack/ directory in your home directory + +**Remediation**: A Slack directory is required for retrieving/storing auth credentials and config data. Check permissions on your system. + +--- + +### hooks_json_location_error {#hooks_json_location_error} + +**Message**: Missing the Slack hooks file from project configurations + +**Remediation**: A `.slack/hooks.json` file must be present in the project's `.slack` directory. + +--- + +### hosted_apps_disallow_user_scopes {#hosted_apps_disallow_user_scopes} + +**Message**: Hosted apps do not support user scopes + +--- + +### http_request_failed {#http_request_failed} + +**Message**: HTTP request failed + +--- + +### http_response_invalid {#http_response_invalid} + +**Message**: Received an invalid response from the server + +--- + +### insecure_request {#insecure_request} + +**Message**: The method was not called via a `POST` request + +--- + +### installation_denied {#installation_denied} + +**Message**: Couldn't install the app because the installation request was denied + +**Remediation**: Reach out to one of your App Managers for additional information. + +--- + +### installation_failed {#installation_failed} + +**Message**: Couldn't install the app + +--- + +### installation_required {#installation_required} + +**Message**: A valid installation of this app is required to take this action + +**Remediation**: Install the app with `slack-cli install` + +--- + +### internal_error {#internal_error} + +**Message**: An internal error has occurred with the Slack platform + +**Remediation**: Please reach out to feedback@slack.com if the problem persists. + +--- + +### invalid_app {#invalid_app} + +**Message**: Either the app does not exist or an app created from the provided manifest would not be valid + +--- + +### invalid_app_directory {#invalid_app_directory} + +**Message**: This is an invalid Slack app project directory + +**Remediation**: A valid Slack project includes the Slack hooks file: .slack/hooks.json + +If this is a Slack project, you can initialize it with `slack-cli init` + +--- + +### invalid_app_flag {#invalid_app_flag} + +**Message**: The provided --app flag value is not valid + +**Remediation**: Specify the environment with `--app local` or `--app deployed` +Or choose a specific app with `--app ` + +--- + +### invalid_app_id {#invalid_app_id} + +**Message**: App ID may be invalid for this user account and workspace + +**Remediation**: Check to make sure you are signed into the correct workspace for this app and you have the required permissions to perform this action. + +--- + +### invalid_archive_ttl {#invalid_archive_ttl} + +**Message**: Invalid TTL + +**Remediation**: Use days (1d), weeks (2w), or months (3mo); min 1d, max 6mo + +--- + +### invalid_args {#invalid_args} + +**Message**: Required arguments either were not provided or contain invalid values + +--- + +### invalid_arguments {#invalid_arguments} + +**Message**: Slack API request parameters are invalid + +--- + +### invalid_arguments_customizable_inputs {#invalid_arguments_customizable_inputs} + +**Message**: A trigger input parameter with customizable: true cannot be set as hidden or locked, nor have a value provided at trigger creation time + +--- + +### invalid_auth {#invalid_auth} + +**Message**: Your user account authorization isn't valid + +**Remediation**: Your user account authorization may be expired or does not have permission to access the resource. Try to login to the same user account again using `slack-cli login`. + +--- + +### invalid_challenge {#invalid_challenge} + +**Message**: The challenge code is invalid + +**Remediation**: The previous slash command and challenge code have now expired. To retry, use `slack-cli login`, paste the slash command in any Slack channel, and enter the challenge code displayed by Slack. It is easiest to copy & paste the challenge code. + +--- + +### invalid_channel_id {#invalid_channel_id} + +**Message**: Channel ID specified doesn't exist or you do not have permissions to access it + +**Remediation**: Channel ID appears to be formatted correctly. Check if this channel exists on the current team and that you have permissions to access it. + +--- + +### invalid_cursor {#invalid_cursor} + +**Message**: Value passed for `cursor` was not valid or is valid no longer + +--- + +### invalid_datastore {#invalid_datastore} + +**Message**: Invalid datastore specified in your project + +--- + +### invalid_datastore_expression {#invalid_datastore_expression} + +**Message**: The provided expression is not valid + +**Remediation**: Verify the expression you provided is valid JSON surrounded by quotations +Use `slack-cli datastore --help` for examples + +--- + +### invalid_distribution_type {#invalid_distribution_type} + +**Message**: This function requires distribution_type to be set as named_entities before adding users + +--- + +### invalid_flag {#invalid_flag} + +**Message**: The provided flag value is invalid + +--- + +### invalid_interactive_trigger_inputs {#invalid_interactive_trigger_inputs} + +**Message**: One or more input parameter types isn't supported by the link trigger type + +--- + +### invalid_manifest {#invalid_manifest} + +**Message**: The provided manifest file does not validate against schema. Consult the additional errors field to locate specific issues + +--- + +### invalid_manifest_source {#invalid_manifest_source} + +**Message**: A manifest does not exist at the provided source + +**Remediation**: Set 'manifest.source' to either "remote" or "local" in .slack/config.json +Read about manifest sourcing with the `slack-cli manifest info --help` command + +--- + +### invalid_parameters {#invalid_parameters} + +**Message**: slack_cli_version supplied is invalid + +--- + +### invalid_permission_type {#invalid_permission_type} + +**Message**: Permission type must be set to `named_entities` before you can manage users + +--- + +### invalid_refresh_token {#invalid_refresh_token} + +**Message**: The given refresh token is invalid + +--- + +### invalid_request_id {#invalid_request_id} + +**Message**: The request_id passed is invalid + +--- + +### invalid_resource_id {#invalid_resource_id} + +**Message**: The resource_id for the given resource_type is invalid + +--- + +### invalid_resource_type {#invalid_resource_type} + +**Message**: The resource_type argument is invalid. + +--- + +### invalid_s3_key {#invalid_s3_key} + +**Message**: An internal error occurred + +**Remediation**: Please reach out to feedback@slack.com if the problem persists. + +--- + +### invalid_sandbox_team_id {#invalid_sandbox_team_id} + +**Message**: The provided sandbox team ID is invalid + +**Remediation**: List your sandboxes with the `slack-cli sandbox list` command to find the ID + +--- + +### invalid_scopes {#invalid_scopes} + +**Message**: Some of the provided scopes do not exist + +--- + +### invalid_semver {#invalid_semver} + +**Message**: The provided version does not follow semantic versioning + +--- + +### invalid_slack_project_directory {#invalid_slack_project_directory} + +**Message**: Current directory is not a Slack project + +**Remediation**: Change in to a Slack project directory. A Slack project always includes the Slack hooks file (`.slack/hooks.json`). + +--- + +### invalid_template_id {#invalid_template_id} + +**Message**: The provided sandbox template value is invalid + +--- + +### invalid_token {#invalid_token} + +**Message**: The provided token is not valid + +--- + +### invalid_trigger {#invalid_trigger} + +**Message**: Invalid trigger specified in your project + +--- + +### invalid_trigger_access {#invalid_trigger_access} + +**Message**: Trigger access can not be configured for more than 10 users + +--- + +### invalid_trigger_config {#invalid_trigger_config} + +**Message**: The provided trigger object does not conform to the trigger type's schema + +--- + +### invalid_trigger_event_type {#invalid_trigger_event_type} + +**Message**: The provided event type is not allowed + +--- + +### invalid_trigger_inputs {#invalid_trigger_inputs} + +**Message**: Required inputs for the referenced function/workflow are not passed + +--- + +### invalid_trigger_type {#invalid_trigger_type} + +**Message**: The provided trigger type is not recognized + +--- + +### invalid_user_id {#invalid_user_id} + +**Message**: A value passed as a user_id is invalid + +--- + +### invalid_webhook_config {#invalid_webhook_config} + +**Message**: Only one of schema or schema_ref should be provided + +--- + +### invalid_webhook_schema_ref {#invalid_webhook_schema_ref} + +**Message**: Unable to parse the schema ref + +--- + +### invalid_workflow_app_id {#invalid_workflow_app_id} + +**Message**: A value passed as workflow_app_id is invalid or missing + +--- + +### invalid_workflow_id {#invalid_workflow_id} + +**Message**: A value passed as a workflow ID is invalid + +--- + +### is_restricted {#is_restricted} + +**Message**: Restricted users cannot request + +--- + +### local_app_not_found {#local_app_not_found} + +**Message**: Couldn't find the local app + +--- + +### local_app_not_supported {#local_app_not_supported} + +**Message**: A local app cannot be used by this command + +--- + +### local_app_removal_error {#local_app_removal_error} + +**Message**: Couldn't remove local app + +--- + +### local_app_run_error {#local_app_run_error} + +**Message**: Couldn't run app locally + +--- + +### method_not_supported {#method_not_supported} + +**Message**: This API method is not supported + +--- + +### mismatched_flags {#mismatched_flags} + +**Message**: The provided flags cannot be used together + +--- + +### missing_app_id {#missing_app_id} + +**Message**: workflow_app_id is required to update via workflow reference + +--- + +### missing_app_team_id {#missing_app_team_id} + +**Message**: team_id is required to create or update this app + +--- + +### missing_challenge {#missing_challenge} + +**Message**: Challenge must be supplied + +--- + +### missing_experiment {#missing_experiment} + +**Message**: The feature is behind an experiment not toggled on + +--- + +### missing_flag {#missing_flag} + +**Message**: An argument must be provided for the flag + +--- + +### missing_function_identifier {#missing_function_identifier} + +**Message**: Could not find the given workflow using the specified reference + +--- + +### missing_input {#missing_input} + +**Message**: A required value was not supplied as input + +--- + +### missing_options {#missing_options} + +**Message**: There are no options to select from + +--- + +### missing_scope {#missing_scope} + +**Message**: Your login is out of date + +**Remediation**: Run `slack-cli logout` and then `slack-cli login` again. + +--- + +### missing_scopes {#missing_scopes} + +**Message**: Additional scopes are required to create this type of trigger + +--- + +### missing_user {#missing_user} + +**Message**: The `user` was not found + +--- + +### missing_value {#missing_value} + +**Message**: Missing `value` property on an input. You must either provide the value now, or mark this input as `customizable`: `true` and provide the value at the time the trigger is executed. + +--- + +### no_file {#no_file} + +**Message**: Couldn't upload your bundled code to server + +**Remediation**: Please try again + +--- + +### no_pending_request {#no_pending_request} + +**Message**: Pending request not found + +--- + +### no_permission {#no_permission} + +**Message**: You are either not a collaborator on this app or you do not have permissions to perform this action + +**Remediation**: Contact the app owner to add you as a collaborator + +--- + +### no_token_found {#no_token_found} + +**Message**: No tokens found to delete + +--- + +### no_triggers {#no_triggers} + +**Message**: There are no triggers installed for this app + +--- + +### no_valid_named_entities {#no_valid_named_entities} + +**Message**: None of the provided named entities were valid + +--- + +### not_authed {#not_authed} + +**Message**: You are either not logged in or your login session has expired + +**Remediation**: Authorize your CLI with `slack-cli login` + +--- + +### not_bearer_token {#not_bearer_token} + +**Message**: Incompatible token type provided + +--- + +### not_found {#not_found} + +**Message**: Couldn't find row + +--- + +### org_grant_exists {#org_grant_exists} + +**Message**: A different org workspace grant already exists for the installed app + +--- + +### org_not_connected {#org_not_connected} + +**Message**: One or more of the listed organizations was not connected + +--- + +### org_not_found {#org_not_found} + +**Message**: One or more of the listed organizations could not be found + +--- + +### os_not_supported {#os_not_supported} + +**Message**: This operating system is not supported + +--- + +### over_resource_limit {#over_resource_limit} + +**Message**: Workspace exceeded the maximum number of Run On Slack functions and/or app datastores. + +--- + +### parameter_validation_failed {#parameter_validation_failed} + +**Message**: There were problems when validating the inputs against the function parameters. See API response for more details + +--- + +### process_interrupted {#process_interrupted} + +**Message**: The process received an interrupt signal + +--- + +### project_compilation_error {#project_compilation_error} + +**Message**: An error occurred while compiling your code + +--- + +### project_config_id_not_found {#project_config_id_not_found} + +**Message**: The "project_id" property is missing from the project-level configuration file + +--- + +### project_config_manifest_source_error {#project_config_manifest_source_error} + +**Message**: Project manifest source is not valid + +**Remediation**: Set 'manifest.source' to either "remote" or "local" in .slack/config.json +Read about manifest sourcing with the `slack-cli manifest info --help` command + +--- + +### project_file_update_error {#project_file_update_error} + +**Message**: Failed to update project files + +--- + +### prompt_error {#prompt_error} + +**Message**: An error occurred while executing prompts + +--- + +### provider_not_found {#provider_not_found} + +**Message**: The provided provider_key is invalid + +--- + +### published_app_only {#published_app_only} + +**Message**: This action is only permitted for published app IDs + +--- + +### ratelimited {#ratelimited} + +**Message**: Too many calls in succession during a short period of time + +--- + +### request_id_or_app_id_is_required {#request_id_or_app_id_is_required} + +**Message**: Must include a request_id or app_id + +--- + +### restricted_plan_level {#restricted_plan_level} + +**Message**: Your Slack plan does not have access to the requested feature + +--- + +### runtime_not_found {#runtime_not_found} + +**Message**: The hook runtime executable was not found + +**Remediation**: Make sure the required runtime has been installed to run hook scripts. + +--- + +### runtime_not_supported {#runtime_not_supported} + +**Message**: The SDK runtime is not supported by the CLI + +--- + +### sample_create_error {#sample_create_error} + +**Message**: Couldn't create app from sample + +--- + +### scopes_exceed_app_config {#scopes_exceed_app_config} + +**Message**: Scopes requested exceed app configuration + +--- + +### sdk_config_load_error {#sdk_config_load_error} + +**Message**: There was an error while reading the Slack hooks file (`.slack/hooks.json`) or running the `get-hooks` hook + +**Remediation**: Run `slack-cli doctor` to check that your system dependencies are up-to-date. + +--- + +### sdk_hook_invocation_failed {#sdk_hook_invocation_failed} + +**Message**: A script hook defined in the Slack Configuration file (`.slack/hooks.json`) returned an error + +**Remediation**: Run `slack-cli doctor` to check that your system dependencies are up-to-date. + +--- + +### sdk_hook_not_found {#sdk_hook_not_found} + +**Message**: A script in .slack/hooks.json was not found + +**Remediation**: Hook scripts are defined in one of these Slack hooks files: +- slack.json +- .slack/hooks.json + +Every app requires a Slack hooks file and you can find an example at: +https://github.com/slack-samples/deno-starter-template/blob/main/.slack/hooks.json + +You can create a hooks file manually or with the `slack-cli init` command. + +When manually creating the hooks file, you must install the hook dependencies. + +--- + +### service_limits_exceeded {#service_limits_exceeded} + +**Message**: Your workspace has exhausted the 10 apps limit for free teams. To create more apps, upgrade your Slack plan at https://my.slack.com/plans + +--- + +### shared_channel_denied {#shared_channel_denied} + +**Message**: The team admin does not allow shared channels to be named_entities + +--- + +### slack_auth_error {#slack_auth_error} + +**Message**: You are not logged into a team or have not installed an app + +**Remediation**: Use the command `slack-cli login` to login and `slack-cli install` to install your app + +--- + +### slack_json_location_error {#slack_json_location_error} + +**Message**: The slack.json configuration file is deprecated + +**Remediation**: Next major version of the CLI will no longer support this configuration file. +Move the slack.json file to .slack/hooks.json and continue onwards. + +--- + +### slack_slack_json_location_error {#slack_slack_json_location_error} + +**Message**: The .slack/slack.json configuration file is deprecated + +**Remediation**: Next major version of the CLI will no longer support this configuration file. +Move the .slack/slack.json file to .slack/hooks.json and proceed again. + +--- + +### socket_connection_error {#socket_connection_error} + +**Message**: Couldn't connect to Slack over WebSocket + +--- + +### streaming_activity_logs_error {#streaming_activity_logs_error} + +**Message**: Failed to stream the most recent activity logs + +--- + +### subdir_not_found {#subdir_not_found} + +**Message**: The specified subdirectory was not found in the template repository + +--- + +### survey_config_not_found {#survey_config_not_found} + +**Message**: Survey config not found + +--- + +### system_config_id_not_found {#system_config_id_not_found} + +**Message**: The "system_id" property is missing from the system-level configuration file + +--- + +### system_requirements_failed {#system_requirements_failed} + +**Message**: Couldn't verify all system requirements + +--- + +### team_access_not_granted {#team_access_not_granted} + +**Message**: There was an issue granting access to the team + +--- + +### team_flag_required {#team_flag_required} + +**Message**: The --team flag must be provided + +**Remediation**: Choose a specific team with `--team ` or `--team ` + +--- + +### team_list_error {#team_list_error} + +**Message**: Couldn't get a list of teams + +--- + +### team_not_connected {#team_not_connected} + +**Message**: One or more of the listed teams was not connected by org + +--- + +### team_not_found {#team_not_found} + +**Message**: Team could not be found + +--- + +### team_not_on_enterprise {#team_not_on_enterprise} + +**Message**: Cannot query team by domain because team is not on an enterprise + +--- + +### team_quota_exceeded {#team_quota_exceeded} + +**Message**: Total number of requests exceeded team quota + +--- + +### template_path_not_found {#template_path_not_found} + +**Message**: No template app was found at the provided path + +--- + +### token_expired {#token_expired} + +**Message**: Your access token has expired + +**Remediation**: Use the command `slack-cli login` to authenticate again + +--- + +### token_revoked {#token_revoked} + +**Message**: Your token has already been revoked + +**Remediation**: Use the command `slack-cli login` to authenticate again + +--- + +### token_rotation_error {#token_rotation_error} + +**Message**: An error occurred while rotating your access token + +**Remediation**: Use the command `slack-cli login` to authenticate again + +--- + +### too_many_customizable_inputs {#too_many_customizable_inputs} + +**Message**: Cannot have more than 10 customizable inputs + +--- + +### too_many_ids_provided {#too_many_ids_provided} + +**Message**: Ensure you provide only app_id OR request_id + +--- + +### too_many_named_entities {#too_many_named_entities} + +**Message**: Too many named entities passed into the trigger permissions setting + +--- + +### trigger_create_error {#trigger_create_error} + +**Message**: Couldn't create a trigger + +--- + +### trigger_delete_error {#trigger_delete_error} + +**Message**: Couldn't delete a trigger + +--- + +### trigger_does_not_exist {#trigger_does_not_exist} + +**Message**: The trigger provided does not exist + +--- + +### trigger_not_found {#trigger_not_found} + +**Message**: The specified trigger cannot be found + +--- + +### trigger_update_error {#trigger_update_error} + +**Message**: Couldn't update a trigger + +--- + +### unable_to_delete {#unable_to_delete} + +**Message**: There was an error deleting tokens + +--- + +### unable_to_open_file {#unable_to_open_file} + +**Message**: Error with file upload + +--- + +### unable_to_parse_json {#unable_to_parse_json} + +**Message**: `` Couldn't be parsed as a json object + +--- + +### uninstall_halted {#uninstall_halted} + +**Message**: The uninstall process was interrupted + +--- + +### unknown_file_type {#unknown_file_type} + +**Message**: Unknown file type, must be application/zip + +--- + +### unknown_function_id {#unknown_function_id} + +**Message**: The provided function_id was not found + +--- + +### unknown_method {#unknown_method} + +**Message**: The Slack API method does not exist or you do not have permissions to access it + +--- + +### unknown_webhook_schema_ref {#unknown_webhook_schema_ref} + +**Message**: Unable to find the corresponding type based on the schema ref + +--- + +### unknown_workflow_id {#unknown_workflow_id} + +**Message**: The provided workflow_id was not found for this app + +--- + +### unsupported_file_name {#unsupported_file_name} + +**Message**: File name is not supported + +--- + +### untrusted_source {#untrusted_source} + +**Message**: Source is by an unknown or untrusted author + +**Remediation**: Use --force flag or set trust_unknown_sources: true in config.json file to disable warning + +--- + +### user_already_owner {#user_already_owner} + +**Message**: The user is already an owner for this app + +--- + +### user_already_requested {#user_already_requested} + +**Message**: The user has a request pending for this app + +--- + +### user_cannot_manage_app {#user_cannot_manage_app} + +**Message**: You do not have permissions to install this app + +**Remediation**: Reach out to one of your App Managers to request permissions to install apps. + +--- + +### user_id_is_required {#user_id_is_required} + +**Message**: Must include a user_id to cancel request for an app with app_id + +--- + +### user_not_found {#user_not_found} + +**Message**: User cannot be found + +--- + +### user_removed_from_team {#user_removed_from_team} + +**Message**: User removed from team (generated) + +--- + +### workflow_not_found {#workflow_not_found} + +**Message**: Workflow not found + +--- + +### yaml_error {#yaml_error} + +**Message**: An error occurred while parsing the app manifest YAML file + +--- + +## Additional help + +These error codes might reference an error you've encountered, but not provide +enough details for a workaround. + +For more help, post to our issue tracker: https://github.com/slackapi/slack-cli/issues diff --git a/internal/slackerror/errors.go b/internal/slackerror/errors.go index 807eb16b..221af014 100644 --- a/internal/slackerror/errors.go +++ b/internal/slackerror/errors.go @@ -693,7 +693,7 @@ Otherwise start your app for local development with: %s`, ErrDocsSearchFlagRequired: { Code: ErrDocsSearchFlagRequired, Message: "Invalid docs command. Did you mean to search?", - Remediation: fmt.Sprintf("Use --search flag: %s", style.Commandf("docs --search \"\"", false)), + Remediation: fmt.Sprintf("Use search subcommand: %s", style.Commandf("docs search \"\"", false)), }, ErrDotEnvFileParse: { From 2bc9fa735e1ac95829b99686b4577d9a0e635827 Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 3 Apr 2026 12:00:08 -0700 Subject: [PATCH 22/24] wrong docgen --- docs/errors.md | 1692 ------------------------------------------------ 1 file changed, 1692 deletions(-) delete mode 100644 docs/errors.md diff --git a/docs/errors.md b/docs/errors.md deleted file mode 100644 index efb51c2e..00000000 --- a/docs/errors.md +++ /dev/null @@ -1,1692 +0,0 @@ -# Slack CLI errors reference - -Troubleshooting errors can be tricky between your development environment, the -Slack CLI, and those encountered when running your code. Below are some common -ones, as well as a list of the errors the Slack CLI may raise, what they mean, -and some ways to remediate them. - -## Slack CLI errors list - -### access_denied {#access_denied} - -**Message**: You don't have the permission to access the specified resource - -**Remediation**: Check with your Slack admin to make sure that you have permission to access the resource. - ---- - -### add_app_to_project_error {#add_app_to_project_error} - -**Message**: Couldn't save your app's info to this project - ---- - -### already_logged_out {#already_logged_out} - -**Message**: You're already logged out - ---- - -### already_resolved {#already_resolved} - -**Message**: The app already has a resolution and cannot be requested - ---- - -### app_add_error {#app_add_error} - -**Message**: Couldn't create a new app - ---- - -### app_add_exists {#app_add_exists} - -**Message**: App already exists belonging to the team - ---- - -### app_approval_request_denied {#app_approval_request_denied} - -**Message**: This app is currently denied for installation - -**Remediation**: Reach out to an admin for additional information, or try requesting again with different scopes and outgoing domains - ---- - -### app_approval_request_eligible {#app_approval_request_eligible} - -**Message**: This app requires permissions that must be reviewed by an admin before you can install it - ---- - -### app_approval_request_pending {#app_approval_request_pending} - -**Message**: This app has requested admin approval to install and is awaiting review - -**Remediation**: Reach out to an admin for additional information - ---- - -### app_auth_team_mismatch {#app_auth_team_mismatch} - -**Message**: Specified app and team are mismatched - -**Remediation**: Try a different combination of `--app` and `--team` flags - ---- - -### app_create_error {#app_create_error} - -**Message**: Couldn't create your app - ---- - -### app_delete_error {#app_delete_error} - -**Message**: Couldn't delete your app - ---- - -### app_deploy_error {#app_deploy_error} - -**Message**: Couldn't deploy your app - ---- - -### app_deploy_function_runtime_not_slack {#app_deploy_function_runtime_not_slack} - -**Message**: Deployment to Slack is not currently supported for apps with `runOnSlack` set as false - -**Remediation**: Learn about building apps with the Deno Slack SDK: - -https://docs.slack.dev/tools/deno-slack-sdk - -If you are using a Bolt framework, add a deploy hook then run: `slack-cli deploy` - -Otherwise start your app for local development with: `slack-cli run` - ---- - -### app_dir_only_fail {#app_dir_only_fail} - -**Message**: The app was neither in the app directory nor created on this team/org, and cannot be requested - ---- - -### app_directory_access_error {#app_directory_access_error} - -**Message**: Couldn't access app directory - ---- - -### app_flag_required {#app_flag_required} - -**Message**: The --app flag must be provided - -**Remediation**: Choose a specific app with `--app ` - ---- - -### app_found {#app_found} - -**Message**: An app was found - ---- - -### app_hosted {#app_hosted} - -**Message**: App is configured for Run on Slack infrastructure - ---- - -### app_install_error {#app_install_error} - -**Message**: Couldn't install your app to a workspace - ---- - -### app_manifest_access_error {#app_manifest_access_error} - -**Message**: Couldn't access your app manifest - ---- - -### app_manifest_create_error {#app_manifest_create_error} - -**Message**: Couldn't create your app manifest - ---- - -### app_manifest_generate_error {#app_manifest_generate_error} - -**Message**: Couldn't generate an app manifest from this project - -**Remediation**: Check to make sure you are in a valid Slack project directory and that your project has no compilation errors. - ---- - -### app_manifest_update_error {#app_manifest_update_error} - -**Message**: The app manifest was not updated - ---- - -### app_manifest_validate_error {#app_manifest_validate_error} - -**Message**: Your app manifest is invalid - ---- - -### app_not_eligible {#app_not_eligible} - -**Message**: The specified app is not eligible for this API - ---- - -### app_not_found {#app_not_found} - -**Message**: The app was not found - ---- - -### app_not_hosted {#app_not_hosted} - -**Message**: App is not configured to be deployed to the Slack platform - -**Remediation**: Deploy an app containing workflow automations to Slack managed infrastructure -Read about ROSI: https://docs.slack.dev/workflows/run-on-slack-infrastructure - ---- - -### app_not_installed {#app_not_installed} - -**Message**: The provided app must be installed on this team - ---- - -### app_remove_error {#app_remove_error} - -**Message**: Couldn't remove your app - ---- - -### app_rename_app {#app_rename_app} - -**Message**: Couldn't rename your app - ---- - -### apps_list_error {#apps_list_error} - -**Message**: Couldn't get a list of your apps - ---- - -### at_active_sandbox_limit {#at_active_sandbox_limit} - -**Message**: You've reached the maximum number of active sandboxes - ---- - -### auth_prod_token_not_found {#auth_prod_token_not_found} - -**Message**: Couldn't find a valid auth token for the Slack API - -**Remediation**: You need to be logged in to at least 1 production (slack.com) team to use this command. Log into one with the `slack-cli login` command and try again. - ---- - -### auth_timeout_error {#auth_timeout_error} - -**Message**: Couldn't receive authorization in the time allowed - -**Remediation**: Ensure you have pasted the command in a Slack workspace and accepted the permissions. - ---- - -### auth_token_error {#auth_token_error} - -**Message**: Couldn't get a token with an active session - ---- - -### auth_verification_error {#auth_verification_error} - -**Message**: Couldn't verify your authorization - ---- - -### bot_invite_required {#bot_invite_required} - -**Message**: Your app must be invited to the channel - -**Remediation**: Try to find the channel declared the source code of a workflow or function. - -Open Slack, join the channel, invite your app, and try the command again. -Learn more: https://slack.com/help/articles/201980108-Add-people-to-a-channel - ---- - -### cannot_abandon_app {#cannot_abandon_app} - -**Message**: The last owner cannot be removed - ---- - -### cannot_add_owner {#cannot_add_owner} - -**Message**: Unable to add the given user as owner - ---- - -### cannot_count_owners {#cannot_count_owners} - -**Message**: Unable to retrieve current app collaborators - ---- - -### cannot_delete_app {#cannot_delete_app} - -**Message**: Unable to delete app - ---- - -### cannot_list_collaborators {#cannot_list_collaborators} - -**Message**: Calling user is unable to list collaborators - ---- - -### cannot_list_owners {#cannot_list_owners} - -**Message**: Calling user is unable to list owners - ---- - -### cannot_remove_collaborators {#cannot_remove_collaborators} - -**Message**: User is unable to remove collaborators - ---- - -### cannot_remove_owner {#cannot_remove_owner} - -**Message**: Unable to remove the given user - ---- - -### cannot_revoke_org_bot_token {#cannot_revoke_org_bot_token} - -**Message**: Revoking org-level bot token is not supported - ---- - -### channel_not_found {#channel_not_found} - -**Message**: Couldn't find the specified Slack channel - -**Remediation**: Try adding your app as a member to the channel. - ---- - -### cli_autoupdate_error {#cli_autoupdate_error} - -**Message**: Couldn't auto-update this command-line tool - -**Remediation**: You can manually install the latest version from: -https://docs.slack.dev/tools/slack-cli - ---- - -### cli_config_invalid {#cli_config_invalid} - -**Message**: Configuration invalid - -**Remediation**: Check your config.json file. - ---- - -### cli_config_location_error {#cli_config_location_error} - -**Message**: The .slack/cli.json configuration file is not supported - -**Remediation**: This version of the CLI no longer supports this configuration file. -Move the .slack/cli.json file to .slack/hooks.json and try again. - ---- - -### cli_read_error {#cli_read_error} - -**Message**: There was an error reading configuration - -**Remediation**: Check your config.json file. - ---- - -### cli_update_required {#cli_update_required} - -**Message**: Slack API requires the latest version of the Slack CLI - -**Remediation**: You can upgrade to the latest version of the Slack CLI using the command: `slack-cli upgrade` - ---- - -### comment_required {#comment_required} - -**Message**: Your admin is requesting a reason to approve installation of this app - ---- - -### connected_org_denied {#connected_org_denied} - -**Message**: The admin does not allow connected organizations to be named_entities - ---- - -### connected_team_denied {#connected_team_denied} - -**Message**: The admin does not allow connected teams to be named_entities - ---- - -### connector_approval_pending {#connector_approval_pending} - -**Message**: A connector requires admin approval before it can be installed -Approval is pending review - -**Remediation**: Contact your Slack admin about the status of your request - ---- - -### connector_approval_required {#connector_approval_required} - -**Message**: A connector requires admin approval before it can be installed - -**Remediation**: Request approval for the given connector from your Slack admin - ---- - -### connector_denied {#connector_denied} - -**Message**: A connector has been denied for use by an admin - -**Remediation**: Contact your Slack admin - ---- - -### connector_not_installed {#connector_not_installed} - -**Message**: A connector requires installation before it can be used - -**Remediation**: Request installation for the given connector - ---- - -### context_value_not_found {#context_value_not_found} - -**Message**: The context value could not be found - ---- - -### credentials_not_found {#credentials_not_found} - -**Message**: No authentication found for this team - -**Remediation**: Use the command `slack-cli login` to login to this workspace - ---- - -### customizable_input_missing_matching_workflow_input {#customizable_input_missing_matching_workflow_input} - -**Message**: Customizable input on the trigger must map to a workflow input of the same name - ---- - -### customizable_input_unsupported_type {#customizable_input_unsupported_type} - -**Message**: Customizable input has been mapped to a workflow input of an unsupported type. Only `UserID`, `ChannelId`, and `String` are supported for customizable inputs - ---- - -### customizable_inputs_not_allowed_on_optional_inputs {#customizable_inputs_not_allowed_on_optional_inputs} - -**Message**: Customizable trigger inputs must map to required workflow inputs - ---- - -### customizable_inputs_only_allowed_on_link_triggers {#customizable_inputs_only_allowed_on_link_triggers} - -**Message**: Customizable inputs are only allowed on link triggers - ---- - -### datastore_error {#datastore_error} - -**Message**: An error occurred while accessing your datastore - ---- - -### datastore_missing_primary_key {#datastore_missing_primary_key} - -**Message**: The primary key for the datastore is missing - ---- - -### datastore_not_found {#datastore_not_found} - -**Message**: The specified datastore could not be found - ---- - -### default_app_access_error {#default_app_access_error} - -**Message**: Couldn't access the default app - ---- - -### default_app_setting_error {#default_app_setting_error} - -**Message**: Couldn't set this app as the default - ---- - -### deno_not_found {#deno_not_found} - -**Message**: Couldn't find the 'deno' language runtime installed on this system - -**Remediation**: To install Deno, visit https://deno.land/#installation. - ---- - -### deployed_app_not_supported {#deployed_app_not_supported} - -**Message**: A deployed app cannot be used by this command - ---- - -### docs_search_flag_required {#docs_search_flag_required} - -**Message**: Invalid docs command. Did you mean to search? - -**Remediation**: Use --search flag: `slack-cli docs --search ""` - ---- - -### documentation_generation_failed {#documentation_generation_failed} - -**Message**: Failed to generate documentation - ---- - -### domain_taken {#domain_taken} - -**Message**: This domain has been claimed by another sandbox - ---- - -### dotenv_file_parse_error {#dotenv_file_parse_error} - -**Message**: Failed to parse the .env file - ---- - -### dotenv_file_read_error {#dotenv_file_read_error} - -**Message**: Failed to read the .env file - ---- - -### dotenv_file_write_error {#dotenv_file_write_error} - -**Message**: Failed to write the .env file - ---- - -### dotenv_var_marshal_error {#dotenv_var_marshal_error} - -**Message**: Failed to marshal the .env variable - ---- - -### enterprise_not_found {#enterprise_not_found} - -**Message**: The `enterprise` was not found - ---- - -### fail_to_get_teams_for_restricted_user {#fail_to_get_teams_for_restricted_user} - -**Message**: Failed to get teams for restricted user - ---- - -### failed_adding_collaborator {#failed_adding_collaborator} - -**Message**: Failed writing a collaborator record for this new app - ---- - -### failed_creating_app {#failed_creating_app} - -**Message**: Failed to create the app model - ---- - -### failed_datastore_operation {#failed_datastore_operation} - -**Message**: Failed while managing datastore infrastructure - -**Remediation**: Please try again and reach out to feedback@slack.com if the problem persists. - ---- - -### failed_export {#failed_export} - -**Message**: Couldn't export the app manifest - ---- - -### failed_for_some_requests {#failed_for_some_requests} - -**Message**: At least one request was not cancelled - ---- - -### failed_to_get_user {#failed_to_get_user} - -**Message**: Couldn't find the user to install the app - ---- - -### failed_to_save_extension_logs {#failed_to_save_extension_logs} - -**Message**: Couldn't save the logs - ---- - -### feedback_name_invalid {#feedback_name_invalid} - -**Message**: The name of the feedback is invalid - -**Remediation**: View the feedback options with `slack-cli feedback --help` - ---- - -### feedback_name_required {#feedback_name_required} - -**Message**: The name of the feedback is required - -**Remediation**: Please provide a `--name ` flag or remove the `--no-prompt` flag -View feedback options with `slack-cli feedback --help` - ---- - -### file_rejected {#file_rejected} - -**Message**: Not an acceptable S3 file - ---- - -### forbidden_team {#forbidden_team} - -**Message**: The authenticated team cannot use this API - ---- - -### free_team_not_allowed {#free_team_not_allowed} - -**Message**: Free workspaces do not support the Slack platform's low-code automation for workflows and functions - -**Remediation**: You can install this app if you upgrade your workspace: https://slack.com/pricing. - ---- - -### function_belongs_to_another_app {#function_belongs_to_another_app} - -**Message**: The provided function_id does not belong to this app_id - ---- - -### function_not_found {#function_not_found} - -**Message**: The specified function couldn't be found - ---- - -### git_clone_error {#git_clone_error} - -**Message**: Git failed to clone repository - ---- - -### git_not_found {#git_not_found} - -**Message**: Couldn't find Git installed on this system - -**Remediation**: To install Git, visit https://github.com/git-guides/install-git. - ---- - -### git_zip_download_error {#git_zip_download_error} - -**Message**: Cannot download Git repository as a .zip archive - ---- - -### home_directory_access_failed {#home_directory_access_failed} - -**Message**: Failed to read/create .slack/ directory in your home directory - -**Remediation**: A Slack directory is required for retrieving/storing auth credentials and config data. Check permissions on your system. - ---- - -### hooks_json_location_error {#hooks_json_location_error} - -**Message**: Missing the Slack hooks file from project configurations - -**Remediation**: A `.slack/hooks.json` file must be present in the project's `.slack` directory. - ---- - -### hosted_apps_disallow_user_scopes {#hosted_apps_disallow_user_scopes} - -**Message**: Hosted apps do not support user scopes - ---- - -### http_request_failed {#http_request_failed} - -**Message**: HTTP request failed - ---- - -### http_response_invalid {#http_response_invalid} - -**Message**: Received an invalid response from the server - ---- - -### insecure_request {#insecure_request} - -**Message**: The method was not called via a `POST` request - ---- - -### installation_denied {#installation_denied} - -**Message**: Couldn't install the app because the installation request was denied - -**Remediation**: Reach out to one of your App Managers for additional information. - ---- - -### installation_failed {#installation_failed} - -**Message**: Couldn't install the app - ---- - -### installation_required {#installation_required} - -**Message**: A valid installation of this app is required to take this action - -**Remediation**: Install the app with `slack-cli install` - ---- - -### internal_error {#internal_error} - -**Message**: An internal error has occurred with the Slack platform - -**Remediation**: Please reach out to feedback@slack.com if the problem persists. - ---- - -### invalid_app {#invalid_app} - -**Message**: Either the app does not exist or an app created from the provided manifest would not be valid - ---- - -### invalid_app_directory {#invalid_app_directory} - -**Message**: This is an invalid Slack app project directory - -**Remediation**: A valid Slack project includes the Slack hooks file: .slack/hooks.json - -If this is a Slack project, you can initialize it with `slack-cli init` - ---- - -### invalid_app_flag {#invalid_app_flag} - -**Message**: The provided --app flag value is not valid - -**Remediation**: Specify the environment with `--app local` or `--app deployed` -Or choose a specific app with `--app ` - ---- - -### invalid_app_id {#invalid_app_id} - -**Message**: App ID may be invalid for this user account and workspace - -**Remediation**: Check to make sure you are signed into the correct workspace for this app and you have the required permissions to perform this action. - ---- - -### invalid_archive_ttl {#invalid_archive_ttl} - -**Message**: Invalid TTL - -**Remediation**: Use days (1d), weeks (2w), or months (3mo); min 1d, max 6mo - ---- - -### invalid_args {#invalid_args} - -**Message**: Required arguments either were not provided or contain invalid values - ---- - -### invalid_arguments {#invalid_arguments} - -**Message**: Slack API request parameters are invalid - ---- - -### invalid_arguments_customizable_inputs {#invalid_arguments_customizable_inputs} - -**Message**: A trigger input parameter with customizable: true cannot be set as hidden or locked, nor have a value provided at trigger creation time - ---- - -### invalid_auth {#invalid_auth} - -**Message**: Your user account authorization isn't valid - -**Remediation**: Your user account authorization may be expired or does not have permission to access the resource. Try to login to the same user account again using `slack-cli login`. - ---- - -### invalid_challenge {#invalid_challenge} - -**Message**: The challenge code is invalid - -**Remediation**: The previous slash command and challenge code have now expired. To retry, use `slack-cli login`, paste the slash command in any Slack channel, and enter the challenge code displayed by Slack. It is easiest to copy & paste the challenge code. - ---- - -### invalid_channel_id {#invalid_channel_id} - -**Message**: Channel ID specified doesn't exist or you do not have permissions to access it - -**Remediation**: Channel ID appears to be formatted correctly. Check if this channel exists on the current team and that you have permissions to access it. - ---- - -### invalid_cursor {#invalid_cursor} - -**Message**: Value passed for `cursor` was not valid or is valid no longer - ---- - -### invalid_datastore {#invalid_datastore} - -**Message**: Invalid datastore specified in your project - ---- - -### invalid_datastore_expression {#invalid_datastore_expression} - -**Message**: The provided expression is not valid - -**Remediation**: Verify the expression you provided is valid JSON surrounded by quotations -Use `slack-cli datastore --help` for examples - ---- - -### invalid_distribution_type {#invalid_distribution_type} - -**Message**: This function requires distribution_type to be set as named_entities before adding users - ---- - -### invalid_flag {#invalid_flag} - -**Message**: The provided flag value is invalid - ---- - -### invalid_interactive_trigger_inputs {#invalid_interactive_trigger_inputs} - -**Message**: One or more input parameter types isn't supported by the link trigger type - ---- - -### invalid_manifest {#invalid_manifest} - -**Message**: The provided manifest file does not validate against schema. Consult the additional errors field to locate specific issues - ---- - -### invalid_manifest_source {#invalid_manifest_source} - -**Message**: A manifest does not exist at the provided source - -**Remediation**: Set 'manifest.source' to either "remote" or "local" in .slack/config.json -Read about manifest sourcing with the `slack-cli manifest info --help` command - ---- - -### invalid_parameters {#invalid_parameters} - -**Message**: slack_cli_version supplied is invalid - ---- - -### invalid_permission_type {#invalid_permission_type} - -**Message**: Permission type must be set to `named_entities` before you can manage users - ---- - -### invalid_refresh_token {#invalid_refresh_token} - -**Message**: The given refresh token is invalid - ---- - -### invalid_request_id {#invalid_request_id} - -**Message**: The request_id passed is invalid - ---- - -### invalid_resource_id {#invalid_resource_id} - -**Message**: The resource_id for the given resource_type is invalid - ---- - -### invalid_resource_type {#invalid_resource_type} - -**Message**: The resource_type argument is invalid. - ---- - -### invalid_s3_key {#invalid_s3_key} - -**Message**: An internal error occurred - -**Remediation**: Please reach out to feedback@slack.com if the problem persists. - ---- - -### invalid_sandbox_team_id {#invalid_sandbox_team_id} - -**Message**: The provided sandbox team ID is invalid - -**Remediation**: List your sandboxes with the `slack-cli sandbox list` command to find the ID - ---- - -### invalid_scopes {#invalid_scopes} - -**Message**: Some of the provided scopes do not exist - ---- - -### invalid_semver {#invalid_semver} - -**Message**: The provided version does not follow semantic versioning - ---- - -### invalid_slack_project_directory {#invalid_slack_project_directory} - -**Message**: Current directory is not a Slack project - -**Remediation**: Change in to a Slack project directory. A Slack project always includes the Slack hooks file (`.slack/hooks.json`). - ---- - -### invalid_template_id {#invalid_template_id} - -**Message**: The provided sandbox template value is invalid - ---- - -### invalid_token {#invalid_token} - -**Message**: The provided token is not valid - ---- - -### invalid_trigger {#invalid_trigger} - -**Message**: Invalid trigger specified in your project - ---- - -### invalid_trigger_access {#invalid_trigger_access} - -**Message**: Trigger access can not be configured for more than 10 users - ---- - -### invalid_trigger_config {#invalid_trigger_config} - -**Message**: The provided trigger object does not conform to the trigger type's schema - ---- - -### invalid_trigger_event_type {#invalid_trigger_event_type} - -**Message**: The provided event type is not allowed - ---- - -### invalid_trigger_inputs {#invalid_trigger_inputs} - -**Message**: Required inputs for the referenced function/workflow are not passed - ---- - -### invalid_trigger_type {#invalid_trigger_type} - -**Message**: The provided trigger type is not recognized - ---- - -### invalid_user_id {#invalid_user_id} - -**Message**: A value passed as a user_id is invalid - ---- - -### invalid_webhook_config {#invalid_webhook_config} - -**Message**: Only one of schema or schema_ref should be provided - ---- - -### invalid_webhook_schema_ref {#invalid_webhook_schema_ref} - -**Message**: Unable to parse the schema ref - ---- - -### invalid_workflow_app_id {#invalid_workflow_app_id} - -**Message**: A value passed as workflow_app_id is invalid or missing - ---- - -### invalid_workflow_id {#invalid_workflow_id} - -**Message**: A value passed as a workflow ID is invalid - ---- - -### is_restricted {#is_restricted} - -**Message**: Restricted users cannot request - ---- - -### local_app_not_found {#local_app_not_found} - -**Message**: Couldn't find the local app - ---- - -### local_app_not_supported {#local_app_not_supported} - -**Message**: A local app cannot be used by this command - ---- - -### local_app_removal_error {#local_app_removal_error} - -**Message**: Couldn't remove local app - ---- - -### local_app_run_error {#local_app_run_error} - -**Message**: Couldn't run app locally - ---- - -### method_not_supported {#method_not_supported} - -**Message**: This API method is not supported - ---- - -### mismatched_flags {#mismatched_flags} - -**Message**: The provided flags cannot be used together - ---- - -### missing_app_id {#missing_app_id} - -**Message**: workflow_app_id is required to update via workflow reference - ---- - -### missing_app_team_id {#missing_app_team_id} - -**Message**: team_id is required to create or update this app - ---- - -### missing_challenge {#missing_challenge} - -**Message**: Challenge must be supplied - ---- - -### missing_experiment {#missing_experiment} - -**Message**: The feature is behind an experiment not toggled on - ---- - -### missing_flag {#missing_flag} - -**Message**: An argument must be provided for the flag - ---- - -### missing_function_identifier {#missing_function_identifier} - -**Message**: Could not find the given workflow using the specified reference - ---- - -### missing_input {#missing_input} - -**Message**: A required value was not supplied as input - ---- - -### missing_options {#missing_options} - -**Message**: There are no options to select from - ---- - -### missing_scope {#missing_scope} - -**Message**: Your login is out of date - -**Remediation**: Run `slack-cli logout` and then `slack-cli login` again. - ---- - -### missing_scopes {#missing_scopes} - -**Message**: Additional scopes are required to create this type of trigger - ---- - -### missing_user {#missing_user} - -**Message**: The `user` was not found - ---- - -### missing_value {#missing_value} - -**Message**: Missing `value` property on an input. You must either provide the value now, or mark this input as `customizable`: `true` and provide the value at the time the trigger is executed. - ---- - -### no_file {#no_file} - -**Message**: Couldn't upload your bundled code to server - -**Remediation**: Please try again - ---- - -### no_pending_request {#no_pending_request} - -**Message**: Pending request not found - ---- - -### no_permission {#no_permission} - -**Message**: You are either not a collaborator on this app or you do not have permissions to perform this action - -**Remediation**: Contact the app owner to add you as a collaborator - ---- - -### no_token_found {#no_token_found} - -**Message**: No tokens found to delete - ---- - -### no_triggers {#no_triggers} - -**Message**: There are no triggers installed for this app - ---- - -### no_valid_named_entities {#no_valid_named_entities} - -**Message**: None of the provided named entities were valid - ---- - -### not_authed {#not_authed} - -**Message**: You are either not logged in or your login session has expired - -**Remediation**: Authorize your CLI with `slack-cli login` - ---- - -### not_bearer_token {#not_bearer_token} - -**Message**: Incompatible token type provided - ---- - -### not_found {#not_found} - -**Message**: Couldn't find row - ---- - -### org_grant_exists {#org_grant_exists} - -**Message**: A different org workspace grant already exists for the installed app - ---- - -### org_not_connected {#org_not_connected} - -**Message**: One or more of the listed organizations was not connected - ---- - -### org_not_found {#org_not_found} - -**Message**: One or more of the listed organizations could not be found - ---- - -### os_not_supported {#os_not_supported} - -**Message**: This operating system is not supported - ---- - -### over_resource_limit {#over_resource_limit} - -**Message**: Workspace exceeded the maximum number of Run On Slack functions and/or app datastores. - ---- - -### parameter_validation_failed {#parameter_validation_failed} - -**Message**: There were problems when validating the inputs against the function parameters. See API response for more details - ---- - -### process_interrupted {#process_interrupted} - -**Message**: The process received an interrupt signal - ---- - -### project_compilation_error {#project_compilation_error} - -**Message**: An error occurred while compiling your code - ---- - -### project_config_id_not_found {#project_config_id_not_found} - -**Message**: The "project_id" property is missing from the project-level configuration file - ---- - -### project_config_manifest_source_error {#project_config_manifest_source_error} - -**Message**: Project manifest source is not valid - -**Remediation**: Set 'manifest.source' to either "remote" or "local" in .slack/config.json -Read about manifest sourcing with the `slack-cli manifest info --help` command - ---- - -### project_file_update_error {#project_file_update_error} - -**Message**: Failed to update project files - ---- - -### prompt_error {#prompt_error} - -**Message**: An error occurred while executing prompts - ---- - -### provider_not_found {#provider_not_found} - -**Message**: The provided provider_key is invalid - ---- - -### published_app_only {#published_app_only} - -**Message**: This action is only permitted for published app IDs - ---- - -### ratelimited {#ratelimited} - -**Message**: Too many calls in succession during a short period of time - ---- - -### request_id_or_app_id_is_required {#request_id_or_app_id_is_required} - -**Message**: Must include a request_id or app_id - ---- - -### restricted_plan_level {#restricted_plan_level} - -**Message**: Your Slack plan does not have access to the requested feature - ---- - -### runtime_not_found {#runtime_not_found} - -**Message**: The hook runtime executable was not found - -**Remediation**: Make sure the required runtime has been installed to run hook scripts. - ---- - -### runtime_not_supported {#runtime_not_supported} - -**Message**: The SDK runtime is not supported by the CLI - ---- - -### sample_create_error {#sample_create_error} - -**Message**: Couldn't create app from sample - ---- - -### scopes_exceed_app_config {#scopes_exceed_app_config} - -**Message**: Scopes requested exceed app configuration - ---- - -### sdk_config_load_error {#sdk_config_load_error} - -**Message**: There was an error while reading the Slack hooks file (`.slack/hooks.json`) or running the `get-hooks` hook - -**Remediation**: Run `slack-cli doctor` to check that your system dependencies are up-to-date. - ---- - -### sdk_hook_invocation_failed {#sdk_hook_invocation_failed} - -**Message**: A script hook defined in the Slack Configuration file (`.slack/hooks.json`) returned an error - -**Remediation**: Run `slack-cli doctor` to check that your system dependencies are up-to-date. - ---- - -### sdk_hook_not_found {#sdk_hook_not_found} - -**Message**: A script in .slack/hooks.json was not found - -**Remediation**: Hook scripts are defined in one of these Slack hooks files: -- slack.json -- .slack/hooks.json - -Every app requires a Slack hooks file and you can find an example at: -https://github.com/slack-samples/deno-starter-template/blob/main/.slack/hooks.json - -You can create a hooks file manually or with the `slack-cli init` command. - -When manually creating the hooks file, you must install the hook dependencies. - ---- - -### service_limits_exceeded {#service_limits_exceeded} - -**Message**: Your workspace has exhausted the 10 apps limit for free teams. To create more apps, upgrade your Slack plan at https://my.slack.com/plans - ---- - -### shared_channel_denied {#shared_channel_denied} - -**Message**: The team admin does not allow shared channels to be named_entities - ---- - -### slack_auth_error {#slack_auth_error} - -**Message**: You are not logged into a team or have not installed an app - -**Remediation**: Use the command `slack-cli login` to login and `slack-cli install` to install your app - ---- - -### slack_json_location_error {#slack_json_location_error} - -**Message**: The slack.json configuration file is deprecated - -**Remediation**: Next major version of the CLI will no longer support this configuration file. -Move the slack.json file to .slack/hooks.json and continue onwards. - ---- - -### slack_slack_json_location_error {#slack_slack_json_location_error} - -**Message**: The .slack/slack.json configuration file is deprecated - -**Remediation**: Next major version of the CLI will no longer support this configuration file. -Move the .slack/slack.json file to .slack/hooks.json and proceed again. - ---- - -### socket_connection_error {#socket_connection_error} - -**Message**: Couldn't connect to Slack over WebSocket - ---- - -### streaming_activity_logs_error {#streaming_activity_logs_error} - -**Message**: Failed to stream the most recent activity logs - ---- - -### subdir_not_found {#subdir_not_found} - -**Message**: The specified subdirectory was not found in the template repository - ---- - -### survey_config_not_found {#survey_config_not_found} - -**Message**: Survey config not found - ---- - -### system_config_id_not_found {#system_config_id_not_found} - -**Message**: The "system_id" property is missing from the system-level configuration file - ---- - -### system_requirements_failed {#system_requirements_failed} - -**Message**: Couldn't verify all system requirements - ---- - -### team_access_not_granted {#team_access_not_granted} - -**Message**: There was an issue granting access to the team - ---- - -### team_flag_required {#team_flag_required} - -**Message**: The --team flag must be provided - -**Remediation**: Choose a specific team with `--team ` or `--team ` - ---- - -### team_list_error {#team_list_error} - -**Message**: Couldn't get a list of teams - ---- - -### team_not_connected {#team_not_connected} - -**Message**: One or more of the listed teams was not connected by org - ---- - -### team_not_found {#team_not_found} - -**Message**: Team could not be found - ---- - -### team_not_on_enterprise {#team_not_on_enterprise} - -**Message**: Cannot query team by domain because team is not on an enterprise - ---- - -### team_quota_exceeded {#team_quota_exceeded} - -**Message**: Total number of requests exceeded team quota - ---- - -### template_path_not_found {#template_path_not_found} - -**Message**: No template app was found at the provided path - ---- - -### token_expired {#token_expired} - -**Message**: Your access token has expired - -**Remediation**: Use the command `slack-cli login` to authenticate again - ---- - -### token_revoked {#token_revoked} - -**Message**: Your token has already been revoked - -**Remediation**: Use the command `slack-cli login` to authenticate again - ---- - -### token_rotation_error {#token_rotation_error} - -**Message**: An error occurred while rotating your access token - -**Remediation**: Use the command `slack-cli login` to authenticate again - ---- - -### too_many_customizable_inputs {#too_many_customizable_inputs} - -**Message**: Cannot have more than 10 customizable inputs - ---- - -### too_many_ids_provided {#too_many_ids_provided} - -**Message**: Ensure you provide only app_id OR request_id - ---- - -### too_many_named_entities {#too_many_named_entities} - -**Message**: Too many named entities passed into the trigger permissions setting - ---- - -### trigger_create_error {#trigger_create_error} - -**Message**: Couldn't create a trigger - ---- - -### trigger_delete_error {#trigger_delete_error} - -**Message**: Couldn't delete a trigger - ---- - -### trigger_does_not_exist {#trigger_does_not_exist} - -**Message**: The trigger provided does not exist - ---- - -### trigger_not_found {#trigger_not_found} - -**Message**: The specified trigger cannot be found - ---- - -### trigger_update_error {#trigger_update_error} - -**Message**: Couldn't update a trigger - ---- - -### unable_to_delete {#unable_to_delete} - -**Message**: There was an error deleting tokens - ---- - -### unable_to_open_file {#unable_to_open_file} - -**Message**: Error with file upload - ---- - -### unable_to_parse_json {#unable_to_parse_json} - -**Message**: `` Couldn't be parsed as a json object - ---- - -### uninstall_halted {#uninstall_halted} - -**Message**: The uninstall process was interrupted - ---- - -### unknown_file_type {#unknown_file_type} - -**Message**: Unknown file type, must be application/zip - ---- - -### unknown_function_id {#unknown_function_id} - -**Message**: The provided function_id was not found - ---- - -### unknown_method {#unknown_method} - -**Message**: The Slack API method does not exist or you do not have permissions to access it - ---- - -### unknown_webhook_schema_ref {#unknown_webhook_schema_ref} - -**Message**: Unable to find the corresponding type based on the schema ref - ---- - -### unknown_workflow_id {#unknown_workflow_id} - -**Message**: The provided workflow_id was not found for this app - ---- - -### unsupported_file_name {#unsupported_file_name} - -**Message**: File name is not supported - ---- - -### untrusted_source {#untrusted_source} - -**Message**: Source is by an unknown or untrusted author - -**Remediation**: Use --force flag or set trust_unknown_sources: true in config.json file to disable warning - ---- - -### user_already_owner {#user_already_owner} - -**Message**: The user is already an owner for this app - ---- - -### user_already_requested {#user_already_requested} - -**Message**: The user has a request pending for this app - ---- - -### user_cannot_manage_app {#user_cannot_manage_app} - -**Message**: You do not have permissions to install this app - -**Remediation**: Reach out to one of your App Managers to request permissions to install apps. - ---- - -### user_id_is_required {#user_id_is_required} - -**Message**: Must include a user_id to cancel request for an app with app_id - ---- - -### user_not_found {#user_not_found} - -**Message**: User cannot be found - ---- - -### user_removed_from_team {#user_removed_from_team} - -**Message**: User removed from team (generated) - ---- - -### workflow_not_found {#workflow_not_found} - -**Message**: Workflow not found - ---- - -### yaml_error {#yaml_error} - -**Message**: An error occurred while parsing the app manifest YAML file - ---- - -## Additional help - -These error codes might reference an error you've encountered, but not provide -enough details for a workaround. - -For more help, post to our issue tracker: https://github.com/slackapi/slack-cli/issues From 7a1eacdc6123a0c34d1113d785cbf42d35b43b5f Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 3 Apr 2026 12:22:56 -0700 Subject: [PATCH 23/24] refactor for test --- internal/api/docs.go | 31 ++++++++++++---- internal/api/docs_test.go | 77 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+), 7 deletions(-) diff --git a/internal/api/docs.go b/internal/api/docs.go index c4fea45a..25d0b214 100644 --- a/internal/api/docs.go +++ b/internal/api/docs.go @@ -45,30 +45,47 @@ type DocsSearchItem struct { Title string `json:"title"` } +func buildDocsSearchURL(baseURL, query string, limit int) (string, error) { + endpoint := fmt.Sprintf("%s?query=%s&limit=%d", docsSearchMethod, url.QueryEscape(query), limit) + sURL, err := url.Parse(baseURL + "/" + endpoint) + if err != nil { + return "", err + } + return sURL.String(), nil +} + +func buildDocsSearchRequest(ctx context.Context, urlStr, cliVersion string) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil) + if err != nil { + return nil, err + } + req.Header.Add("User-Agent", fmt.Sprintf("slack-cli/%s (os: %s)", cliVersion, runtime.GOOS)) + return req, nil +} + // DocsSearch searches the Slack developer docs API func (c *Client) DocsSearch(ctx context.Context, query string, limit int) (*DocsSearchResponse, error) { var span opentracing.Span span, _ = opentracing.StartSpanFromContext(ctx, "apiclient.DocsSearch") defer span.Finish() - endpoint := fmt.Sprintf("%s?query=%s&limit=%d", docsSearchMethod, url.QueryEscape(query), limit) - sURL, err := url.Parse(docsBaseURL + "/" + endpoint) + urlStr, err := buildDocsSearchURL(docsBaseURL, query, limit) if err != nil { return nil, errHTTPRequestFailed.WithRootCause(err) } + sURL, _ := url.Parse(urlStr) span.SetTag("request_url", sURL) - req, err := http.NewRequestWithContext(ctx, "GET", sURL.String(), nil) + cliVersion, err := slackcontext.Version(ctx) if err != nil { - return nil, errHTTPRequestFailed.WithRootCause(err) + return nil, err } - cliVersion, err := slackcontext.Version(ctx) + req, err := buildDocsSearchRequest(ctx, urlStr, cliVersion) if err != nil { - return nil, err + return nil, errHTTPRequestFailed.WithRootCause(err) } - req.Header.Add("User-Agent", fmt.Sprintf("slack-cli/%s (os: %s)", cliVersion, runtime.GOOS)) c.printRequest(ctx, req, false) diff --git a/internal/api/docs_test.go b/internal/api/docs_test.go index 7f19e716..4ffbc792 100644 --- a/internal/api/docs_test.go +++ b/internal/api/docs_test.go @@ -15,6 +15,7 @@ package api import ( + "context" "testing" "github.com/slackapi/slack-cli/internal/slackcontext" @@ -22,6 +23,82 @@ import ( "github.com/stretchr/testify/require" ) +func Test_buildDocsSearchURL(t *testing.T) { + tests := map[string]struct { + baseURL string + query string + limit int + expectedURL string + expectedErrorContains string + }{ + "builds valid URL": { + baseURL: "https://docs.slack.dev", + query: "Block Kit", + limit: 20, + expectedURL: "https://docs.slack.dev/api/v1/search?query=Block+Kit&limit=20", + }, + "encodes special characters": { + baseURL: "https://docs.slack.dev", + query: "messages & webhooks", + limit: 5, + expectedURL: "https://docs.slack.dev/api/v1/search?query=messages+%26+webhooks&limit=5", + }, + "returns error for invalid base URL": { + baseURL: "ht!tp://invalid", + query: "test", + limit: 20, + expectedErrorContains: "invalid", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + url, err := buildDocsSearchURL(tc.baseURL, tc.query, tc.limit) + + if tc.expectedErrorContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErrorContains) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedURL, url) + } + }) + } +} + +func Test_buildDocsSearchRequest(t *testing.T) { + tests := map[string]struct { + url string + cliVersion string + expectedErrorContains string + }{ + "builds valid request": { + url: "https://docs.slack.dev/api/v1/search?query=test&limit=20", + cliVersion: "1.0.0", + }, + "returns error for invalid URL": { + url: "ht!tp://invalid", + cliVersion: "1.0.0", + expectedErrorContains: "invalid", + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + ctx := context.Background() + req, err := buildDocsSearchRequest(ctx, tc.url, tc.cliVersion) + + if tc.expectedErrorContains != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErrorContains) + } else { + require.NoError(t, err) + require.NotNil(t, req) + require.Equal(t, "GET", req.Method) + require.Contains(t, req.Header.Get("User-Agent"), "slack-cli/1.0.0") + } + }) + } +} + func Test_Client_DocsSearch(t *testing.T) { tests := map[string]struct { query string From f1ca5c3a3a2c4d915575a02d9732d88c2ff4aa7d Mon Sep 17 00:00:00 2001 From: Luke Russell Date: Fri, 3 Apr 2026 12:27:37 -0700 Subject: [PATCH 24/24] coverage --- cmd/docs/search_test.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/cmd/docs/search_test.go b/cmd/docs/search_test.go index 9537cbd8..1586eab5 100644 --- a/cmd/docs/search_test.go +++ b/cmd/docs/search_test.go @@ -82,6 +82,24 @@ func Test_Docs_SearchCommand(t *testing.T) { cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) }, }, + "returns JSON results with absolute URLs": { + CmdArgs: []string{"search", "test", "--output=json"}, + Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { + cm.API.On("DocsSearch", mock.Anything, "test", 20).Return(&api.DocsSearchResponse{ + TotalResults: 1, + Limit: 20, + Results: []api.DocsSearchItem{ + {Title: "Test", URL: "https://docs.slack.dev/test"}, + }, + }, nil) + }, + ExpectedStdoutOutputs: []string{ + `"url": "https://docs.slack.dev/test"`, + }, + ExpectedAsserts: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock) { + cm.IO.AssertCalled(t, "PrintTrace", mock.Anything, slacktrace.DocsSearchSuccess, mock.Anything) + }, + }, "returns empty results": { CmdArgs: []string{"search", "nonexistent"}, Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { @@ -102,6 +120,13 @@ func Test_Docs_SearchCommand(t *testing.T) { }, ExpectedErrorStrings: []string{slackerror.ErrHTTPRequestFailed}, }, + "returns error on API failure for JSON output": { + CmdArgs: []string{"search", "test", "--output=json"}, + Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) { + cm.API.On("DocsSearch", mock.Anything, "test", 20).Return(nil, slackerror.New(slackerror.ErrHTTPRequestFailed)) + }, + ExpectedErrorStrings: []string{slackerror.ErrHTTPRequestFailed}, + }, "passes custom limit": { CmdArgs: []string{"search", "test", "--limit=5"}, Setup: func(t *testing.T, ctx context.Context, cm *shared.ClientsMock, cf *shared.ClientFactory) {