From 02966430574e6f2553999e38052910f884eae0a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:37:38 +0000 Subject: [PATCH 1/7] Initial plan From 95f3fc54be58feafbebec151a89bdc1198f74f4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:49:22 +0000 Subject: [PATCH 2/7] Support azure.yml in addition to azure.yaml - Updated Go code to search for both azure.yaml and azure.yml files - azure.yaml takes precedence when both files exist - Updated VS Code extension context menus to show for both file extensions - Updated glob patterns and validation schemas for both file names - Updated error messages to mention both file extensions Co-authored-by: spboyer <7681382+spboyer@users.noreply.github.com> --- .../pkg/environment/azdcontext/azdcontext.go | 40 +++++++++++++------ ext/vscode/package.json | 30 ++++++++------ ext/vscode/src/commands/cmdUtil.ts | 11 ++--- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/cli/azd/pkg/environment/azdcontext/azdcontext.go b/cli/azd/pkg/environment/azdcontext/azdcontext.go index 1b8f46a8e97..303915123a4 100644 --- a/cli/azd/pkg/environment/azdcontext/azdcontext.go +++ b/cli/azd/pkg/environment/azdcontext/azdcontext.go @@ -20,8 +20,12 @@ const DotEnvFileName = ".env" const ConfigFileName = "config.json" const ConfigFileVersion = 1 +// ProjectFileNames lists all valid project file names, in order of preference +var ProjectFileNames = []string{"azure.yaml", "azure.yml"} + type AzdContext struct { projectDirectory string + projectFilePath string } func (c *AzdContext) ProjectDirectory() string { @@ -33,6 +37,9 @@ func (c *AzdContext) SetProjectDirectory(dir string) { } func (c *AzdContext) ProjectPath() string { + if c.projectFilePath != "" { + return c.projectFilePath + } return filepath.Join(c.ProjectDirectory(), ProjectFileName) } @@ -129,26 +136,35 @@ func NewAzdContextFromWd(wd string) (*AzdContext, error) { return nil, fmt.Errorf("resolving path: %w", err) } + var foundProjectFilePath string for { - projectFilePath := filepath.Join(searchDir, ProjectFileName) - stat, err := os.Stat(projectFilePath) - if os.IsNotExist(err) || (err == nil && stat.IsDir()) { - parent := filepath.Dir(searchDir) - if parent == searchDir { - return nil, ErrNoProject + // Try all valid project file names in order of preference + for _, fileName := range ProjectFileNames { + projectFilePath := filepath.Join(searchDir, fileName) + stat, err := os.Stat(projectFilePath) + if err == nil && !stat.IsDir() { + // We found a valid project file + foundProjectFilePath = projectFilePath + break } - searchDir = parent - } else if err == nil { - // We found our azure.yaml file, and searchDir is the directory - // that contains it. + } + + if foundProjectFilePath != "" { + // We found our project file, and searchDir is the directory that contains it. break - } else { - return nil, fmt.Errorf("searching for project file: %w", err) } + + // No project file found in this directory, move up to parent + parent := filepath.Dir(searchDir) + if parent == searchDir { + return nil, ErrNoProject + } + searchDir = parent } return &AzdContext{ projectDirectory: searchDir, + projectFilePath: foundProjectFilePath, }, nil } diff --git a/ext/vscode/package.json b/ext/vscode/package.json index ca8f7ff8c25..d8eb573203b 100644 --- a/ext/vscode/package.json +++ b/ext/vscode/package.json @@ -194,7 +194,7 @@ "explorer/context": [ { "submenu": "azure-dev.explorer.submenu", - "when": "resourceFilename =~ /(azure.yaml|pom.xml)/i", + "when": "resourceFilename =~ /(azure.ya?ml|pom.xml)/i", "group": "azure-dev" } ], @@ -205,62 +205,62 @@ "group": "10provision@10" }, { - "when": "resourceFilename =~ /azure.yaml/i", + "when": "resourceFilename =~ /azure.ya?ml/i", "command": "azure-dev.commands.cli.provision", "group": "10provision@10" }, { - "when": "resourceFilename =~ /azure.yaml/i", + "when": "resourceFilename =~ /azure.ya?ml/i", "command": "azure-dev.commands.cli.deploy", "group": "10provision@20" }, { - "when": "resourceFilename =~ /azure.yaml/i", + "when": "resourceFilename =~ /azure.ya?ml/i", "command": "azure-dev.commands.cli.up", "group": "10provision@30" }, { - "when": "resourceFilename =~ /azure.yaml/i", + "when": "resourceFilename =~ /azure.ya?ml/i", "command": "azure-dev.commands.cli.down", "group": "10provision@40" }, { - "when": "resourceFilename =~ /azure.yaml/i", + "when": "resourceFilename =~ /azure.ya?ml/i", "command": "azure-dev.commands.cli.env-new", "group": "20env@10" }, { - "when": "resourceFilename =~ /azure.yaml/i", + "when": "resourceFilename =~ /azure.ya?ml/i", "command": "azure-dev.commands.cli.env-select", "group": "20env@20" }, { - "when": "resourceFilename =~ /azure.yaml/i", + "when": "resourceFilename =~ /azure.ya?ml/i", "command": "azure-dev.commands.cli.env-refresh", "group": "20env@30" }, { - "when": "resourceFilename =~ /azure.yaml/i", + "when": "resourceFilename =~ /azure.ya?ml/i", "command": "azure-dev.commands.cli.env-list", "group": "20env@40" }, { - "when": "resourceFilename =~ /azure.yaml/i", + "when": "resourceFilename =~ /azure.ya?ml/i", "command": "azure-dev.commands.cli.restore", "group": "30develop@10" }, { - "when": "resourceFilename =~ /azure.yaml/i", + "when": "resourceFilename =~ /azure.ya?ml/i", "command": "azure-dev.commands.cli.package", "group": "30develop@20" }, { - "when": "resourceFilename =~ /azure.yaml/i", + "when": "resourceFilename =~ /azure.ya?ml/i", "command": "azure-dev.commands.cli.pipeline-config", "group": "30develop@30" }, { - "when": "resourceFilename =~ /azure.yaml/i", + "when": "resourceFilename =~ /azure.ya?ml/i", "command": "azure-dev.commands.cli.monitor", "group": "40monitor@10" } @@ -500,6 +500,10 @@ { "fileMatch": "azure.yaml", "url": "https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json" + }, + { + "fileMatch": "azure.yml", + "url": "https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json" } ] }, diff --git a/ext/vscode/src/commands/cmdUtil.ts b/ext/vscode/src/commands/cmdUtil.ts index 8d7bc02b474..bf0aebcf8fa 100644 --- a/ext/vscode/src/commands/cmdUtil.ts +++ b/ext/vscode/src/commands/cmdUtil.ts @@ -10,10 +10,10 @@ import { createAzureDevCli } from '../utils/azureDevCli'; import { execAsync } from '../utils/execAsync'; import { fileExists } from '../utils/fileUtils'; -const AzureYamlGlobPattern: vscode.GlobPattern = '**/[aA][zZ][uU][rR][eE].[yY][aA][mM][lL]'; +const AzureYamlGlobPattern: vscode.GlobPattern = '**/azure.{yaml,yml}'; // If the command was invoked with a specific file context, use the file context as the working directory for running Azure developer CLI commands. -// Otherwise search the workspace for "azure.yaml" files. If only one is found, use it (i.e. its folder). If more than one is found, ask the user which one to use. +// Otherwise search the workspace for "azure.yaml" or "azure.yml" files. If only one is found, use it (i.e. its folder). If more than one is found, ask the user which one to use. // If at this point we still do not have a working directory, prompt the user to select one. export async function getWorkingFolder(context: IActionContext, selectedFile?: vscode.Uri): Promise { let folderPath = selectedFile ? path.dirname(selectedFile.fsPath) : undefined; @@ -39,10 +39,11 @@ export async function getWorkingFolder(context: IActionContext, selectedFile?: v const folderUri = localFolderUris[0]; const azureYamlUri = vscode.Uri.joinPath(folderUri, 'azure.yaml'); + const azureYmlUri = vscode.Uri.joinPath(folderUri, 'azure.yml'); - if (!await fileExists(azureYamlUri)) { + if (!await fileExists(azureYamlUri) && !await fileExists(azureYmlUri)) { context.errorHandling.suppressReportIssue = true; - throw new Error(vscode.l10n.t("The selected folder does not contain 'azure.yaml' file and cannot be used to run Azure Developer CLI commands")); + throw new Error(vscode.l10n.t("The selected folder does not contain 'azure.yaml' or 'azure.yml' file and cannot be used to run Azure Developer CLI commands")); } folderPath = folderUri.fsPath; @@ -66,7 +67,7 @@ export async function pickAzureYamlFile(context: IActionContext): Promise Date: Tue, 25 Nov 2025 19:55:23 +0000 Subject: [PATCH 3/7] Address code review feedback - Add clarifying comment to ProjectPath() method explaining its dual behavior - Fix VS Code glob pattern to be case-insensitive using character classes - Pattern now correctly matches azure.yaml, azure.yml, AZURE.YAML, Azure.Yml, etc. Co-authored-by: spboyer <7681382+spboyer@users.noreply.github.com> --- cli/azd/pkg/environment/azdcontext/azdcontext.go | 3 +++ ext/vscode/src/commands/cmdUtil.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cli/azd/pkg/environment/azdcontext/azdcontext.go b/cli/azd/pkg/environment/azdcontext/azdcontext.go index 303915123a4..acbfcdaf69c 100644 --- a/cli/azd/pkg/environment/azdcontext/azdcontext.go +++ b/cli/azd/pkg/environment/azdcontext/azdcontext.go @@ -36,6 +36,9 @@ func (c *AzdContext) SetProjectDirectory(dir string) { c.projectDirectory = dir } +// ProjectPath returns the path to the project file. If the context was created by searching +// for a project file, returns the actual file that was found. Otherwise, returns the default +// project file name joined with the project directory (useful when creating new projects). func (c *AzdContext) ProjectPath() string { if c.projectFilePath != "" { return c.projectFilePath diff --git a/ext/vscode/src/commands/cmdUtil.ts b/ext/vscode/src/commands/cmdUtil.ts index bf0aebcf8fa..dc251c28a2e 100644 --- a/ext/vscode/src/commands/cmdUtil.ts +++ b/ext/vscode/src/commands/cmdUtil.ts @@ -10,7 +10,7 @@ import { createAzureDevCli } from '../utils/azureDevCli'; import { execAsync } from '../utils/execAsync'; import { fileExists } from '../utils/fileUtils'; -const AzureYamlGlobPattern: vscode.GlobPattern = '**/azure.{yaml,yml}'; +const AzureYamlGlobPattern: vscode.GlobPattern = '**/[aA][zZ][uU][rR][eE].{[yY][aA][mM][lL],[yY][mM][lL]}'; // If the command was invoked with a specific file context, use the file context as the working directory for running Azure developer CLI commands. // Otherwise search the workspace for "azure.yaml" or "azure.yml" files. If only one is found, use it (i.e. its folder). If more than one is found, ask the user which one to use. From 26f551425e4d663524bd82238001ee1522abe320 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:59:16 +0000 Subject: [PATCH 4/7] Final verification and security review completed Co-authored-by: spboyer <7681382+spboyer@users.noreply.github.com> --- cli/azd/go.mod | 1 - cli/azd/go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/cli/azd/go.mod b/cli/azd/go.mod index e546991bbfa..f51c6a9552f 100644 --- a/cli/azd/go.mod +++ b/cli/azd/go.mod @@ -52,7 +52,6 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/joho/godotenv v1.5.1 - github.com/magefile/mage v1.15.0 github.com/mark3labs/mcp-go v0.41.1 github.com/mattn/go-colorable v0.1.14 github.com/mattn/go-isatty v0.0.20 diff --git a/cli/azd/go.sum b/cli/azd/go.sum index 9a9a668bb40..b245f675535 100644 --- a/cli/azd/go.sum +++ b/cli/azd/go.sum @@ -284,8 +284,6 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kUL github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= -github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mark3labs/mcp-go v0.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA= From 22927162ea79b1fe33fb74b340f693ac56d35a50 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:34:43 +0000 Subject: [PATCH 5/7] Revert go.mod and go.sum to original state These files are manifest/lockfiles managed by the Go toolchain and should not be manually modified. Restored them to their original state before my changes. Co-authored-by: spboyer <7681382+spboyer@users.noreply.github.com> --- cli/azd/go.mod | 1 + cli/azd/go.sum | 2 ++ 2 files changed, 3 insertions(+) diff --git a/cli/azd/go.mod b/cli/azd/go.mod index f51c6a9552f..e546991bbfa 100644 --- a/cli/azd/go.mod +++ b/cli/azd/go.mod @@ -52,6 +52,7 @@ require ( github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/joho/godotenv v1.5.1 + github.com/magefile/mage v1.15.0 github.com/mark3labs/mcp-go v0.41.1 github.com/mattn/go-colorable v0.1.14 github.com/mattn/go-isatty v0.0.20 diff --git a/cli/azd/go.sum b/cli/azd/go.sum index b245f675535..9a9a668bb40 100644 --- a/cli/azd/go.sum +++ b/cli/azd/go.sum @@ -284,6 +284,8 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kUL github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mark3labs/mcp-go v0.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA= From 83cc6ab0f78e2d9927af330120fbfb50766dff03 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 19:35:11 +0000 Subject: [PATCH 6/7] Handle unexpected errors from os.Stat in project file search Added proper error handling to return errors from os.Stat that are not os.ErrNotExist. This ensures unexpected file system errors (e.g., permission issues) are properly propagated instead of being silently ignored. Co-authored-by: weikanglim <2322434+weikanglim@users.noreply.github.com> --- cli/azd/pkg/environment/azdcontext/azdcontext.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cli/azd/pkg/environment/azdcontext/azdcontext.go b/cli/azd/pkg/environment/azdcontext/azdcontext.go index acbfcdaf69c..ebff5bf0e37 100644 --- a/cli/azd/pkg/environment/azdcontext/azdcontext.go +++ b/cli/azd/pkg/environment/azdcontext/azdcontext.go @@ -145,10 +145,16 @@ func NewAzdContextFromWd(wd string) (*AzdContext, error) { for _, fileName := range ProjectFileNames { projectFilePath := filepath.Join(searchDir, fileName) stat, err := os.Stat(projectFilePath) - if err == nil && !stat.IsDir() { + if os.IsNotExist(err) || (err == nil && stat.IsDir()) { + // File doesn't exist or is a directory, try next file name + continue + } else if err == nil { // We found a valid project file foundProjectFilePath = projectFilePath break + } else { + // An unexpected error occurred + return nil, fmt.Errorf("searching for project file: %w", err) } } From f298fdf29114662d5a00c78417d15f03b562cb41 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 21:41:07 +0000 Subject: [PATCH 7/7] Add comprehensive unit tests for azdcontext package Added unit tests to validate: - Finding azure.yaml files - Finding azure.yml files - Precedence when both files exist (azure.yaml takes priority) - Walking up from subdirectories - Handling missing project files (returns ErrNoProject) - Handling directories with same name as project files - ProjectPath() behavior with and without found files - Helper methods (SetProjectDirectory, EnvironmentDirectory, etc.) - ProjectFileNames order and contents All tests pass with 0 linting issues. Co-authored-by: wbreza <6540159+wbreza@users.noreply.github.com> --- .../environment/azdcontext/azdcontext_test.go | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 cli/azd/pkg/environment/azdcontext/azdcontext_test.go diff --git a/cli/azd/pkg/environment/azdcontext/azdcontext_test.go b/cli/azd/pkg/environment/azdcontext/azdcontext_test.go new file mode 100644 index 00000000000..6ca6653e73b --- /dev/null +++ b/cli/azd/pkg/environment/azdcontext/azdcontext_test.go @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package azdcontext + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewAzdContextFromWd_WithAzureYaml(t *testing.T) { + tempDir := t.TempDir() + + // Create azure.yaml file + azureYamlPath := filepath.Join(tempDir, "azure.yaml") + err := os.WriteFile(azureYamlPath, []byte("name: test\n"), 0600) + require.NoError(t, err) + + // Test from the directory containing azure.yaml + ctx, err := NewAzdContextFromWd(tempDir) + require.NoError(t, err) + require.NotNil(t, ctx) + require.Equal(t, tempDir, ctx.ProjectDirectory()) + require.Equal(t, azureYamlPath, ctx.ProjectPath()) +} + +func TestNewAzdContextFromWd_WithAzureYml(t *testing.T) { + tempDir := t.TempDir() + + // Create azure.yml file + azureYmlPath := filepath.Join(tempDir, "azure.yml") + err := os.WriteFile(azureYmlPath, []byte("name: test\n"), 0600) + require.NoError(t, err) + + // Test from the directory containing azure.yml + ctx, err := NewAzdContextFromWd(tempDir) + require.NoError(t, err) + require.NotNil(t, ctx) + require.Equal(t, tempDir, ctx.ProjectDirectory()) + require.Equal(t, azureYmlPath, ctx.ProjectPath()) +} + +func TestNewAzdContextFromWd_BothFilesExist_YamlTakesPrecedence(t *testing.T) { + tempDir := t.TempDir() + + // Create both azure.yaml and azure.yml + azureYamlPath := filepath.Join(tempDir, "azure.yaml") + azureYmlPath := filepath.Join(tempDir, "azure.yml") + err := os.WriteFile(azureYamlPath, []byte("name: yaml\n"), 0600) + require.NoError(t, err) + err = os.WriteFile(azureYmlPath, []byte("name: yml\n"), 0600) + require.NoError(t, err) + + // Test that azure.yaml takes precedence + ctx, err := NewAzdContextFromWd(tempDir) + require.NoError(t, err) + require.NotNil(t, ctx) + require.Equal(t, tempDir, ctx.ProjectDirectory()) + require.Equal(t, azureYamlPath, ctx.ProjectPath(), "azure.yaml should take precedence over azure.yml") +} + +func TestNewAzdContextFromWd_FromSubdirectory(t *testing.T) { + tempDir := t.TempDir() + subDir := filepath.Join(tempDir, "src", "api") + err := os.MkdirAll(subDir, 0755) + require.NoError(t, err) + + // Create azure.yml in the root + azureYmlPath := filepath.Join(tempDir, "azure.yml") + err = os.WriteFile(azureYmlPath, []byte("name: test\n"), 0600) + require.NoError(t, err) + + // Test from subdirectory - should walk up and find the file + ctx, err := NewAzdContextFromWd(subDir) + require.NoError(t, err) + require.NotNil(t, ctx) + require.Equal(t, tempDir, ctx.ProjectDirectory()) + require.Equal(t, azureYmlPath, ctx.ProjectPath()) +} + +func TestNewAzdContextFromWd_NoProjectFile(t *testing.T) { + tempDir := t.TempDir() + + // No project file exists + ctx, err := NewAzdContextFromWd(tempDir) + require.Error(t, err) + require.Nil(t, ctx) + require.ErrorIs(t, err, ErrNoProject) +} + +func TestNewAzdContextFromWd_DirectoryWithSameName(t *testing.T) { + tempDir := t.TempDir() + + // Create a directory named azure.yaml (edge case) + azureYamlDir := filepath.Join(tempDir, "azure.yaml") + err := os.Mkdir(azureYamlDir, 0755) + require.NoError(t, err) + + // Create actual azure.yml file + azureYmlPath := filepath.Join(tempDir, "azure.yml") + err = os.WriteFile(azureYmlPath, []byte("name: test\n"), 0600) + require.NoError(t, err) + + // Should find azure.yml and skip the directory + ctx, err := NewAzdContextFromWd(tempDir) + require.NoError(t, err) + require.NotNil(t, ctx) + require.Equal(t, tempDir, ctx.ProjectDirectory()) + require.Equal(t, azureYmlPath, ctx.ProjectPath()) +} + +func TestNewAzdContextFromWd_InvalidPath(t *testing.T) { + // Test with a path that doesn't exist + ctx, err := NewAzdContextFromWd("/this/path/does/not/exist/at/all") + require.Error(t, err) + require.Nil(t, ctx) +} + +func TestProjectPath_WithFoundFile(t *testing.T) { + tempDir := t.TempDir() + azureYmlPath := filepath.Join(tempDir, "azure.yml") + err := os.WriteFile(azureYmlPath, []byte("name: test\n"), 0600) + require.NoError(t, err) + + ctx, err := NewAzdContextFromWd(tempDir) + require.NoError(t, err) + + // ProjectPath should return the actual found file + require.Equal(t, azureYmlPath, ctx.ProjectPath()) +} + +func TestProjectPath_WithoutFoundFile(t *testing.T) { + // Create context directly without searching + ctx := NewAzdContextWithDirectory("/some/path") + + // ProjectPath should return default azure.yaml + expected := filepath.Join("/some/path", "azure.yaml") + require.Equal(t, expected, ctx.ProjectPath()) +} + +func TestNewAzdContextWithDirectory(t *testing.T) { + testDir := "/test/directory" + ctx := NewAzdContextWithDirectory(testDir) + + require.NotNil(t, ctx) + require.Equal(t, testDir, ctx.ProjectDirectory()) + require.Equal(t, filepath.Join(testDir, "azure.yaml"), ctx.ProjectPath()) +} + +func TestSetProjectDirectory(t *testing.T) { + ctx := NewAzdContextWithDirectory("/original/path") + require.Equal(t, "/original/path", ctx.ProjectDirectory()) + + ctx.SetProjectDirectory("/new/path") + require.Equal(t, "/new/path", ctx.ProjectDirectory()) +} + +func TestEnvironmentDirectory(t *testing.T) { + ctx := NewAzdContextWithDirectory("/test/path") + expected := filepath.Join("/test/path", ".azure") + require.Equal(t, expected, ctx.EnvironmentDirectory()) +} + +func TestEnvironmentRoot(t *testing.T) { + ctx := NewAzdContextWithDirectory("/test/path") + expected := filepath.Join("/test/path", ".azure", "env1") + require.Equal(t, expected, ctx.EnvironmentRoot("env1")) +} + +func TestGetEnvironmentWorkDirectory(t *testing.T) { + ctx := NewAzdContextWithDirectory("/test/path") + expected := filepath.Join("/test/path", ".azure", "env1", "wd") + require.Equal(t, expected, ctx.GetEnvironmentWorkDirectory("env1")) +} + +func TestProjectFileNames_Order(t *testing.T) { + // Verify the order of preference + require.Len(t, ProjectFileNames, 2) + require.Equal(t, "azure.yaml", ProjectFileNames[0], "azure.yaml should be first (highest priority)") + require.Equal(t, "azure.yml", ProjectFileNames[1], "azure.yml should be second") +} + +func TestProjectName(t *testing.T) { + tests := []struct { + name string + inputDir string + expected string + }{ + { + name: "Simple directory name", + inputDir: "/home/user/my-project", + expected: "my-project", + }, + { + name: "Directory with special characters", + inputDir: "/home/user/My_Project-123", + expected: "my-project-123", + }, + { + name: "Root directory", + inputDir: "/", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ProjectName(tt.inputDir) + require.Equal(t, tt.expected, result) + }) + } +}