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
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,45 @@ func TestCheckAgentDefinitionValid_InlineWithoutFile_Passes(
require.Contains(t, got.Message, "agent definition valid")
}

func TestCheckAgentDefinitionValid_InlineInvalidKind_Fails(
t *testing.T,
) {
t.Parallel()

props, err := structpb.NewStruct(map[string]any{
"kind": "nonsense",
"name": "echo-agent",
})
require.NoError(t, err)

client := newTestAzdClient(t,
&fakeProjectServer{resp: &azdext.GetProjectResponse{
Project: &azdext.ProjectConfig{
Path: t.TempDir(),
Services: map[string]*azdext.ServiceConfig{
"echo-agent": {
Name: "echo-agent",
Host: agentHost,
RelativePath: "src/agent",
AdditionalProperties: props,
},
},
},
}},
&fakeEnvironmentServer{})
check := newCheckAgentDefinitionValid(
Dependencies{AzdClient: client},
)

got := check.Fn(t.Context(), Options{}, nil)

require.Equal(t, StatusFail, got.Status)
failures, ok := got.Details["failures"].([]string)
require.True(t, ok)
require.Len(t, failures, 1)
require.Contains(t, failures[0], "template.kind must be one of")
}

func TestCheckAgentDefinitionValid_InlineWinsOverStaleFile(
t *testing.T,
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package project

import (
"fmt"
"log"
"maps"
"os"
"sync"
Expand Down Expand Up @@ -594,6 +593,9 @@ func agentDefinitionFromStruct(s *structpb.Struct, coreImage string) (agent_yaml
}

if inline.Kind != agent_yaml.AgentKindHosted {
if err := validateAgentServiceDefinition(s.AsMap()); err != nil {
return agent_yaml.ContainerAgent{}, false, err
}
return agent_yaml.ContainerAgent{}, false, nil
}

Expand All @@ -608,21 +610,8 @@ func agentDefinitionFromStruct(s *structpb.Struct, coreImage string) (agent_yaml

ca := inline.toContainerAgent(cfg.Container, coreImage)

// Validate the inline definition with the same rules the on-disk agent.yaml
// path uses (kind, name format, policies), so an inline definition cannot
// silently bypass validation. Marshal back to YAML so ValidateAgentDefinition
// sees the same shape it expects from disk.
if defBytes, marshalErr := yaml.Marshal(ca); marshalErr != nil {
// A ContainerAgent should always marshal; log at debug so a regression
// here is visible during troubleshooting rather than silently skipping
// validation.
log.Printf("[debug] skipping inline agent definition validation: marshal to YAML failed: %v", marshalErr)
} else if err := agent_yaml.ValidateAgentDefinition(defBytes); err != nil {
return agent_yaml.ContainerAgent{}, false, exterrors.Validation(
exterrors.CodeInvalidAgentManifest,
fmt.Sprintf("agent service definition is not valid: %s", err),
"fix the agent service entry in azure.yaml or re-run `azd ai agent init`",
)
if err := validateAgentServiceDefinition(ca); err != nil {
return agent_yaml.ContainerAgent{}, false, err
}

if ca.Image != "" && !containerImageRefRe.MatchString(ca.Image) {
Expand All @@ -636,6 +625,28 @@ func agentDefinitionFromStruct(s *structpb.Struct, coreImage string) (agent_yaml
return ca, true, nil
}

func validateAgentServiceDefinition(definition any) error {
defBytes, err := yaml.Marshal(definition)
if err != nil {
return exterrors.Validation(
exterrors.CodeInvalidAgentManifest,
fmt.Sprintf(
"agent service definition is not valid: failed to marshal: %s",
err,
),
"fix the agent service entry in azure.yaml or re-run `azd ai agent init`",
)
}
if err := agent_yaml.ValidateAgentDefinition(defBytes); err != nil {
return exterrors.Validation(
exterrors.CodeInvalidAgentManifest,
fmt.Sprintf("agent service definition is not valid: %s", err),
"fix the agent service entry in azure.yaml or re-run `azd ai agent init`",
)
}
return nil
}

// agentDefinitionFromDisk reads a legacy agent.yaml/agent.yml from the service
// directory. This is the deprecation fallback for projects written before the
// definition moved into azure.yaml.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,81 @@ func TestAgentDefinitionFromService_InvalidDefinition(t *testing.T) {
require.Error(t, err)
}

func TestLoadAgentDefinition_ResolvedKindValidation(t *testing.T) {
tests := []struct {
name string
kind agent_yaml.AgentKind
useFileRef bool
wantError bool
}{
{
name: "inline invalid kind",
kind: "nonsense",
wantError: true,
},
{
name: "valid workflow",
kind: agent_yaml.AgentKindWorkflow,
},
{
name: "referenced invalid kind",
kind: "nonsense",
useFileRef: true,
wantError: true,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()

projectRoot := t.TempDir()
propsValues := map[string]any{
"kind": string(test.kind),
"name": "kind-test-agent",
}
if test.useFileRef {
require.NoError(t, os.WriteFile(
filepath.Join(projectRoot, "definition.yaml"),
[]byte(
"kind: "+string(test.kind)+"\n"+
"name: kind-test-agent\n",
),
0o600,
))
propsValues = map[string]any{
"$ref": "./definition.yaml",
}
}

props, err := structpb.NewStruct(propsValues)
require.NoError(t, err)
svc := &azdext.ServiceConfig{
Name: "kind-test-agent",
Host: "azure.ai.agent",
AdditionalProperties: props,
}

_, isHosted, source, err := LoadAgentDefinition(
svc,
projectRoot,
)

if test.wantError {
require.ErrorContains(
t,
err,
"template.kind must be one of",
)
} else {
require.NoError(t, err)
}
require.False(t, isHosted)
require.Equal(t, AgentDefinitionSourceInline, source)
})
}
}

func TestLoadAgentDefinition_ToolboxServiceReference(t *testing.T) {
t.Parallel()

Expand Down
Loading