Skip to content

Commit 24d5351

Browse files
fixup: address Simon's review
- Restore the pre-refactor validation that combining --project and --global on `list` is an error, not a silent equivalent of --scope=both. Moved the check into parseScopeFlag itself so it applies uniformly across commands. - For commands that pass allowBoth=false (install, uninstall), the invalid-value error no longer mentions "both" as a valid option. - Added TestUpdateScopeFlag and TestUninstallScopeFlag covering the new Cobra wiring (--scope=global/both, invalid value, legacy-flag conflict, and both-deprecated-flags-together cases). Introduced updateSkillsFn and uninstallSkillsFn package-level test-injection vars mirroring the existing installSkillsForAgentsFn pattern in install.go. Co-authored-by: Isaac
1 parent 941d8f7 commit 24d5351

7 files changed

Lines changed: 175 additions & 7 deletions

File tree

cmd/aitools/list_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func TestListScopeFlag(t *testing.T) {
5757
{name: "scope global", args: []string{"--scope", "global"}, wantScope: installer.ScopeGlobal},
5858
{name: "scope both shows both", args: []string{"--scope", "both"}, wantScope: ""},
5959
{name: "scope invalid", args: []string{"--scope", "all"}, wantErr: `invalid --scope "all"`},
60-
{name: "legacy both flags shows both", args: []string{"--project", "--global"}, wantScope: ""},
60+
{name: "legacy both flags together rejected", args: []string{"--project", "--global"}, wantErr: "cannot use --global and --project together"},
6161
}
6262

6363
for _, tt := range tests {

cmd/aitools/scope.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,18 @@ func markScopeBoolsDeprecated(cmd *cobra.Command) {
9797
// parseScopeFlag translates --scope into the equivalent --project/--global bool pair.
9898
// Returns (projectFlag, globalFlag, nil) unchanged when --scope is empty so the
9999
// deprecated booleans can keep flowing through the existing resolveScope* helpers.
100-
// Errors if --scope is combined with --project or --global. When allowBoth is
100+
// Errors if --scope is combined with --project or --global, or if both deprecated
101+
// flags are set together (matching the pre-refactor validation). When allowBoth is
101102
// false, --scope=both is rejected up front so install and uninstall don't have
102103
// to special-case it.
103104
func parseScopeFlag(scopeFlag string, projectFlag, globalFlag, allowBoth bool) (proj, glob bool, err error) {
104105
if scopeFlag == "" {
106+
// Preserve the pre-refactor behavior: combining the two deprecated flags
107+
// is always wrong, regardless of allowBoth. Users who want both scopes
108+
// should use --scope=both (where supported).
109+
if projectFlag && globalFlag {
110+
return false, false, errors.New("cannot use --global and --project together")
111+
}
105112
return projectFlag, globalFlag, nil
106113
}
107114
if projectFlag || globalFlag {
@@ -118,7 +125,10 @@ func parseScopeFlag(scopeFlag string, projectFlag, globalFlag, allowBoth bool) (
118125
}
119126
return true, true, nil
120127
default:
121-
return false, false, fmt.Errorf("invalid --scope %q: must be one of project, global, both", scopeFlag)
128+
if allowBoth {
129+
return false, false, fmt.Errorf("invalid --scope %q: must be one of project, global, both", scopeFlag)
130+
}
131+
return false, false, fmt.Errorf("invalid --scope %q: must be one of project, global", scopeFlag)
122132
}
123133
}
124134

cmd/aitools/scope_test.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,13 @@ func TestParseScopeFlag(t *testing.T) {
8383
{name: "unset", scope: ""},
8484
{name: "legacy project only", project: true, wantProj: true},
8585
{name: "legacy global only", global: true, wantGlob: true},
86-
{name: "legacy both passthrough", project: true, global: true, wantProj: true, wantGlob: true},
86+
{name: "legacy both flags together rejected", project: true, global: true, wantErr: "cannot use --global and --project together"},
8787
{name: "scope project", scope: "project", wantProj: true},
8888
{name: "scope global", scope: "global", wantGlob: true},
8989
{name: "scope both allowed", scope: "both", allowBoth: true, wantProj: true, wantGlob: true},
9090
{name: "scope both disallowed", scope: "both", wantErr: "--scope=both is not supported"},
91-
{name: "scope invalid value", scope: "all", wantErr: `invalid --scope "all"`},
91+
{name: "scope invalid value with allowBoth", scope: "all", allowBoth: true, wantErr: `invalid --scope "all": must be one of project, global, both`},
92+
{name: "scope invalid value without allowBoth omits both from error", scope: "all", wantErr: `invalid --scope "all": must be one of project, global`},
9293
{name: "scope conflicts with project", scope: "project", project: true, wantErr: "cannot use --scope with --project or --global"},
9394
{name: "scope conflicts with global", scope: "global", global: true, wantErr: "cannot use --scope with --project or --global"},
9495
}
@@ -106,6 +107,15 @@ func TestParseScopeFlag(t *testing.T) {
106107
assert.Equal(t, tt.wantGlob, glob)
107108
})
108109
}
110+
111+
// Stronger check that the without-allowBoth invalid-value branch omits
112+
// "both" from the error message (the table assertion uses Contains which
113+
// can't distinguish a substring shared with the allowBoth variant).
114+
t.Run("invalid scope error message without allowBoth does not mention both", func(t *testing.T) {
115+
_, _, err := parseScopeFlag("all", false, false, false)
116+
require.Error(t, err)
117+
assert.NotContains(t, err.Error(), "both")
118+
})
109119
}
110120

111121
// --- detectInstalledScopes tests (table-driven) ---

cmd/aitools/uninstall.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
package aitools
22

33
import (
4+
"context"
5+
46
"github.com/databricks/cli/libs/aitools/installer"
57
"github.com/spf13/cobra"
68
)
79

10+
// Package-level for testability. Tests override via uninstall_test.go.
11+
var uninstallSkillsFn = func(ctx context.Context, opts installer.UninstallOptions) error {
12+
return installer.UninstallSkillsOpts(ctx, opts)
13+
}
14+
815
func NewUninstallCmd() *cobra.Command {
916
var skillsFlag, scopeFlag string
1017
var projectFlag, globalFlag bool
@@ -42,7 +49,7 @@ By default, removes all skills. Use --skills to remove specific skills only.`,
4249
Scope: scope,
4350
}
4451
opts.Skills = splitAndTrim(skillsFlag)
45-
return installer.UninstallSkillsOpts(ctx, opts)
52+
return uninstallSkillsFn(ctx, opts)
4653
},
4754
}
4855

cmd/aitools/uninstall_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package aitools
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/databricks/cli/libs/aitools/installer"
8+
"github.com/databricks/cli/libs/cmdio"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func setupUninstallMock(t *testing.T) *[]installer.UninstallOptions {
14+
t.Helper()
15+
orig := uninstallSkillsFn
16+
t.Cleanup(func() { uninstallSkillsFn = orig })
17+
18+
var calls []installer.UninstallOptions
19+
uninstallSkillsFn = func(_ context.Context, opts installer.UninstallOptions) error {
20+
calls = append(calls, opts)
21+
return nil
22+
}
23+
return &calls
24+
}
25+
26+
func TestUninstallScopeFlag(t *testing.T) {
27+
tests := []struct {
28+
name string
29+
args []string
30+
wantScope string
31+
wantErr string
32+
}{
33+
// scope=project requires installed project state and is covered via TestParseScopeFlag
34+
// and TestResolveScopeForUninstallProjectFlagWithState. Here we cover the no-state paths
35+
// and the failure modes specific to the Cobra wiring.
36+
{name: "scope global", args: []string{"--scope", "global"}, wantScope: installer.ScopeGlobal},
37+
{name: "scope both rejected", args: []string{"--scope", "both"}, wantErr: "--scope=both is not supported"},
38+
{name: "scope invalid value", args: []string{"--scope", "all"}, wantErr: `invalid --scope "all"`},
39+
{name: "scope conflicts with legacy project", args: []string{"--scope", "global", "--project"}, wantErr: "cannot use --scope with --project or --global"},
40+
{name: "legacy both flags together rejected", args: []string{"--project", "--global"}, wantErr: "cannot use --global and --project together"},
41+
}
42+
43+
for _, tt := range tests {
44+
t.Run(tt.name, func(t *testing.T) {
45+
setupTestAgents(t)
46+
calls := setupUninstallMock(t)
47+
48+
ctx := cmdio.MockDiscard(t.Context())
49+
cmd := NewUninstallCmd()
50+
cmd.SetContext(ctx)
51+
cmd.SetArgs(tt.args)
52+
cmd.SilenceErrors = true
53+
cmd.SilenceUsage = true
54+
55+
err := cmd.Execute()
56+
if tt.wantErr != "" {
57+
require.Error(t, err)
58+
assert.Contains(t, err.Error(), tt.wantErr)
59+
return
60+
}
61+
require.NoError(t, err)
62+
require.Len(t, *calls, 1)
63+
assert.Equal(t, tt.wantScope, (*calls)[0].Scope)
64+
})
65+
}
66+
}

cmd/aitools/update.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package aitools
22

33
import (
4+
"context"
45
"fmt"
56

67
"github.com/databricks/cli/libs/aitools/agents"
@@ -9,6 +10,11 @@ import (
910
"github.com/spf13/cobra"
1011
)
1112

13+
// Package-level for testability. Tests override via update_test.go.
14+
var updateSkillsFn = func(ctx context.Context, src installer.ManifestSource, installed []*agents.Agent, opts installer.UpdateOptions) (*installer.UpdateResult, error) {
15+
return installer.UpdateSkills(ctx, src, installed, opts)
16+
}
17+
1218
func NewUpdateCmd() *cobra.Command {
1319
var check, force, noNew bool
1420
var skillsFlag, scopeFlag string
@@ -62,7 +68,7 @@ preview what would change without downloading.`,
6268
}
6369
opts.Skills = skills
6470

65-
result, err := installer.UpdateSkills(ctx, src, installed, opts)
71+
result, err := updateSkillsFn(ctx, src, installed, opts)
6672
if err != nil {
6773
return err
6874
}

cmd/aitools/update_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package aitools
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/databricks/cli/libs/aitools/agents"
8+
"github.com/databricks/cli/libs/aitools/installer"
9+
"github.com/databricks/cli/libs/cmdio"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func setupUpdateMock(t *testing.T) *[]installer.UpdateOptions {
15+
t.Helper()
16+
orig := updateSkillsFn
17+
t.Cleanup(func() { updateSkillsFn = orig })
18+
19+
var calls []installer.UpdateOptions
20+
updateSkillsFn = func(_ context.Context, _ installer.ManifestSource, _ []*agents.Agent, opts installer.UpdateOptions) (*installer.UpdateResult, error) {
21+
calls = append(calls, opts)
22+
return &installer.UpdateResult{}, nil
23+
}
24+
return &calls
25+
}
26+
27+
func TestUpdateScopeFlag(t *testing.T) {
28+
tests := []struct {
29+
name string
30+
args []string
31+
wantScopes []string
32+
wantErr string
33+
}{
34+
// scope=project requires installed project state and is covered via TestParseScopeFlag
35+
// (cmd/aitools/scope_test.go) and TestResolveScopeForUpdateProjectFlagWithState. Here
36+
// we cover the no-state paths and the failure modes specific to the Cobra wiring.
37+
{name: "scope global", args: []string{"--scope", "global"}, wantScopes: []string{installer.ScopeGlobal}},
38+
{name: "scope both with no installs falls through to global", args: []string{"--scope", "both"}, wantScopes: []string{installer.ScopeGlobal}},
39+
{name: "scope invalid value", args: []string{"--scope", "all"}, wantErr: `invalid --scope "all"`},
40+
{name: "scope conflicts with legacy project", args: []string{"--scope", "global", "--project"}, wantErr: "cannot use --scope with --project or --global"},
41+
{name: "legacy both flags together rejected", args: []string{"--project", "--global"}, wantErr: "cannot use --global and --project together"},
42+
}
43+
44+
for _, tt := range tests {
45+
t.Run(tt.name, func(t *testing.T) {
46+
setupTestAgents(t)
47+
calls := setupUpdateMock(t)
48+
49+
ctx := cmdio.MockDiscard(t.Context())
50+
cmd := NewUpdateCmd()
51+
cmd.SetContext(ctx)
52+
cmd.SetArgs(tt.args)
53+
cmd.SilenceErrors = true
54+
cmd.SilenceUsage = true
55+
56+
err := cmd.Execute()
57+
if tt.wantErr != "" {
58+
require.Error(t, err)
59+
assert.Contains(t, err.Error(), tt.wantErr)
60+
return
61+
}
62+
require.NoError(t, err)
63+
require.Len(t, *calls, len(tt.wantScopes))
64+
for i, scope := range tt.wantScopes {
65+
assert.Equal(t, scope, (*calls)[i].Scope)
66+
}
67+
})
68+
}
69+
}

0 commit comments

Comments
 (0)