Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions pkg/cmd/gpusearch/gpusearch.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,11 @@ type GPUSearchStore interface {
var (
searchLong = `Search instance types available on Brev.

Use 'brev search gpu' (default) to find GPU instances.
The search command has two subcommands:
gpu Search GPU instance types (default when no subcommand is given)
cpu Search CPU-only instance types

Use 'brev search' or 'brev search gpu' to find GPU instances.
Use 'brev search cpu' to find CPU-only instances.

Features column shows instance capabilities:
Expand Down Expand Up @@ -213,9 +217,28 @@ func NewCmdGPUSearch(t *terminal.Terminal, store GPUSearchStore) *cobra.Command
Use: "search",
Aliases: []string{},
DisableFlagsInUseLine: true,
Short: "Search and filter instance types",
Short: "Search GPU and CPU instance types",
Long: searchLong,
Example: gpuExample,
ValidArgs: []string{"cpu", "gpu"},
Args: func(cmd *cobra.Command, args []string) error {
// The parent runs GPU search by default when no positional args are
// given. Unknown positional tokens (e.g. "cpus", "CPU", "badcmd")
// do not match the cpu/gpu subcommands, so cobra falls back to the
// parent; reject them with a helpful error instead of silently
// running GPU search.
if len(args) == 0 {
return nil
}
path := cmd.CommandPath()
return breverrors.NewValidationError(fmt.Sprintf(
"unknown subcommand %q for %q.\n"+
"Available subcommands: cpu, gpu.\n"+
"Use %q (default) or %q gpu for GPU instances; use %q cpu for CPU-only instances.\n"+
"Run %q --help for more details.",
args[0], path, path, path, path, path,
))
},
RunE: func(cmd *cobra.Command, args []string) error {
// Default behavior: GPU search
return RunGPUSearch(t, store, gpuName, shared.provider, shared.arch, minVRAM, minTotalVRAM, minCapability, shared.minRAM, shared.minDisk, shared.minVCPU, shared.maxBootTime, shared.stoppable, shared.rebootable, shared.flexPorts, shared.sortBy, shared.descending, shared.jsonOutput, wide)
Expand Down
80 changes: 80 additions & 0 deletions pkg/cmd/gpusearch/gpusearch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gpusearch
import (
"testing"

"github.com/brevdev/brev-cli/pkg/terminal"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -625,3 +626,82 @@ func TestAllInstanceTypesResponseLookup(t *testing.T) {
assert.False(t, resp.HasInstanceType(""))
})
}

// trackingStore is a GPUSearchStore mock that records the includeCPU flag passed to
// GetInstanceTypes so command dispatch tests can assert which search path executed.
// It returns an empty (but non-nil) response by default so RunGPUSearch/RunCPUSearch
// hit the empty-results path and return nil without rendering a table.
type trackingStore struct {
Response *InstanceTypesResponse
Err error
calls []bool
}

func (m *trackingStore) GetInstanceTypes(includeCPU bool) (*InstanceTypesResponse, error) {
m.calls = append(m.calls, includeCPU)
if m.Err != nil {
return nil, m.Err
}
if m.Response != nil {
return m.Response, nil
}
return &InstanceTypesResponse{Items: []InstanceType{}}, nil
}

func TestSearchCommandDefaultsToGPUSearchWhenNoArgs(t *testing.T) {
store := &trackingStore{}
cmd := NewCmdGPUSearch(terminal.New(), store)
cmd.SetArgs([]string{})

err := cmd.Execute()
assert.NoError(t, err)
assert.Len(t, store.calls, 1, "default search should call the store once")
assert.False(t, store.calls[0], "default search should request GPU instances (includeCPU=false)")
}

func TestSearchCommandGPUSubcommandDispatchesToGPUSearch(t *testing.T) {
store := &trackingStore{}
cmd := NewCmdGPUSearch(terminal.New(), store)
cmd.SetArgs([]string{"gpu"})

err := cmd.Execute()
assert.NoError(t, err)
assert.Len(t, store.calls, 1, "gpu subcommand should call the store once")
assert.False(t, store.calls[0], "gpu subcommand should request GPU instances (includeCPU=false)")
}

func TestSearchCommandCPUSubcommandDispatchesToCPUSearch(t *testing.T) {
store := &trackingStore{}
cmd := NewCmdGPUSearch(terminal.New(), store)
cmd.SetArgs([]string{"cpu"})

err := cmd.Execute()
assert.NoError(t, err)
assert.Len(t, store.calls, 1, "cpu subcommand should call the store once")
assert.True(t, store.calls[0], "cpu subcommand should request CPU instances (includeCPU=true)")
}

func TestSearchCommandRejectsUnknownPositionalTokens(t *testing.T) {
tests := []struct {
name string
args []string
}{
{"plural cpus", []string{"cpus"}},
{"uppercase CPU", []string{"CPU"}},
{"uppercase GPU", []string{"GPU"}},
{"arbitrary badcmd", []string{"badcmd"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := &trackingStore{}
cmd := NewCmdGPUSearch(terminal.New(), store)
cmd.SetArgs(tt.args)

err := cmd.Execute()
assert.Error(t, err)
assert.ErrorContains(t, err, "unknown subcommand")
assert.ErrorContains(t, err, "Available subcommands: cpu, gpu")
assert.Empty(t, store.calls, "unknown subcommand should not call the store")
})
}
}
Loading