-
Notifications
You must be signed in to change notification settings - Fork 78
Multiple providers flag #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -564,14 +564,16 @@ Provide a specific agent ID to sync a specific provider.` | |||||
| slog.Info("Running sync command") | ||||||
| registry := factory.GetRegistry() | ||||||
|
|
||||||
| // Check if user specified a provider | ||||||
| if len(args) > 0 { | ||||||
| // Sync specific provider | ||||||
| return syncSingleProvider(registry, args[0], cmd) | ||||||
| } else { | ||||||
| // Sync all providers with activity | ||||||
| return syncAllProviders(registry, cmd) | ||||||
| providersFlag, _ := cmd.Flags().GetStringSlice("providers") | ||||||
| resolvedIDs, err := cmdpkg.ResolveProviderIDs(registry, args, providersFlag) | ||||||
| if err != nil { | ||||||
| return err | ||||||
| } | ||||||
|
|
||||||
| if len(resolvedIDs) == 1 { | ||||||
| return syncSingleProvider(registry, resolvedIDs[0], cmd) | ||||||
| } | ||||||
| return syncAllProviders(registry, cmd, resolvedIDs) | ||||||
| }, | ||||||
| } | ||||||
| } | ||||||
|
|
@@ -1024,8 +1026,9 @@ func syncProvider(provider spi.Provider, providerID string, config utils.OutputC | |||||
| return sessionCount, nil | ||||||
| } | ||||||
|
|
||||||
| // syncAllProviders syncs all providers that have activity in the current directory | ||||||
| func syncAllProviders(registry *factory.Registry, cmd *cobra.Command) error { | ||||||
| // syncAllProviders syncs all (or a filtered subset of) providers that have activity in the current directory | ||||||
| // filterIDs, if non-nil, limits which providers are synced; nil means sync all registered providers. | ||||||
|
||||||
| // filterIDs, if non-nil, limits which providers are synced; nil means sync all registered providers. | |
| // filterIDs, if non-empty, limits which providers are synced; empty or nil means sync all registered providers. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/specstoryai/getspecstory/specstory-cli/pkg/spi/factory" | ||
| ) | ||
|
|
||
| func TestResolveProviderIDs(t *testing.T) { | ||
| registry := factory.GetRegistry() | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| args []string | ||
| providersFlag []string | ||
| wantIDs []string // nil means "all providers" | ||
| wantErrSubstr string // non-empty means an error is expected containing this substring | ||
| }{ | ||
| // ── Neither specified ─────────────────────────────────────────────────── | ||
| { | ||
| name: "neither arg nor flag returns nil", | ||
| wantIDs: nil, | ||
| }, | ||
|
Comment on lines
+20
to
+24
|
||
|
|
||
| // ── Positional arg ────────────────────────────────────────────────────── | ||
| { | ||
| name: "positional arg returned as-is without validation", | ||
| args: []string{"claude"}, | ||
| wantIDs: []string{"claude"}, | ||
| }, | ||
| { | ||
| // Callers handle validation for positional args, so even unknown values | ||
| // should pass through. | ||
| name: "unknown positional arg passed through without error", | ||
| args: []string{"unknown-provider"}, | ||
| wantIDs: []string{"unknown-provider"}, | ||
| }, | ||
|
|
||
| // ── Conflict ──────────────────────────────────────────────────────────── | ||
| { | ||
| name: "positional arg and providers flag together is an error", | ||
| args: []string{"claude"}, | ||
| providersFlag: []string{"codex"}, | ||
| wantErrSubstr: "cannot use both", | ||
| }, | ||
|
|
||
| // ── --providers flag: happy paths ──────────────────────────────────────── | ||
| { | ||
| name: "single valid provider", | ||
| providersFlag: []string{"claude"}, | ||
| wantIDs: []string{"claude"}, | ||
| }, | ||
| { | ||
| name: "multiple valid providers preserves order", | ||
| providersFlag: []string{"codex", "claude"}, | ||
| wantIDs: []string{"codex", "claude"}, | ||
| }, | ||
| { | ||
| name: "mixed case is normalised to lower", | ||
| providersFlag: []string{"Claude", "CODEX"}, | ||
| wantIDs: []string{"claude", "codex"}, | ||
| }, | ||
| { | ||
| name: "leading and trailing whitespace is trimmed", | ||
| providersFlag: []string{" claude ", " codex"}, | ||
| wantIDs: []string{"claude", "codex"}, | ||
| }, | ||
|
|
||
| // ── Deduplication ──────────────────────────────────────────────────────── | ||
| { | ||
| name: "exact duplicate is removed keeping first occurrence", | ||
| providersFlag: []string{"claude", "codex", "claude"}, | ||
| wantIDs: []string{"claude", "codex"}, | ||
| }, | ||
| { | ||
| name: "case-variant duplicate is removed after normalisation", | ||
| providersFlag: []string{"Claude", "claude"}, | ||
| wantIDs: []string{"claude"}, | ||
| }, | ||
| { | ||
| name: "whitespace-variant duplicate is removed after trimming", | ||
| providersFlag: []string{"claude", " claude "}, | ||
| wantIDs: []string{"claude"}, | ||
| }, | ||
| { | ||
| name: "all duplicates collapsed to single entry", | ||
| providersFlag: []string{"gemini", "GEMINI", " gemini "}, | ||
| wantIDs: []string{"gemini"}, | ||
| }, | ||
| { | ||
| name: "three providers with one duplicate preserves remaining order", | ||
| providersFlag: []string{"cursor", "claude", "cursor", "codex"}, | ||
| wantIDs: []string{"cursor", "claude", "codex"}, | ||
| }, | ||
|
|
||
| // ── Empty / blank entries ───────────────────────────────────────────────── | ||
| { | ||
| name: "blank entries in flag slice are silently skipped", | ||
| providersFlag: []string{"", " ", "claude"}, | ||
| wantIDs: []string{"claude"}, | ||
| }, | ||
| { | ||
| name: "only blank entries is an error", | ||
| providersFlag: []string{"", " "}, | ||
| wantErrSubstr: "--providers requires at least one", | ||
| }, | ||
|
|
||
| // ── Invalid provider ID ─────────────────────────────────────────────────── | ||
| { | ||
| name: "unknown provider ID is an error", | ||
| providersFlag: []string{"notaprovider"}, | ||
| wantErrSubstr: "not a valid provider ID", | ||
| }, | ||
| { | ||
| name: "unknown provider mixed with valid is still an error", | ||
| providersFlag: []string{"claude", "notaprovider"}, | ||
| wantErrSubstr: "not a valid provider ID", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| ids, err := ResolveProviderIDs(registry, tt.args, tt.providersFlag) | ||
|
|
||
| if tt.wantErrSubstr != "" { | ||
| if err == nil { | ||
| t.Errorf("expected error containing %q, got nil", tt.wantErrSubstr) | ||
| return | ||
| } | ||
| if !strings.Contains(err.Error(), tt.wantErrSubstr) { | ||
| t.Errorf("error %q does not contain %q", err.Error(), tt.wantErrSubstr) | ||
| } | ||
| return | ||
| } | ||
|
|
||
| if err != nil { | ||
| t.Errorf("unexpected error: %v", err) | ||
| return | ||
| } | ||
|
|
||
| if len(ids) != len(tt.wantIDs) { | ||
| t.Errorf("got %v, want %v", ids, tt.wantIDs) | ||
| return | ||
| } | ||
| for i := range tt.wantIDs { | ||
| if ids[i] != tt.wantIDs[i] { | ||
| t.Errorf("ids[%d] = %q, want %q", i, ids[i], tt.wantIDs[i]) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.