From 7e145d22e8695a2ef465db3234cf90b80712a481 Mon Sep 17 00:00:00 2001 From: trangevi Date: Tue, 14 Apr 2026 16:06:07 -0700 Subject: [PATCH 1/2] Better .net handling Signed-off-by: trangevi --- .../azure.ai.agents/internal/cmd/run.go | 11 +- .../azure.ai.agents/internal/cmd/run_test.go | 133 ++++++++++++++++++ 2 files changed, 143 insertions(+), 1 deletion(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/run.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/run.go index 3b8b3132ba5..0eb7d34f35f 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/run.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/run.go @@ -89,6 +89,10 @@ func runRun(ctx context.Context, flags *runFlags) error { } projectDir := runCtx.ProjectDir + // Detect project type early — used for both start-command resolution and + // environment setup (e.g., setting ASPNETCORE_URLS for .NET). + pt := detectProjectType(projectDir) + // Resolve start command: --start-command flag > azure.yaml startupCommand > detect startCmd := flags.startCommand if startCmd == "" { @@ -96,7 +100,6 @@ func runRun(ctx context.Context, flags *runFlags) error { } if startCmd == "" { - pt := detectProjectType(projectDir) if pt.StartCmd != "" { startCmd = pt.StartCmd fmt.Printf("Detected %s project. Start command: %s\n", pt.Language, startCmd) @@ -137,6 +140,12 @@ func runRun(ctx context.Context, flags *runFlags) error { env := os.Environ() env = append(env, fmt.Sprintf("PORT=%d", flags.port)) + // ASP.NET Core ignores the PORT env var — it uses ASPNETCORE_URLS to configure + // Kestrel's listening address. Set it so .NET agents bind to the correct port. + if pt.Language == "dotnet" { + env = append(env, fmt.Sprintf("ASPNETCORE_URLS=http://localhost:%d", flags.port)) + } + // Load azd environment variables (e.g., AZURE_AI_PROJECT_ENDPOINT) // so the agent can reach Azure services during local development. // Also translate azd env keys to FOUNDRY_* env vars so the agent code diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/run_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/run_test.go index afc7d0dbc96..ce86ffbf4b3 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/run_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/run_test.go @@ -4,6 +4,7 @@ package cmd import ( + "fmt" "os" "path/filepath" "runtime" @@ -336,3 +337,135 @@ func TestAppendFoundryEnvVars(t *testing.T) { } }) } + +func TestDotnetProjectSetsAspnetcoreUrls(t *testing.T) { + t.Parallel() + + t.Run("dotnet project includes ASPNETCORE_URLS", func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + // Create a .csproj file so detectProjectType returns "dotnet" + if err := os.WriteFile( + filepath.Join(dir, "MyAgent.csproj"), + []byte(""), + 0600, + ); err != nil { + t.Fatal(err) + } + + pt := detectProjectType(dir) + if pt.Language != "dotnet" { + t.Fatalf("expected dotnet, got %s", pt.Language) + } + + port := 8088 + var env []string + env = append(env, fmt.Sprintf("PORT=%d", port)) + if pt.Language == "dotnet" { + env = append(env, fmt.Sprintf("ASPNETCORE_URLS=http://localhost:%d", port)) + } + + if !slices.Contains(env, "PORT=8088") { + t.Errorf("expected PORT=8088 in env, got %v", env) + } + if !slices.Contains(env, "ASPNETCORE_URLS=http://localhost:8088") { + t.Errorf("expected ASPNETCORE_URLS=http://localhost:8088 in env, got %v", env) + } + }) + + t.Run("python project does not include ASPNETCORE_URLS", func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + if err := os.WriteFile( + filepath.Join(dir, "requirements.txt"), + []byte("flask"), + 0600, + ); err != nil { + t.Fatal(err) + } + + pt := detectProjectType(dir) + if pt.Language != "python" { + t.Fatalf("expected python, got %s", pt.Language) + } + + port := 8088 + var env []string + env = append(env, fmt.Sprintf("PORT=%d", port)) + if pt.Language == "dotnet" { + env = append(env, fmt.Sprintf("ASPNETCORE_URLS=http://localhost:%d", port)) + } + + if !slices.Contains(env, "PORT=8088") { + t.Errorf("expected PORT=8088 in env, got %v", env) + } + for _, entry := range env { + if strings.HasPrefix(entry, "ASPNETCORE_URLS=") { + t.Errorf("ASPNETCORE_URLS should not be set for python projects, got %v", env) + } + } + }) + + t.Run("node project does not include ASPNETCORE_URLS", func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + if err := os.WriteFile( + filepath.Join(dir, "package.json"), + []byte("{}"), + 0600, + ); err != nil { + t.Fatal(err) + } + + pt := detectProjectType(dir) + if pt.Language != "node" { + t.Fatalf("expected node, got %s", pt.Language) + } + + port := 9090 + var env []string + env = append(env, fmt.Sprintf("PORT=%d", port)) + if pt.Language == "dotnet" { + env = append(env, fmt.Sprintf("ASPNETCORE_URLS=http://localhost:%d", port)) + } + + if !slices.Contains(env, "PORT=9090") { + t.Errorf("expected PORT=9090 in env, got %v", env) + } + for _, entry := range env { + if strings.HasPrefix(entry, "ASPNETCORE_URLS=") { + t.Errorf("ASPNETCORE_URLS should not be set for node projects, got %v", env) + } + } + }) + + t.Run("dotnet project respects custom port", func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + if err := os.WriteFile( + filepath.Join(dir, "MyAgent.csproj"), + []byte(""), + 0600, + ); err != nil { + t.Fatal(err) + } + + pt := detectProjectType(dir) + port := 3000 + var env []string + env = append(env, fmt.Sprintf("PORT=%d", port)) + if pt.Language == "dotnet" { + env = append(env, fmt.Sprintf("ASPNETCORE_URLS=http://localhost:%d", port)) + } + + if !slices.Contains(env, "PORT=3000") { + t.Errorf("expected PORT=3000 in env, got %v", env) + } + if !slices.Contains(env, "ASPNETCORE_URLS=http://localhost:3000") { + t.Errorf( + "expected ASPNETCORE_URLS=http://localhost:3000 in env, got %v", + env, + ) + } + }) +} From ec55bc0740702b614ad9aad3d63182f1383344f8 Mon Sep 17 00:00:00 2001 From: trangevi Date: Tue, 14 Apr 2026 17:20:04 -0700 Subject: [PATCH 2/2] PR comments Signed-off-by: trangevi --- .../azure.ai.agents/internal/cmd/run.go | 19 +-- .../azure.ai.agents/internal/cmd/run_test.go | 114 +++++------------- 2 files changed, 40 insertions(+), 93 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/run.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/run.go index 0eb7d34f35f..b48a4b16cc4 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/run.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/run.go @@ -138,13 +138,7 @@ func runRun(ctx context.Context, flags *runFlags) error { cmdParts = resolveVenvCommand(projectDir, cmdParts) env := os.Environ() - env = append(env, fmt.Sprintf("PORT=%d", flags.port)) - - // ASP.NET Core ignores the PORT env var — it uses ASPNETCORE_URLS to configure - // Kestrel's listening address. Set it so .NET agents bind to the correct port. - if pt.Language == "dotnet" { - env = append(env, fmt.Sprintf("ASPNETCORE_URLS=http://localhost:%d", flags.port)) - } + env = appendPortEnvVars(env, pt, flags.port) // Load azd environment variables (e.g., AZURE_AI_PROJECT_ENDPOINT) // so the agent can reach Azure services during local development. @@ -209,6 +203,17 @@ func runRun(ctx context.Context, flags *runFlags) error { return nil } +// appendPortEnvVars appends PORT and, for .NET projects, ASPNETCORE_URLS to the +// environment slice so the agent listens on the correct port. +// ASP.NET Core ignores PORT — it uses ASPNETCORE_URLS to configure Kestrel. +func appendPortEnvVars(env []string, pt ProjectType, port int) []string { + env = append(env, fmt.Sprintf("PORT=%d", port)) + if pt.Language == "dotnet" { + env = append(env, fmt.Sprintf("ASPNETCORE_URLS=http://localhost:%d", port)) + } + return env +} + // --- Dependency installation --- func installDependencies(projectDir string) error { diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/run_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/run_test.go index ce86ffbf4b3..bca96f30fb3 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/run_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/run_test.go @@ -4,7 +4,6 @@ package cmd import ( - "fmt" "os" "path/filepath" "runtime" @@ -338,32 +337,13 @@ func TestAppendFoundryEnvVars(t *testing.T) { }) } -func TestDotnetProjectSetsAspnetcoreUrls(t *testing.T) { +func TestAppendPortEnvVars(t *testing.T) { t.Parallel() t.Run("dotnet project includes ASPNETCORE_URLS", func(t *testing.T) { t.Parallel() - dir := t.TempDir() - // Create a .csproj file so detectProjectType returns "dotnet" - if err := os.WriteFile( - filepath.Join(dir, "MyAgent.csproj"), - []byte(""), - 0600, - ); err != nil { - t.Fatal(err) - } - - pt := detectProjectType(dir) - if pt.Language != "dotnet" { - t.Fatalf("expected dotnet, got %s", pt.Language) - } - - port := 8088 - var env []string - env = append(env, fmt.Sprintf("PORT=%d", port)) - if pt.Language == "dotnet" { - env = append(env, fmt.Sprintf("ASPNETCORE_URLS=http://localhost:%d", port)) - } + pt := ProjectType{Language: "dotnet", StartCmd: "dotnet run"} + env := appendPortEnvVars(nil, pt, 8088) if !slices.Contains(env, "PORT=8088") { t.Errorf("expected PORT=8088 in env, got %v", env) @@ -375,97 +355,59 @@ func TestDotnetProjectSetsAspnetcoreUrls(t *testing.T) { t.Run("python project does not include ASPNETCORE_URLS", func(t *testing.T) { t.Parallel() - dir := t.TempDir() - if err := os.WriteFile( - filepath.Join(dir, "requirements.txt"), - []byte("flask"), - 0600, - ); err != nil { - t.Fatal(err) - } - - pt := detectProjectType(dir) - if pt.Language != "python" { - t.Fatalf("expected python, got %s", pt.Language) - } - - port := 8088 - var env []string - env = append(env, fmt.Sprintf("PORT=%d", port)) - if pt.Language == "dotnet" { - env = append(env, fmt.Sprintf("ASPNETCORE_URLS=http://localhost:%d", port)) - } + pt := ProjectType{Language: "python", StartCmd: "python main.py"} + env := appendPortEnvVars(nil, pt, 8088) if !slices.Contains(env, "PORT=8088") { t.Errorf("expected PORT=8088 in env, got %v", env) } for _, entry := range env { if strings.HasPrefix(entry, "ASPNETCORE_URLS=") { - t.Errorf("ASPNETCORE_URLS should not be set for python projects, got %v", env) + t.Errorf("ASPNETCORE_URLS should not be set for python, got %v", env) } } }) t.Run("node project does not include ASPNETCORE_URLS", func(t *testing.T) { t.Parallel() - dir := t.TempDir() - if err := os.WriteFile( - filepath.Join(dir, "package.json"), - []byte("{}"), - 0600, - ); err != nil { - t.Fatal(err) - } - - pt := detectProjectType(dir) - if pt.Language != "node" { - t.Fatalf("expected node, got %s", pt.Language) - } - - port := 9090 - var env []string - env = append(env, fmt.Sprintf("PORT=%d", port)) - if pt.Language == "dotnet" { - env = append(env, fmt.Sprintf("ASPNETCORE_URLS=http://localhost:%d", port)) - } + pt := ProjectType{Language: "node", StartCmd: "npm start"} + env := appendPortEnvVars(nil, pt, 9090) if !slices.Contains(env, "PORT=9090") { t.Errorf("expected PORT=9090 in env, got %v", env) } for _, entry := range env { if strings.HasPrefix(entry, "ASPNETCORE_URLS=") { - t.Errorf("ASPNETCORE_URLS should not be set for node projects, got %v", env) + t.Errorf("ASPNETCORE_URLS should not be set for node, got %v", env) } } }) t.Run("dotnet project respects custom port", func(t *testing.T) { t.Parallel() - dir := t.TempDir() - if err := os.WriteFile( - filepath.Join(dir, "MyAgent.csproj"), - []byte(""), - 0600, - ); err != nil { - t.Fatal(err) - } - - pt := detectProjectType(dir) - port := 3000 - var env []string - env = append(env, fmt.Sprintf("PORT=%d", port)) - if pt.Language == "dotnet" { - env = append(env, fmt.Sprintf("ASPNETCORE_URLS=http://localhost:%d", port)) - } + pt := ProjectType{Language: "dotnet", StartCmd: "dotnet run"} + env := appendPortEnvVars(nil, pt, 3000) if !slices.Contains(env, "PORT=3000") { t.Errorf("expected PORT=3000 in env, got %v", env) } - if !slices.Contains(env, "ASPNETCORE_URLS=http://localhost:3000") { - t.Errorf( - "expected ASPNETCORE_URLS=http://localhost:3000 in env, got %v", - env, - ) + expected := "ASPNETCORE_URLS=http://localhost:3000" + if !slices.Contains(env, expected) { + t.Errorf("expected %q in env, got %v", expected, env) + } + }) + + t.Run("preserves existing env entries", func(t *testing.T) { + t.Parallel() + pt := ProjectType{Language: "dotnet", StartCmd: "dotnet run"} + existing := []string{"HOME=/home/user", "PATH=/usr/bin"} + env := appendPortEnvVars(existing, pt, 8088) + + if len(env) != 4 { + t.Errorf("expected 4 entries (2 existing + PORT + ASPNETCORE_URLS), got %d: %v", len(env), env) + } + if env[0] != "HOME=/home/user" || env[1] != "PATH=/usr/bin" { + t.Errorf("existing entries not preserved, got %v", env) } }) }