Skip to content
Merged
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: 27 additions & 0 deletions cli/azd/extensions/azure.ai.agents/ci-test.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
$gopath = go env GOPATH
$gotestsumBinary = "gotestsum"
if ($IsWindows) {
$gotestsumBinary += ".exe"
}
$gotestsum = Join-Path $gopath "bin" $gotestsumBinary

Write-Host "Running unit tests..."

if (Test-Path $gotestsum) {
# Use gotestsum for better output formatting and summary
& $gotestsum --format testname -- ./... -count=1
} else {
# Fallback to go test if gotestsum is not installed
Write-Host "gotestsum not found, using go test..." -ForegroundColor Yellow
go test ./... -v -count=1
}

if ($LASTEXITCODE -ne 0) {
Write-Host ""
Write-Host "Tests failed with exit code: $LASTEXITCODE" -ForegroundColor Red
exit $LASTEXITCODE
}

Write-Host ""
Write-Host "All tests passed!" -ForegroundColor Green
exit 0
2 changes: 1 addition & 1 deletion cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,7 @@ func (a *InitAction) downloadAgentYaml(
// Parse and validate the YAML content against AgentManifest structure
agentManifest, err := agent_yaml.LoadAndValidateAgentManifest(content)
if err != nil {
return nil, "", fmt.Errorf("AgentManifest %w", err)
return nil, "", err
}

fmt.Println("✓ YAML content successfully validated against AgentManifest format")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ func LoadAndValidateAgentManifest(manifestYamlContent []byte) (*AgentManifest, e

agentDef, err := ExtractAgentDefinition(manifestYamlContent)
if err != nil {
return nil, fmt.Errorf("YAML content does not conform to AgentManifest format: %w", err)
return nil, err
}
manifest.Template = agentDef

resourceDefs, err := ExtractResourceDefinitions(manifestYamlContent)
if err != nil {
return nil, fmt.Errorf("YAML content does not conform to AgentManifest format: %w", err)
return nil, err
}
manifest.Resources = resourceDefs

Expand All @@ -47,8 +47,27 @@ func ExtractAgentDefinition(manifestYamlContent []byte) (any, error) {
return nil, fmt.Errorf("YAML content is not valid: %w", err)
}

template := genericManifest["template"].(map[string]interface{})
templateBytes, _ := yaml.Marshal(template)
// Handle manifest format with "template" field
var templateBytes []byte

if templateValue, exists := genericManifest["template"]; exists && templateValue != nil {
// Manifest format with "template" field
template, ok := templateValue.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("template field must be a map, got %T", templateValue)
}
if len(template) == 0 {
return nil, fmt.Errorf("YAML content does not conform to AgentManifest format: template field is empty. See https://microsoft.github.io/AgentSchema/reference/agentmanifest for the expected format and https://github.com/microsoft-foundry/foundry-samples for examples")
}
var err error
templateBytes, err = yaml.Marshal(template)
if err != nil {
return nil, fmt.Errorf("failed to marshal template: %w", err)
}
} else {
// "template" field not found - return error
return nil, fmt.Errorf("YAML content does not conform to AgentManifest format: must contain 'template' field. See https://microsoft.github.io/AgentSchema/reference/agentmanifest for the expected format and https://github.com/microsoft-foundry/foundry-samples for examples")
}

var agentDef AgentDefinition
if err := yaml.Unmarshal(templateBytes, &agentDef); err != nil {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package agent_yaml

import (
"strings"
"testing"
)

// TestExtractAgentDefinition_WithTemplateField tests parsing YAML with a template field (manifest format)
func TestExtractAgentDefinition_WithTemplateField(t *testing.T) {
yamlContent := []byte(`
name: test-manifest
template:
kind: hosted
name: test-agent
description: Test agent with template field
protocols:
- protocol: responses
version: v1
`)

agent, err := ExtractAgentDefinition(yamlContent)
if err != nil {
t.Fatalf("ExtractAgentDefinition failed: %v", err)
}

containerAgent, ok := agent.(ContainerAgent)
if !ok {
t.Fatalf("Expected ContainerAgent, got %T", agent)
}

if containerAgent.Name != "test-agent" {
t.Errorf("Expected name 'test-agent', got '%s'", containerAgent.Name)
}

if containerAgent.Kind != AgentKindHosted {
t.Errorf("Expected kind 'hosted', got '%s'", containerAgent.Kind)
}
}

// TestExtractAgentDefinition_EmptyTemplateField tests that an empty or null template field returns an error
func TestExtractAgentDefinition_EmptyTemplateField(t *testing.T) {
testCases := []struct {
name string
yaml string
}{
{
name: "null template field",
yaml: `
name: test-manifest
template: null
`,
},
{
name: "empty template field",
yaml: `
name: test-manifest
template: {}
`,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
_, err := ExtractAgentDefinition([]byte(tc.yaml))
if err == nil {
t.Fatal("Expected error for empty/null template field, got nil")
}

// The error should indicate the template field issue
expectedMsg := "YAML content does not conform to AgentManifest format"
if !strings.Contains(err.Error(), expectedMsg) {
t.Errorf("Expected error message to contain '%s', got '%s'", expectedMsg, err.Error())
}
})
}
}

// TestExtractAgentDefinition_WithoutTemplateField tests that YAML without template field returns an error
func TestExtractAgentDefinition_WithoutTemplateField(t *testing.T) {
yamlContent := []byte(`
kind: hosted
name: lego-social-media-agent
description: An AI-powered social media content generator for LEGO products.
metadata:
authors:
- LEGO Social Media Team
tags:
- Social Media
- Content Generation
protocols:
- protocol: responses
version: v1
environment_variables:
- name: POSTGRES_SERVER
value: ${POSTGRES_SERVER}
- name: POSTGRES_DATABASE
value: ${POSTGRES_DATABASE}
`)

_, err := ExtractAgentDefinition(yamlContent)
if err == nil {
t.Fatal("Expected error for YAML without template field, got nil")
}

expectedMsg := "must contain 'template' field"
if !strings.Contains(err.Error(), expectedMsg) {
t.Errorf("Expected error message to contain '%s', got '%s'", expectedMsg, err.Error())
}
}

// TestExtractAgentDefinition_InvalidTemplate tests that an invalid template field returns an error
func TestExtractAgentDefinition_InvalidTemplate(t *testing.T) {
yamlContent := []byte(`
name: test-manifest
template: "this is not a map"
`)

_, err := ExtractAgentDefinition(yamlContent)
if err == nil {
t.Fatal("Expected error for invalid template field, got nil")
}

expectedMsg := "template field must be a map"
if !strings.Contains(err.Error(), expectedMsg) {
t.Errorf("Expected error message to contain '%s', got '%s'", expectedMsg, err.Error())
}
}

// TestExtractAgentDefinition_MissingTemplateField tests that YAML without template field returns an error
func TestExtractAgentDefinition_MissingTemplateField(t *testing.T) {
yamlContent := []byte(`
name: test-agent
description: Test agent without template field
`)

_, err := ExtractAgentDefinition(yamlContent)
if err == nil {
t.Fatal("Expected error for YAML without template field, got nil")
}

expectedMsg := "must contain 'template' field"
if !strings.Contains(err.Error(), expectedMsg) {
t.Errorf("Expected error message to contain '%s', got '%s'", expectedMsg, err.Error())
}
}

// TestLoadAndValidateAgentManifest_WithoutTemplateField tests that YAML without template field returns an error
func TestLoadAndValidateAgentManifest_WithoutTemplateField(t *testing.T) {
yamlContent := []byte(`
kind: hosted
name: test-standalone-agent
description: A standalone agent definition
protocols:
- protocol: responses
version: v1
`)

_, err := LoadAndValidateAgentManifest(yamlContent)
if err == nil {
t.Fatal("Expected error for YAML without template field, got nil")
}

expectedMsg := "must contain 'template' field"
if !strings.Contains(err.Error(), expectedMsg) {
t.Errorf("Expected error message to contain '%s', got '%s'", expectedMsg, err.Error())
}
}

// TestExtractAgentDefinition_IssueExample tests the exact YAML from the GitHub issue
func TestExtractAgentDefinition_IssueExample(t *testing.T) {
// This is the exact YAML from the GitHub issue that was causing the panic
// It should now return a proper error message instead of panicking
yamlContent := []byte(`# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml

kind: hosted
name: lego-social-media-agent
description: |
An AI-powered social media content generator for LEGO products.
metadata:
authors:
- LEGO Social Media Team
example:
- content: Generate a Twitter post about Star Wars LEGO sets
role: user
tags:
- Social Media
- Content Generation
protocols:
- protocol: responses
version: v1
environment_variables:
- name: POSTGRES_SERVER
value: ${POSTGRES_SERVER}
- name: POSTGRES_DATABASE
value: ${POSTGRES_DATABASE}
`)

_, err := ExtractAgentDefinition(yamlContent)
if err == nil {
t.Fatal("Expected error for YAML without template field, got nil")
}

expectedMsg := "must contain 'template' field"
if !strings.Contains(err.Error(), expectedMsg) {
t.Errorf("Expected error message to contain '%s', got '%s'", expectedMsg, err.Error())
}

// Test full validation flow - should also return error
_, err = LoadAndValidateAgentManifest(yamlContent)
if err == nil {
t.Fatal("Expected error from LoadAndValidateAgentManifest for YAML without template field, got nil")
}

if !strings.Contains(err.Error(), expectedMsg) {
t.Errorf("Expected error message to contain '%s', got '%s'", expectedMsg, err.Error())
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
agent:
template:
kind: prompt
name: Learn French Agent
description: |-
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
agent:
template:
kind: prompt
name: github-agent
description: An agent leveraging github-mcp to assist with GitHub-related tasks.
Expand Down
1 change: 0 additions & 1 deletion eng/pipelines/release-ext-azure-ai-agents.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,3 @@ extends:
AzdExtensionId: azure.ai.agents
SanitizedExtensionId: azure-ai-agents
AzdExtensionDirectory: cli/azd/extensions/azure.ai.agents
SkipTests: true
Loading