From e309c00aa60daf9526ba2c9142deb05ea85f875f Mon Sep 17 00:00:00 2001 From: dheeraj12347 Date: Mon, 8 Jun 2026 19:55:24 +0000 Subject: [PATCH 1/3] feat: use Related metadata for autocomplete API discovery --- cli/completer.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/cli/completer.go b/cli/completer.go index 6db3d30..9ea953c 100644 --- a/cli/completer.go +++ b/cli/completer.go @@ -269,6 +269,33 @@ func findAutocompleteAPI(arg *config.APIArg, apiFound *config.API, apiMap map[st relatedNoun = pluralizeNoun(base) } + // Prefer authoritative Related metadata: a list API whose noun matches + // the related noun derived above. + for _, relatedAPI := range arg.Related { + if !strings.HasPrefix(strings.ToLower(relatedAPI), "list") { + continue + } + for _, listAPI := range apiMap["list"] { + if strings.EqualFold(listAPI.Name, relatedAPI) && strings.EqualFold(listAPI.Noun, relatedNoun) { + config.Debug("Autocomplete: API found using Related metadata: ", listAPI.Name) + return listAPI + } + } + } + + // Fall back to any list API named in the Related metadata. + for _, relatedAPI := range arg.Related { + if !strings.HasPrefix(strings.ToLower(relatedAPI), "list") { + continue + } + for _, listAPI := range apiMap["list"] { + if strings.EqualFold(listAPI.Name, relatedAPI) { + config.Debug("Autocomplete: API found using Related metadata fallback: ", listAPI.Name) + return listAPI + } + } + } + config.Debug("Possible related noun for the arg: ", relatedNoun, " and type: ", arg.Type) autocompleteAPI = findAPI(apiMap, relatedNoun) From 4513eca1c50b91b0d3c67bcc2aafa7d9d4c310ee Mon Sep 17 00:00:00 2001 From: dheeraj12347 Date: Fri, 19 Jun 2026 08:45:10 +0000 Subject: [PATCH 2/3] test: add autocomplete API discovery coverage --- cli/completer_test.go | 165 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 cli/completer_test.go diff --git a/cli/completer_test.go b/cli/completer_test.go new file mode 100644 index 0000000..c404e66 --- /dev/null +++ b/cli/completer_test.go @@ -0,0 +1,165 @@ +package cli + +import ( + "testing" + + "github.com/apache/cloudstack-cloudmonkey/config" +) + +func TestFindAutocompleteAPIRelatedNounMatch(t *testing.T) { + arg := &config.APIArg{ + Name: "domainid=", + Related: []string{ + "createDomain", + "listDomains", + "updateDomain", + }, + } + + apiFound := &config.API{ + Name: "listVirtualMachines", + Verb: "list", + Noun: "virtualmachines", + } + + apiMap := map[string][]*config.API{ + "list": { + { + Name: "listDomains", + Noun: "domains", + }, + }, + } + + result := findAutocompleteAPI(arg, apiFound, apiMap) + + if result == nil { + t.Fatal("expected API, got nil") + } + + if result.Name != "listDomains" { + t.Fatalf("expected listDomains, got %s", result.Name) + } +} + +func TestFindAutocompleteAPIRelatedFallback(t *testing.T) { + arg := &config.APIArg{ + Name: "domainid=", + Related: []string{ + "listDomainChildren", + }, + } + + apiFound := &config.API{ + Name: "listVirtualMachines", + Verb: "list", + Noun: "virtualmachines", + } + + apiMap := map[string][]*config.API{ + "list": { + { + Name: "listDomainChildren", + Noun: "domainchildren", + }, + }, + } + + result := findAutocompleteAPI(arg, apiFound, apiMap) + + if result == nil { + t.Fatal("expected API, got nil") + } + + if result.Name != "listDomainChildren" { + t.Fatalf("expected listDomainChildren, got %s", result.Name) + } +} + +func TestFindAutocompleteAPIEmptyRelatedFallsBackToHeuristic(t *testing.T) { + arg := &config.APIArg{ + Name: "zoneid=", + } + + apiFound := &config.API{ + Name: "listVirtualMachines", + Verb: "list", + Noun: "virtualmachines", + } + + apiMap := map[string][]*config.API{ + "list": { + { + Name: "listZones", + Noun: "zones", + }, + }, + } + + result := findAutocompleteAPI(arg, apiFound, apiMap) + + if result == nil { + t.Fatal("expected API, got nil") + } + + if result.Name != "listZones" { + t.Fatalf("expected listZones, got %s", result.Name) + } +} + +func TestFindAutocompleteAPINonListRelatedFallsBackToHeuristic(t *testing.T) { + arg := &config.APIArg{ + Name: "zoneid=", + Related: []string{ + "createZone", + "updateZone", + }, + } + + apiFound := &config.API{ + Name: "listVirtualMachines", + Verb: "list", + Noun: "virtualmachines", + } + + apiMap := map[string][]*config.API{ + "list": { + { + Name: "listZones", + Noun: "zones", + }, + }, + } + + result := findAutocompleteAPI(arg, apiFound, apiMap) + + if result == nil { + t.Fatal("expected API, got nil") + } + + if result.Name != "listZones" { + t.Fatalf("expected listZones, got %s", result.Name) + } +} + +func TestFindAutocompleteAPIMapTypeReturnsNil(t *testing.T) { + arg := &config.APIArg{ + Type: "map", + } + + apiFound := &config.API{ + Name: "listVirtualMachines", + Verb: "list", + Noun: "virtualmachines", + } + + apiMap := map[string][]*config.API{ + "list": {}, + } + + result := findAutocompleteAPI(arg, apiFound, apiMap) + + if result != nil { + t.Fatalf("expected nil, got %v", result) + } +} From 622e83b1aaecd1a34fd5b4b2e5d1fa5132b9685a Mon Sep 17 00:00:00 2001 From: Boris Stoyanov Date: Wed, 9 Sep 2026 09:50:48 +0300 Subject: [PATCH 3/3] Use Related metadata only when the noun heuristics find nothing Checking Related before the heuristics changed 47 existing completions against the bundled API cache, many of them wrong: registerIso projectid resolved to listProjectAccounts instead of listProjects, acquirePodIpAddress podid to listZones instead of listPods, and executeWebhookDelivery webhookid to listWebhookDeliveries instead of listWebhooks. The loose Related match picks the first list API in the array, whatever its noun. Move the Related lookup after the noun heuristics and run it only when they found no API. That keeps all 142 new completions Related metadata adds and leaves every existing completion untouched. Add a test pinning the ordering. --- cli/completer.go | 43 ++++++++++++++++--------------------------- cli/completer_test.go | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 27 deletions(-) diff --git a/cli/completer.go b/cli/completer.go index 9ea953c..73155db 100644 --- a/cli/completer.go +++ b/cli/completer.go @@ -269,33 +269,6 @@ func findAutocompleteAPI(arg *config.APIArg, apiFound *config.API, apiMap map[st relatedNoun = pluralizeNoun(base) } - // Prefer authoritative Related metadata: a list API whose noun matches - // the related noun derived above. - for _, relatedAPI := range arg.Related { - if !strings.HasPrefix(strings.ToLower(relatedAPI), "list") { - continue - } - for _, listAPI := range apiMap["list"] { - if strings.EqualFold(listAPI.Name, relatedAPI) && strings.EqualFold(listAPI.Noun, relatedNoun) { - config.Debug("Autocomplete: API found using Related metadata: ", listAPI.Name) - return listAPI - } - } - } - - // Fall back to any list API named in the Related metadata. - for _, relatedAPI := range arg.Related { - if !strings.HasPrefix(strings.ToLower(relatedAPI), "list") { - continue - } - for _, listAPI := range apiMap["list"] { - if strings.EqualFold(listAPI.Name, relatedAPI) { - config.Debug("Autocomplete: API found using Related metadata fallback: ", listAPI.Name) - return listAPI - } - } - } - config.Debug("Possible related noun for the arg: ", relatedNoun, " and type: ", arg.Type) autocompleteAPI = findAPI(apiMap, relatedNoun) @@ -314,6 +287,22 @@ func findAutocompleteAPI(arg *config.APIArg, apiFound *config.API, apiMap map[st relatedNoun = relatedNoun[:len(relatedNoun)-1] } + // Prefer the API's own Related metadata when the noun heuristics found + // nothing, so entity-reference args still get completions. + if autocompleteAPI == nil { + for _, relatedAPI := range arg.Related { + if !strings.HasPrefix(strings.ToLower(relatedAPI), "list") { + continue + } + for _, listAPI := range apiMap["list"] { + if strings.EqualFold(listAPI.Name, relatedAPI) { + config.Debug("Autocomplete: API found using Related metadata: ", listAPI.Name) + return listAPI + } + } + } + } + // Heuristic: find any list API that contains the arg name if autocompleteAPI == nil { config.Debug("Finding possible API that have: ", argName, " related APIs: ", arg.Related) diff --git a/cli/completer_test.go b/cli/completer_test.go index c404e66..aea7836 100644 --- a/cli/completer_test.go +++ b/cli/completer_test.go @@ -163,3 +163,44 @@ func TestFindAutocompleteAPIMapTypeReturnsNil(t *testing.T) { t.Fatalf("expected nil, got %v", result) } } + +func TestFindAutocompleteAPIHeuristicWinsOverRelated(t *testing.T) { + // registerIso's projectid arg lists many related APIs; the noun heuristic + // must keep winning so the completion stays listProjects. + arg := &config.APIArg{ + Name: "projectid=", + Related: []string{ + "listProjectAccounts", + "listProjects", + }, + } + + apiFound := &config.API{ + Name: "registerIso", + Verb: "register", + Noun: "iso", + } + + apiMap := map[string][]*config.API{ + "list": { + { + Name: "listProjectAccounts", + Noun: "projectaccounts", + }, + { + Name: "listProjects", + Noun: "projects", + }, + }, + } + + result := findAutocompleteAPI(arg, apiFound, apiMap) + + if result == nil { + t.Fatal("expected API, got nil") + } + + if result.Name != "listProjects" { + t.Fatalf("expected listProjects, got %s", result.Name) + } +}