Skip to content

Commit 64b114b

Browse files
authored
Merge branch 'main' into zimeg-fix-charm-chevron
2 parents c105559 + defbe8f commit 64b114b

10 files changed

Lines changed: 146 additions & 30 deletions

File tree

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ jobs:
113113
default: "dev-build"
114114
docker: # run the steps with Docker
115115
# CircleCI Go images available at: https://hub.docker.com/r/circleci/golang/
116-
- image: cimg/go:1.26.0
116+
- image: cimg/go:1.26.1
117117
steps: # steps that comprise the `build` job
118118
- checkout # check out source code to working directory
119119
- restore_cache: # restores saved cache if no changes are detected since last run

.claude/CLAUDE.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,19 @@ func NewExampleCommand(clients *shared.ClientFactory) *cobra.Command {
135135
- Mock the `ClientFactory` and its dependencies for testing
136136
- Always mock file system operations using `afero.Fs` to enable testability
137137

138+
### Test Naming Conventions
139+
140+
Test function names use the format `Test_StructName_FunctionName` for methods on a struct, or `Test_FunctionName` for package-level functions:
141+
142+
```go
143+
func Test_Client_GetAppStatus(t *testing.T) { ... } // struct method
144+
func Test_getKeyLength(t *testing.T) { ... } // package-level function
145+
```
146+
147+
### Test Ordering Conventions
148+
149+
Constructor functions (`NewXYZ`) should always be declared first, at the top of the test file. After constructors, test functions should be ordered alphabetically within each file. When a file has logical sections (separated by comments), tests should be alphabetical within each section. Getter and setter functions are grouped together under the base name — ignore the `Get` or `Set` prefix when determining order (e.g. `Test_AppName` and `Test_SetAppName` both sort under `A`). Exceptions to alphabetical ordering can be made when it doesn't work well for readability or logical grouping.
150+
138151
### Table-Driven Test Conventions
139152

140153
**Preferred: Map pattern** - uses `tc` for test case variable:

.github/MAINTAINERS_GUIDE.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,34 @@ The branch name can also be set by changing
383383
for the `build-lint-test-e2e-test` workflow in the `.circleci/config.yml` file,
384384
but take care not to merge this change into `main`!
385385
386+
#### Test naming conventions
387+
388+
Test function names should use the format `Test_StructName_FunctionName` for methods
389+
on a struct, or `Test_FunctionName` for package-level functions. The underscore after
390+
`Test` separates the Go test prefix from the identifier being tested:
391+
392+
```go
393+
// Testing a method on a struct
394+
func Test_Client_GetAppStatus(t *testing.T) { ... }
395+
396+
// Testing a package-level function
397+
func Test_getKeyLength(t *testing.T) { ... }
398+
```
399+
400+
#### Test ordering conventions
401+
402+
Constructor functions (`NewXYZ`) should always be declared first, at the top of the
403+
test file. After constructors, test functions should be ordered alphabetically. When
404+
a file has logical sections (separated by comments), tests should be alphabetical
405+
within each section.
406+
407+
Getter and setter functions should be grouped together under the base name. Ignore
408+
the `Get` or `Set` prefix when determining alphabetical order. For example,
409+
`Test_AppName` and `Test_SetAppName` are both sorted under `A` for `AppName`.
410+
411+
Exceptions to alphabetical ordering can be made when it doesn't work well for
412+
readability or logical grouping.
413+
386414
#### Contributing tests
387415

388416
If you'd like to add tests, please review our

.github/workflows/dependencies.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
steps:
4646
- name: Gather credentials
4747
id: credentials
48-
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
48+
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
4949
with:
5050
app-id: ${{ secrets.GH_APP_ID_RELEASER }}
5151
private-key: ${{ secrets.GH_APP_PRIVATE_KEY_RELEASER }}

.github/workflows/tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
- name: Set up Go
2626
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
2727
with:
28-
go-version: "1.26.0"
28+
go-version: "1.26.1"
2929
- name: Lint
3030
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
3131
with:
@@ -57,7 +57,7 @@ jobs:
5757
- name: Set up Go
5858
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
5959
with:
60-
go-version: "1.26.0"
60+
go-version: "1.26.1"
6161
- name: Report health score
6262
uses: slackapi/slack-health-score@d58a419f15cdaff97e9aa7f09f95772830ab66f7 # v0.1.1
6363
with:

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/slackapi/slack-cli
22

3-
go 1.26.0
3+
go 1.26.1
44

55
require (
66
charm.land/bubbletea/v2 v2.0.2

internal/iostreams/iostreams_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020

2121
"github.com/slackapi/slack-cli/internal/config"
2222
"github.com/slackapi/slack-cli/internal/slackdeps"
23+
"github.com/spf13/cobra"
2324
"github.com/stretchr/testify/assert"
2425
"github.com/stretchr/testify/require"
2526
)
@@ -34,6 +35,36 @@ func Test_IOSteams_NewIOStreams(t *testing.T) {
3435
require.True(t, io.config.DebugEnabled, "iostreams references config")
3536
}
3637

38+
func Test_IOStreams_ExitCode(t *testing.T) {
39+
tests := map[string]struct {
40+
setCode ExitCode
41+
expected ExitCode
42+
}{
43+
"default is ExitOK": {
44+
setCode: ExitOK,
45+
expected: ExitOK,
46+
},
47+
"set to ExitError": {
48+
setCode: ExitError,
49+
expected: ExitError,
50+
},
51+
"set to ExitCancel": {
52+
setCode: ExitCancel,
53+
expected: ExitCancel,
54+
},
55+
}
56+
for name, tc := range tests {
57+
t.Run(name, func(t *testing.T) {
58+
fsMock := slackdeps.NewFsMock()
59+
osMock := slackdeps.NewOsMock()
60+
cfg := config.NewConfig(fsMock, osMock)
61+
io := NewIOStreams(cfg, fsMock, osMock)
62+
io.SetExitCode(tc.setCode)
63+
assert.Equal(t, tc.expected, io.GetExitCode())
64+
})
65+
}
66+
}
67+
3768
func Test_IOStreams_IsTTY(t *testing.T) {
3869
tests := map[string]struct {
3970
fileInfo os.FileInfo
@@ -65,3 +96,15 @@ func Test_IOStreams_IsTTY(t *testing.T) {
6596
})
6697
}
6798
}
99+
100+
func Test_IOStreams_SetCmdIO(t *testing.T) {
101+
fsMock := slackdeps.NewFsMock()
102+
osMock := slackdeps.NewOsMock()
103+
cfg := config.NewConfig(fsMock, osMock)
104+
io := NewIOStreams(cfg, fsMock, osMock)
105+
cmd := &cobra.Command{Use: "test"}
106+
io.SetCmdIO(cmd)
107+
assert.NotNil(t, cmd.InOrStdin())
108+
assert.NotNil(t, cmd.OutOrStdout())
109+
assert.NotNil(t, cmd.ErrOrStderr())
110+
}

internal/iostreams/writer_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,24 @@ func Test_FilteredWriter(t *testing.T) {
199199
}
200200
}
201201

202+
func Test_IOStreams_WriteErr(t *testing.T) {
203+
fsMock := slackdeps.NewFsMock()
204+
osMock := slackdeps.NewOsMock()
205+
cfg := config.NewConfig(fsMock, osMock)
206+
io := NewIOStreams(cfg, fsMock, osMock)
207+
w := io.WriteErr()
208+
require.NotNil(t, w)
209+
}
210+
211+
func Test_IOStreams_WriteOut(t *testing.T) {
212+
fsMock := slackdeps.NewFsMock()
213+
osMock := slackdeps.NewOsMock()
214+
cfg := config.NewConfig(fsMock, osMock)
215+
io := NewIOStreams(cfg, fsMock, osMock)
216+
w := io.WriteOut()
217+
require.NotNil(t, w)
218+
}
219+
202220
func Test_WriteIndent(t *testing.T) {
203221
tests := map[string]struct {
204222
input string

internal/runtime/python/python.go

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,13 @@ func installRequirementsTxt(fs afero.Fs, projectDirPath string) (output string,
216216
}
217217

218218
// installPyProjectToml handles adding slack-cli-hooks to pyproject.toml
219-
func installPyProjectToml(fs afero.Fs, projectDirPath string) (output string, err error) {
219+
func installPyProjectToml(ctx context.Context, ios iostreams.IOStreamer, fs afero.Fs, projectDirPath string) (output string, err error) {
220220
pyProjectFilePath := filepath.Join(projectDirPath, "pyproject.toml")
221221

222222
file, err := afero.ReadFile(fs, pyProjectFilePath)
223223
if err != nil {
224-
return fmt.Sprintf("Error reading pyproject.toml: %s", err), err
224+
ios.PrintDebug(ctx, "Warning: could not read pyproject.toml: %s", err)
225+
return "Skipped updating pyproject.toml because file cannot be read", nil
225226
}
226227

227228
fileData := string(file)
@@ -235,25 +236,26 @@ func installPyProjectToml(fs afero.Fs, projectDirPath string) (output string, er
235236
var config map[string]interface{}
236237
err = toml.Unmarshal(file, &config)
237238
if err != nil {
238-
return fmt.Sprintf("Error parsing pyproject.toml: %s", err), err
239+
ios.PrintDebug(ctx, "Warning: could not parse pyproject.toml: %s", err)
240+
return "Skipped updating pyproject.toml because invalid TOML", nil
239241
}
240242

241243
// Verify `project` section and `project.dependencies` array exist
242244
projectSection, exists := config["project"]
243245
if !exists {
244-
err := fmt.Errorf("pyproject.toml missing project section")
245-
return fmt.Sprintf("Error updating pyproject.toml: %s", err), err
246+
ios.PrintDebug(ctx, "Warning: pyproject.toml missing [project] section")
247+
return "Skipped updating pyproject.toml because project section missing", nil
246248
}
247249

248250
projectMap, ok := projectSection.(map[string]interface{})
249251
if !ok {
250-
err := fmt.Errorf("pyproject.toml project section is not a valid format")
251-
return fmt.Sprintf("Error updating pyproject.toml: %s", err), err
252+
ios.PrintDebug(ctx, "Warning: pyproject.toml [project] section is not a valid format")
253+
return "Skipped updating pyproject.toml because project section invalid", nil
252254
}
253255

254256
if _, exists := projectMap["dependencies"]; !exists {
255-
err := fmt.Errorf("pyproject.toml missing dependencies array")
256-
return fmt.Sprintf("Error updating pyproject.toml: %s", err), err
257+
ios.PrintDebug(ctx, "Warning: pyproject.toml missing dependencies array in [project] section")
258+
return "Skipped updating pyproject.toml because dependencies missing", nil
257259
}
258260

259261
// Use string manipulation to add the dependency while preserving formatting.
@@ -264,8 +266,8 @@ func installPyProjectToml(fs afero.Fs, projectDirPath string) (output string, er
264266
matches := dependenciesRegex.FindStringSubmatch(fileData)
265267

266268
if len(matches) == 0 {
267-
err := fmt.Errorf("pyproject.toml missing dependencies array")
268-
return fmt.Sprintf("Error updating pyproject.toml: %s", err), err
269+
ios.PrintDebug(ctx, "Warning: pyproject.toml dependencies array could not be located")
270+
return "Skipped updating pyproject.toml because dependencies missing", nil
269271
}
270272

271273
prefix := matches[1] // "...dependencies = ["
@@ -347,16 +349,20 @@ func (p *Python) InstallProjectDependencies(ctx context.Context, projectDirPath
347349
// Handle requirements.txt if it exists
348350
if hasRequirementsTxt {
349351
output, err := installRequirementsTxt(fs, projectDirPath)
350-
outputs = append(outputs, output)
352+
if output != "" {
353+
outputs = append(outputs, output)
354+
}
351355
if err != nil {
352356
errs = append(errs, err)
353357
}
354358
}
355359

356360
// Handle pyproject.toml if it exists
357361
if hasPyProjectToml {
358-
output, err := installPyProjectToml(fs, projectDirPath)
359-
outputs = append(outputs, output)
362+
output, err := installPyProjectToml(ctx, ios, fs, projectDirPath)
363+
if output != "" {
364+
outputs = append(outputs, output)
365+
}
360366
if err != nil {
361367
errs = append(errs, err)
362368
}

internal/runtime/python/python_test.go

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,13 @@ dependencies = ["pytest==8.3.2"]`,
229229
}
230230
for name, tc := range tests {
231231
t.Run(name, func(t *testing.T) {
232+
ctx := slackcontext.MockContext(t.Context())
232233
fs := slackdeps.NewFsMock()
234+
os := slackdeps.NewOsMock()
235+
os.AddDefaultMocks()
236+
cfg := config.NewConfig(fs, os)
237+
ios := iostreams.NewIOStreamsMock(cfg, fs, os)
238+
ios.AddDefaultMocks()
233239
projectDirPath := "/path/to/project"
234240

235241
// Create pyproject.toml
@@ -240,7 +246,7 @@ dependencies = ["pytest==8.3.2"]`,
240246
require.NoError(t, err)
241247

242248
// Test
243-
output, err := installPyProjectToml(fs, projectDirPath)
249+
output, err := installPyProjectToml(ctx, ios, fs, projectDirPath)
244250

245251
// Assertions
246252
if tc.expectedError {
@@ -249,7 +255,9 @@ dependencies = ["pytest==8.3.2"]`,
249255
require.NoError(t, err)
250256
}
251257

252-
require.Contains(t, output, tc.expectedOutput)
258+
if tc.expectedOutput != "" {
259+
require.Contains(t, output, tc.expectedOutput)
260+
}
253261

254262
// Verify file content contains expected strings
255263
content, err := afero.ReadFile(fs, pyprojectPath)
@@ -527,29 +535,29 @@ dependencies = [
527535
expectedOutputs: []string{"Updated requirements.txt"},
528536
expectedError: false,
529537
},
530-
"Error when pyproject.toml has no dependencies array": {
538+
"Warning when pyproject.toml has no dependencies array": {
531539
existingFiles: map[string]string{
532540
"pyproject.toml": `[project]
533541
name = "my-app"`,
534542
},
535-
expectedOutputs: []string{"Error updating pyproject.toml: pyproject.toml missing dependencies array"},
536-
expectedError: true,
543+
expectedOutputs: []string{"Skipped updating pyproject.toml because dependencies missing"},
544+
expectedError: false,
537545
},
538-
"Error when pyproject.toml has no [project] section": {
546+
"Warning when pyproject.toml has no [project] section": {
539547
existingFiles: map[string]string{
540548
"pyproject.toml": `[tool.black]
541549
line-length = 88`,
542550
},
543-
expectedOutputs: []string{"Error updating pyproject.toml: pyproject.toml missing project section"},
544-
expectedError: true,
551+
expectedOutputs: []string{"Skipped updating pyproject.toml because project section missing"},
552+
expectedError: false,
545553
},
546-
"Error when pyproject.toml is invalid TOML": {
554+
"Warning when pyproject.toml is invalid TOML": {
547555
existingFiles: map[string]string{
548556
"pyproject.toml": `[project
549557
name = "broken`,
550558
},
551-
expectedOutputs: []string{"Error parsing pyproject.toml"},
552-
expectedError: true,
559+
expectedOutputs: []string{"Skipped updating pyproject.toml because invalid TOML"},
560+
expectedError: false,
553561
},
554562
}
555563
for name, tc := range tests {

0 commit comments

Comments
 (0)