diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go index a7416eccc92..6065c2b7edb 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go @@ -73,12 +73,7 @@ func preprovisionHandler(ctx context.Context, azdClient *azdext.AzdClient, args for _, svc := range args.Project.Services { switch svc.Host { case AiAgentHost: - if err := prepareContainerSettings( - ctx, - azdClient, - svc, - args.Project.Path, - ); err != nil { + if err := prepareContainerSettings(svc, args.Project.Path); err != nil { return fmt.Errorf("failed to populate container settings for service %q: %w", svc.Name, err) } if err := envUpdate( @@ -246,12 +241,7 @@ func predeployHandler(ctx context.Context, azdClient *azdext.AzdClient, args *az return err } - if err := prepareContainerSettings( - ctx, - azdClient, - svc, - args.Project.Path, - ); err != nil { + if err := prepareContainerSettings(svc, args.Project.Path); err != nil { return fmt.Errorf("failed to populate container settings for service %q: %w", svc.Name, err) } if err := envUpdate( @@ -761,8 +751,6 @@ func setEnvVar(ctx context.Context, azdClient *azdext.AzdClient, envName string, } func prepareContainerSettings( - ctx context.Context, - azdClient *azdext.AzdClient, svc *azdext.ServiceConfig, projectRoot string, ) error { @@ -811,26 +799,15 @@ func prepareContainerSettings( result.Cpu = project.DefaultCpu } - // Persist the resolved container settings back onto the service's inline - // properties, preserving the agent definition and other config keys. - containerPath, containerValue, err := project.SetAgentContainerSettings( + // Defaults are runtime values. Do not persist them here: + // lifecycle hooks must not rewrite user-authored azure.yaml. + if err := project.SetAgentContainerSettings( svc, &project.ContainerSettings{Resources: result}, - ) - if err != nil { + ); err != nil { return fmt.Errorf("failed to update agent container settings: %w", err) } - if !hasRootFileRef { - if _, err := azdClient.Project().SetServiceConfigValue(ctx, &azdext.SetServiceConfigValueRequest{ - ServiceName: svc.GetName(), - Path: containerPath, - Value: containerValue, - }); err != nil { - return fmt.Errorf("persisting agent container settings: %w", err) - } - } - return nil } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/listen_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/listen_test.go index b047bbf233d..a027b743cdb 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/listen_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/listen_test.go @@ -5,7 +5,6 @@ package cmd import ( "bytes" - "context" "os" "path/filepath" "strings" @@ -21,50 +20,51 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) -type containerSettingsProjectServer struct { - azdext.UnimplementedProjectServiceServer - - mu sync.Mutex - addServiceCalls int - setServiceRequests []*azdext.SetServiceConfigValueRequest -} - -func (s *containerSettingsProjectServer) AddService( - _ context.Context, - _ *azdext.AddServiceRequest, -) (*azdext.EmptyResponse, error) { - s.mu.Lock() - defer s.mu.Unlock() - s.addServiceCalls++ - return &azdext.EmptyResponse{}, nil -} - -func (s *containerSettingsProjectServer) SetServiceConfigValue( - _ context.Context, - req *azdext.SetServiceConfigValueRequest, -) (*azdext.EmptyResponse, error) { - s.mu.Lock() - defer s.mu.Unlock() - s.setServiceRequests = append(s.setServiceRequests, req) - return &azdext.EmptyResponse{}, nil -} - -func TestPrepareContainerSettings_UsesTargetedConfigUpdate(t *testing.T) { +func TestPrepareContainerSettings_AppliesSettingsInMemory(t *testing.T) { t.Parallel() tests := []struct { - name string - legacy bool - wantPath string + name string + legacy bool + resources map[string]any + wantCPU string + wantMemory string }{ { - name: "inline service properties", - wantPath: "container", + name: "inline explicit resources", + resources: map[string]any{ + "cpu": "0.25", + "memory": "0.5Gi", + }, + wantCPU: "0.25", + wantMemory: "0.5Gi", + }, + { + name: "legacy explicit resources", + legacy: true, + resources: map[string]any{ + "cpu": "0.25", + "memory": "0.5Gi", + }, + wantCPU: "0.25", + wantMemory: "0.5Gi", }, { - name: "legacy config properties", - legacy: true, - wantPath: "config.container", + name: "inline missing memory", + resources: map[string]any{ + "cpu": "1", + }, + wantCPU: "1", + wantMemory: project.DefaultMemory, + }, + { + name: "legacy missing memory", + legacy: true, + resources: map[string]any{ + "cpu": "1", + }, + wantCPU: "1", + wantMemory: project.DefaultMemory, }, } @@ -77,9 +77,7 @@ func TestPrepareContainerSettings_UsesTargetedConfigUpdate(t *testing.T) { "name": "my-chat-agent", "customField": "preserved", "container": map[string]any{ - "resources": map[string]any{ - "cpu": "1", - }, + "resources": tt.resources, }, }) require.NoError(t, err) @@ -95,28 +93,14 @@ func TestPrepareContainerSettings_UsesTargetedConfigUpdate(t *testing.T) { svc.AdditionalProperties = props } - server := &containerSettingsProjectServer{} - client := newProjectRecorderClient(t, server) - - require.NoError(t, prepareContainerSettings(t.Context(), client, svc, t.TempDir())) - - server.mu.Lock() - defer server.mu.Unlock() - - require.Zero(t, server.addServiceCalls, - "full service replacement would drop fields that are not modeled by the extension") - require.Len(t, server.setServiceRequests, 1) - - req := server.setServiceRequests[0] - require.Equal(t, "agent", req.ServiceName) - require.Equal(t, tt.wantPath, req.Path) - require.Equal(t, map[string]any{ - "resources": map[string]any{ - "cpu": "1", - "memory": project.DefaultMemory, - }, - }, req.Value.AsInterface()) + require.NoError(t, prepareContainerSettings(svc, t.TempDir())) + cfg, err := project.LoadServiceTargetAgentConfig(svc) + require.NoError(t, err) + require.NotNil(t, cfg.Container) + require.NotNil(t, cfg.Container.Resources) + require.Equal(t, tt.wantCPU, cfg.Container.Resources.Cpu) + require.Equal(t, tt.wantMemory, cfg.Container.Resources.Memory) require.Equal(t, "myregistry.azurecr.io/my-agent:${MY_TAG}", svc.Image) require.Equal(t, "preserved", project.ServiceConfigProps(svc).GetFields()["customField"].GetStringValue()) @@ -235,7 +219,7 @@ func TestIsHostedAgentServiceRejectsTraversal(t *testing.T) { } } -func TestPrepareContainerSettings_DoesNotPersistResolvedFileRef( +func TestPrepareContainerSettings_ResolvesFileRefInMemory( t *testing.T, ) { t.Parallel() @@ -263,10 +247,7 @@ func TestPrepareContainerSettings_DoesNotPersistResolvedFileRef( RelativePath: "src/echo", AdditionalProperties: props, } - server := &containerSettingsProjectServer{} - client := newProjectRecorderClient(t, server) - - err = prepareContainerSettings(t.Context(), client, svc, root) + err = prepareContainerSettings(svc, root) require.NoError(t, err) require.Equal(t, "src/echo", svc.GetRelativePath()) @@ -276,7 +257,6 @@ func TestPrepareContainerSettings_DoesNotPersistResolvedFileRef( require.NotNil(t, cfg.Container.Resources) require.Equal(t, "2", cfg.Container.Resources.Cpu) require.Equal(t, "4Gi", cfg.Container.Resources.Memory) - require.Empty(t, server.setServiceRequests) } func TestPrepareContainerSettings_PreservesNestedFileRef(t *testing.T) { @@ -295,9 +275,7 @@ func TestPrepareContainerSettings_PreservesNestedFileRef(t *testing.T) { Host: AiAgentHost, AdditionalProperties: props, } - client := newProjectRecorderClient(t, &containerSettingsProjectServer{}) - - err = prepareContainerSettings(t.Context(), client, svc, t.TempDir()) + err = prepareContainerSettings(svc, t.TempDir()) require.NoError(t, err) deployments, ok := svc.GetAdditionalProperties(). @@ -335,9 +313,7 @@ func TestPrepareContainerSettings_NormalizesInlineEnvironment(t *testing.T) { Host: AiAgentHost, AdditionalProperties: props, } - client := newProjectRecorderClient(t, &containerSettingsProjectServer{}) - - err = prepareContainerSettings(t.Context(), client, svc, t.TempDir()) + err = prepareContainerSettings(svc, t.TempDir()) require.NoError(t, err) require.Equal( @@ -352,15 +328,16 @@ func TestPrepareContainerSettings_NormalizesInlineEnvironment(t *testing.T) { func TestPrepareContainerSettings_WithoutProperties(t *testing.T) { t.Parallel() - client := newProjectRecorderClient(t, &containerSettingsProjectServer{}) - err := prepareContainerSettings( - t.Context(), - client, - &azdext.ServiceConfig{Name: "echo", Host: AiAgentHost}, - t.TempDir(), - ) + svc := &azdext.ServiceConfig{Name: "echo", Host: AiAgentHost} + err := prepareContainerSettings(svc, t.TempDir()) require.NoError(t, err) + cfg, err := project.LoadServiceTargetAgentConfig(svc) + require.NoError(t, err) + require.NotNil(t, cfg.Container) + require.NotNil(t, cfg.Container.Resources) + require.Equal(t, project.DefaultCpu, cfg.Container.Resources.Cpu) + require.Equal(t, project.DefaultMemory, cfg.Container.Resources.Memory) } func TestKindEnvUpdateRejectsTraversal(t *testing.T) { diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/agent_definition.go b/cli/azd/extensions/azure.ai.agents/internal/project/agent_definition.go index 921c7236ff7..8e537eabf1a 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/agent_definition.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/agent_definition.go @@ -548,18 +548,13 @@ func UpsertAgentEnvVars(svc *azdext.ServiceConfig, kv map[string]string) error { // agent service's inline properties, preserving every other key (the agent // definition and the rest of the deploy/provision config). It mutates whichever // shape the service uses (the unified AdditionalProperties, or — for older -// projects — the config-nested struct). The returned path and value identify -// the exact mutation for callers that need to persist it through the azd host. +// projects — the config-nested struct). func SetAgentContainerSettings( svc *azdext.ServiceConfig, container *ContainerSettings, -) (string, *structpb.Value, error) { +) error { props := ServiceConfigProps(svc) legacy := props != nil && props == svc.GetConfig() - containerPath := "container" - if legacy { - containerPath = "config.container" - } if props == nil { props = &structpb.Struct{} } @@ -569,17 +564,16 @@ func SetAgentContainerSettings( containerStruct, err := MarshalStruct(container) if err != nil { - return "", nil, fmt.Errorf("marshaling container settings: %w", err) + return fmt.Errorf("marshaling container settings: %w", err) } - containerValue := structpb.NewStructValue(containerStruct) - props.Fields["container"] = containerValue + props.Fields["container"] = structpb.NewStructValue(containerStruct) if legacy { svc.Config = props } else { svc.AdditionalProperties = props } - return containerPath, containerValue, nil + return nil } // agentDefinitionFromStruct builds the ContainerAgent from an inline/config diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/agent_definition_test.go b/cli/azd/extensions/azure.ai.agents/internal/project/agent_definition_test.go index 3c56fbf0387..17cc8a7cb35 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/agent_definition_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/agent_definition_test.go @@ -157,29 +157,25 @@ func TestAgentDefinitionFromService_NoDefinition(t *testing.T) { require.False(t, found) } -func TestSetAgentContainerSettings_ReturnsPersistenceTarget(t *testing.T) { +func TestSetAgentContainerSettings_PreservesServiceProperties(t *testing.T) { t.Parallel() tests := []struct { name string legacy bool unrelatedInline bool - wantPath string }{ { - name: "inline service properties", - wantPath: "container", + name: "inline service properties", }, { - name: "legacy config properties", - legacy: true, - wantPath: "config.container", + name: "legacy config properties", + legacy: true, }, { name: "legacy config properties with unrelated inline properties", legacy: true, unrelatedInline: true, - wantPath: "config.container", }, } @@ -206,21 +202,19 @@ func TestSetAgentContainerSettings_ReturnsPersistenceTarget(t *testing.T) { require.NoError(t, err) } - path, value, err := SetAgentContainerSettings(svc, &ContainerSettings{ + err = SetAgentContainerSettings(svc, &ContainerSettings{ Resources: &ResourceSettings{Cpu: "1", Memory: "2Gi"}, }) require.NoError(t, err) - require.Equal(t, tt.wantPath, path) + + storedProps := ServiceConfigProps(svc) + require.Equal(t, "preserved", storedProps.GetFields()["customField"].GetStringValue()) require.Equal(t, map[string]any{ "resources": map[string]any{ "cpu": "1", "memory": "2Gi", }, - }, value.AsInterface()) - - storedProps := ServiceConfigProps(svc) - require.Equal(t, "preserved", storedProps.GetFields()["customField"].GetStringValue()) - require.Same(t, value, storedProps.GetFields()["container"]) + }, storedProps.GetFields()["container"].AsInterface()) }) } }