From f5001c5c25c875b70319a6967878743ca7c7df07 Mon Sep 17 00:00:00 2001 From: hemarina Date: Fri, 24 Apr 2026 16:38:21 -0700 Subject: [PATCH 1/2] fix 7920 --- .../cmd/init_from_templates_helpers.go | 65 ++++++++---- .../cmd/init_from_templates_helpers_test.go | 99 ++++++++++--------- 2 files changed, 99 insertions(+), 65 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go index fb08b29e417..b4d8d2abed6 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go @@ -32,16 +32,24 @@ const ( // TemplateTypeAzd is a full azd template repository. TemplateTypeAzd = "azd" + + // templateTypeExtensionAIAgent is the discriminator value in the unified + // awesome-azd templates.json manifest that identifies an agent-init + // template. Entries with any other (or empty) templateType belong to the + // standard awesome-azd gallery and are filtered out. + templateTypeExtensionAIAgent = "extension.ai.agent" ) // AgentTemplate represents an agent template entry from the remote JSON catalog. +// Field names mirror the awesome-azd templates.json schema. type AgentTemplate struct { - Title string `json:"title"` - Description string `json:"description"` - Language string `json:"language"` - Framework string `json:"framework"` - Source string `json:"source"` - Tags []string `json:"tags"` + Title string `json:"title"` + Description string `json:"description"` + Languages []string `json:"languages"` + ExtensionFramework string `json:"extensionFramework"` + Source string `json:"source"` + Tags []string `json:"extensionTags"` + TemplateType string `json:"templateType"` } // EffectiveType determines the template type by inspecting the source URL. @@ -110,14 +118,27 @@ func dirIsEmpty(dir string) (bool, error) { return len(entries) == 0, nil } -// fetchAgentTemplates retrieves the agent template catalog from the remote JSON URL. +// fetchAgentTemplates retrieves the agent template catalog from the remote +// awesome-azd manifest URL. func fetchAgentTemplates(ctx context.Context, httpClient *http.Client) ([]AgentTemplate, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, agentTemplatesURL, nil) + return fetchAgentTemplatesFromURL(ctx, httpClient, agentTemplatesURL) +} + +// fetchAgentTemplatesFromURL retrieves the awesome-azd templates manifest from +// the given URL and returns only entries whose templateType marks them as +// agent-init templates. The URL is parameterized to keep this function +// directly testable against an httptest server. +func fetchAgentTemplatesFromURL( + ctx context.Context, + httpClient *http.Client, + url string, +) ([]AgentTemplate, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, fmt.Errorf("failed to create request: %w", err) } - //nolint:gosec // URL is the hard-coded agentTemplatesURL constant, not user input + //nolint:gosec // URL is supplied by the caller from a trusted constant or a test server resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to fetch agent templates: %w", err) @@ -133,12 +154,21 @@ func fetchAgentTemplates(ctx context.Context, httpClient *http.Client) ([]AgentT return nil, fmt.Errorf("failed to read agent templates response: %w", err) } - var templates []AgentTemplate - if err := json.Unmarshal(body, &templates); err != nil { + var all []AgentTemplate + if err := json.Unmarshal(body, &all); err != nil { return nil, fmt.Errorf("failed to parse agent templates: %w", err) } - return templates, nil + // Keep only agent-init entries. The shared templates.json manifest also + // carries the awesome-azd gallery; those entries must not surface here. + filtered := make([]AgentTemplate, 0, len(all)) + for _, t := range all { + if t.TemplateType == templateTypeExtensionAIAgent { + filtered = append(filtered, t) + } + } + + return filtered, nil } // promptAgentTemplate guides the user through language selection and template selection. @@ -169,10 +199,11 @@ func promptAgentTemplate( return nil, fmt.Errorf("no agent templates available") } - // Prompt for language + // Prompt for language. Values must match the language tokens used in + // the awesome-azd templates.json `languages` field (e.g. "dotnetCsharp"). languageChoices := []*azdext.SelectChoice{ {Label: "Python", Value: "python"}, - {Label: "C#", Value: "csharp"}, + {Label: "C#", Value: "dotnetCsharp"}, } langResp, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ @@ -190,10 +221,10 @@ func promptAgentTemplate( selectedLanguage := languageChoices[*langResp.Value].Value - // Filter templates by selected language + // Filter templates by selected language (entries can declare multiple). var filtered []AgentTemplate for _, t := range templates { - if t.Language == selectedLanguage { + if slices.Contains(t.Languages, selectedLanguage) { filtered = append(filtered, t) } } @@ -210,7 +241,7 @@ func promptAgentTemplate( // Build template choices with framework in label templateChoices := make([]*azdext.SelectChoice, len(filtered)) for i, t := range filtered { - label := fmt.Sprintf("%s (%s)", t.Title, t.Framework) + label := fmt.Sprintf("%s (%s)", t.Title, t.ExtensionFramework) templateChoices[i] = &azdext.SelectChoice{ Label: label, Value: fmt.Sprintf("%d", i), diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers_test.go index b5b34a853aa..db3903be790 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers_test.go @@ -4,10 +4,7 @@ package cmd import ( - "context" "encoding/json" - "fmt" - "io" "net/http" "net/http/httptest" "os" @@ -89,25 +86,41 @@ func TestEffectiveType(t *testing.T) { func TestFetchAgentTemplates(t *testing.T) { t.Parallel() - t.Run("success", func(t *testing.T) { + t.Run("success filters by templateType", func(t *testing.T) { t.Parallel() - templates := []AgentTemplate{ + // Manifest mixes gallery entries (no templateType / wrong templateType) + // with agent-init entries. Only the latter should survive. + manifest := []map[string]any{ { - Title: "Echo Agent", - Language: "python", - Framework: "Agent Framework", - Source: "https://github.com/org/repo/blob/main/echo-agent/agent.yaml", + "title": "Echo Agent", + "languages": []string{"python"}, + "extensionFramework": "Agent Framework", + "source": "https://github.com/org/repo/blob/main/echo-agent/agent.yaml", + "templateType": "extension.ai.agent", }, { - Title: "Calculator Agent", - Language: "csharp", - Framework: "LangGraph", - Source: "Azure-Samples/calculator-agent", + "title": "Calculator Agent", + "languages": []string{"dotnetCsharp"}, + "extensionFramework": "LangGraph", + "source": "Azure-Samples/calculator-agent", + "templateType": "extension.ai.agent", + }, + { + "title": "Some gallery template", + "languages": []string{"python"}, + "source": "Azure-Samples/some-template", + // no templateType -> standard awesome-azd gallery entry + }, + { + "title": "Future extension category", + "languages": []string{"python"}, + "source": "Azure-Samples/some-other-extension", + "templateType": "extension.something.else", }, } - data, err := json.Marshal(templates) + data, err := json.Marshal(manifest) require.NoError(t, err) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -117,14 +130,15 @@ func TestFetchAgentTemplates(t *testing.T) { })) defer server.Close() - // Use a custom URL by overriding the HTTP client to redirect result, err := fetchAgentTemplatesFromURL(t.Context(), server.Client(), server.URL) require.NoError(t, err) require.Len(t, result, 2) require.Equal(t, "Echo Agent", result[0].Title) - require.Equal(t, "python", result[0].Language) + require.Equal(t, []string{"python"}, result[0].Languages) + require.Equal(t, "Agent Framework", result[0].ExtensionFramework) + require.Equal(t, "extension.ai.agent", result[0].TemplateType) require.Equal(t, "Calculator Agent", result[1].Title) - require.Equal(t, "csharp", result[1].Language) + require.Equal(t, []string{"dotnetCsharp"}, result[1].Languages) }) t.Run("HTTP error", func(t *testing.T) { @@ -167,41 +181,30 @@ func TestFetchAgentTemplates(t *testing.T) { require.NoError(t, err) require.Empty(t, result) }) -} - -// fetchAgentTemplatesFromURL is a test helper that fetches templates from a custom URL. -func fetchAgentTemplatesFromURL( - ctx context.Context, - httpClient *http.Client, - url string, -) ([]AgentTemplate, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, err - } - //nolint:gosec // URL points to a local httptest server, not user input - resp, err := httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to fetch agent templates: HTTP %d", resp.StatusCode) - } + t.Run("manifest with only gallery entries returns empty", func(t *testing.T) { + t.Parallel() - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } + manifest := []map[string]any{ + { + "title": "Some gallery template", + "languages": []string{"python"}, + "source": "Azure-Samples/some-template", + }, + } + data, err := json.Marshal(manifest) + require.NoError(t, err) - var templates []AgentTemplate - if err := json.Unmarshal(body, &templates); err != nil { - return nil, fmt.Errorf("failed to parse agent templates: %w", err) - } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(data) + })) + defer server.Close() - return templates, nil + result, err := fetchAgentTemplatesFromURL(t.Context(), server.Client(), server.URL) + require.NoError(t, err) + require.Empty(t, result) + }) } func TestFindAgentManifest(t *testing.T) { From bc05b47690a51a2bdf4f0739e8a891f9b8e718d2 Mon Sep 17 00:00:00 2001 From: hemarina Date: Tue, 28 Apr 2026 14:55:57 -0700 Subject: [PATCH 2/2] address feedback --- .../cmd/init_from_templates_helpers.go | 24 ++++++++++++++++--- .../cmd/init_from_templates_helpers_test.go | 8 ++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go index b4d8d2abed6..36a9729692f 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "io/fs" + "log" "net/http" "os" "path/filepath" @@ -48,7 +49,7 @@ type AgentTemplate struct { Languages []string `json:"languages"` ExtensionFramework string `json:"extensionFramework"` Source string `json:"source"` - Tags []string `json:"extensionTags"` + ExtensionTags []string `json:"extensionTags"` TemplateType string `json:"templateType"` } @@ -138,7 +139,6 @@ func fetchAgentTemplatesFromURL( return nil, fmt.Errorf("failed to create request: %w", err) } - //nolint:gosec // URL is supplied by the caller from a trusted constant or a test server resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("failed to fetch agent templates: %w", err) @@ -168,6 +168,24 @@ func fetchAgentTemplatesFromURL( } } + // Always emit the fetched/matched counts to make transition-period and + // misconfiguration issues debuggable. + log.Printf( + "agent templates manifest: fetched %d templateType=%q (source=%s)", + len(filtered), templateTypeExtensionAIAgent, url, + ) + + // If we received entries but filtered them all out, the manifest is + // almost certainly in the legacy format or the discriminator value has + // changed. Surface that explicitly instead of returning an empty list, + // which the caller cannot distinguish from an intentionally empty manifest. + if len(all) > 0 && len(filtered) == 0 { + return nil, fmt.Errorf( + "agent templates manifest at %s contained %d entries but none had templateType=%q", + url, len(all), templateTypeExtensionAIAgent, + ) + } + return filtered, nil } @@ -222,7 +240,7 @@ func promptAgentTemplate( selectedLanguage := languageChoices[*langResp.Value].Value // Filter templates by selected language (entries can declare multiple). - var filtered []AgentTemplate + filtered := make([]AgentTemplate, 0, len(templates)) for _, t := range templates { if slices.Contains(t.Languages, selectedLanguage) { filtered = append(filtered, t) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers_test.go index db3903be790..cc0a1364dd0 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_templates_helpers_test.go @@ -182,7 +182,7 @@ func TestFetchAgentTemplates(t *testing.T) { require.Empty(t, result) }) - t.Run("manifest with only gallery entries returns empty", func(t *testing.T) { + t.Run("manifest with only gallery entries returns error", func(t *testing.T) { t.Parallel() manifest := []map[string]any{ @@ -202,8 +202,10 @@ func TestFetchAgentTemplates(t *testing.T) { defer server.Close() result, err := fetchAgentTemplatesFromURL(t.Context(), server.Client(), server.URL) - require.NoError(t, err) - require.Empty(t, result) + require.Error(t, err) + require.Nil(t, result) + require.Contains(t, err.Error(), "extension.ai.agent") + require.Contains(t, err.Error(), "1 entries") }) }