-
Notifications
You must be signed in to change notification settings - Fork 6
Fix config precedence: respect local configuration in multi-blueprint workflows #170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Co-authored-by: sellakumaran <147754920+sellakumaran@users.noreply.github.com>
Co-authored-by: sellakumaran <147754920+sellakumaran@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This pull request fixes a critical configuration precedence issue where the CLI was unconditionally syncing state to the global directory, causing configuration from different projects to overwrite each other. The fix modifies ConfigService.SaveStateAsync() to enforce proper precedence by saving state locally when in a project directory (identified by the presence of a365.config.json) and only using the global directory when running outside a project context.
Changes:
- Modified
SaveStateAsyncto check for local static config presence before deciding where to save state - Added logic to save state locally only when in a project directory, preventing cross-contamination
- Added two comprehensive tests verifying local-only and global-only save behavior
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigService.cs | Implemented conditional save logic based on presence of local static config file |
| src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/Agent365ConfigServiceTests.cs | Added tests for local-only save (with static config) and global-only save (without static config) |
| [Fact] | ||
| public async Task SaveStateAsync_SavesLocallyWhenStaticConfigExists() | ||
| { | ||
| // Arrange - Create a project directory with a static config | ||
| var projectDir = Path.Combine(Path.GetTempPath(), $"agent365-project-{Guid.NewGuid()}"); | ||
| Directory.CreateDirectory(projectDir); | ||
|
|
||
| try | ||
| { | ||
| var originalDir = Environment.CurrentDirectory; | ||
| Environment.CurrentDirectory = projectDir; | ||
|
|
||
| try | ||
| { | ||
| // Create a static config file in the project directory | ||
| var staticConfigPath = Path.Combine(projectDir, ConfigConstants.DefaultConfigFileName); | ||
| var staticConfig = new | ||
| { | ||
| tenantId = "12345678-1234-1234-1234-123456789012", | ||
| subscriptionId = "87654321-4321-4321-4321-210987654321", | ||
| resourceGroup = "rg-test", | ||
| location = "eastus", | ||
| appServicePlanName = "asp-test", | ||
| webAppName = "webapp-test", | ||
| agentIdentityDisplayName = "Test Agent" | ||
| }; | ||
| await File.WriteAllTextAsync(staticConfigPath, JsonSerializer.Serialize(staticConfig, new JsonSerializerOptions { WriteIndented = true })); | ||
|
|
||
| // Create a config to save | ||
| var config = new Agent365Config { TenantId = "12345678-1234-1234-1234-123456789012" }; | ||
| config.AgentBlueprintId = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"; | ||
|
|
||
| // Get global config path to verify it's NOT written there | ||
| var globalDir = ConfigService.GetGlobalConfigDirectory(); | ||
| var globalStatePath = Path.Combine(globalDir, ConfigConstants.DefaultStateFileName); | ||
|
|
||
| // Delete global state if it exists to ensure clean test | ||
| if (File.Exists(globalStatePath)) | ||
| { | ||
| File.Delete(globalStatePath); | ||
| } | ||
|
|
||
| // Act - Save state (should go to local directory, NOT global) | ||
| await _service.SaveStateAsync(config, ConfigConstants.DefaultStateFileName); | ||
|
|
||
| // Assert - State should be saved locally | ||
| var localStatePath = Path.Combine(projectDir, ConfigConstants.DefaultStateFileName); | ||
| Assert.True(File.Exists(localStatePath), "Local state file should exist in project directory"); | ||
|
|
||
| var localContent = await File.ReadAllTextAsync(localStatePath); | ||
| Assert.Contains("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", localContent); | ||
|
|
||
| // Assert - State should NOT be saved to global directory | ||
| Assert.False(File.Exists(globalStatePath), "Global state file should NOT exist when saving in a project directory"); | ||
| } | ||
| finally | ||
| { | ||
| Environment.CurrentDirectory = originalDir; | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| if (Directory.Exists(projectDir)) | ||
| { | ||
| Directory.Delete(projectDir, recursive: true); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task SaveStateAsync_SavesGloballyWhenNoStaticConfigExists() | ||
| { | ||
| // Arrange - Use a directory without a static config | ||
| var tempDir = Path.Combine(Path.GetTempPath(), $"agent365-noproj-{Guid.NewGuid()}"); | ||
| Directory.CreateDirectory(tempDir); | ||
|
|
||
| try | ||
| { | ||
| var originalDir = Environment.CurrentDirectory; | ||
| Environment.CurrentDirectory = tempDir; | ||
|
|
||
| try | ||
| { | ||
| // Create a config to save | ||
| var config = new Agent365Config { TenantId = "12345678-1234-1234-1234-123456789012" }; | ||
| config.AgentBlueprintId = "bbbbbbbb-cccc-dddd-eeee-ffffffffffff"; | ||
|
|
||
| // Get global config path | ||
| var globalDir = ConfigService.GetGlobalConfigDirectory(); | ||
| var globalStatePath = Path.Combine(globalDir, ConfigConstants.DefaultStateFileName); | ||
|
|
||
| // Delete global state if it exists to ensure clean test | ||
| if (File.Exists(globalStatePath)) | ||
| { | ||
| File.Delete(globalStatePath); | ||
| } | ||
|
|
||
| // Act - Save state (should go to global directory, NOT local) | ||
| await _service.SaveStateAsync(config, ConfigConstants.DefaultStateFileName); | ||
|
|
||
| // Assert - State should be saved globally | ||
| Assert.True(File.Exists(globalStatePath), "Global state file should exist when no local config present"); | ||
|
|
||
| var globalContent = await File.ReadAllTextAsync(globalStatePath); | ||
| Assert.Contains("bbbbbbbb-cccc-dddd-eeee-ffffffffffff", globalContent); | ||
|
|
||
| // Assert - State should NOT be saved to current directory | ||
| var localStatePath = Path.Combine(tempDir, ConfigConstants.DefaultStateFileName); | ||
| Assert.False(File.Exists(localStatePath), "Local state file should NOT exist when no static config present"); | ||
| } | ||
| finally | ||
| { | ||
| Environment.CurrentDirectory = originalDir; | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| if (Directory.Exists(tempDir)) | ||
| { | ||
| Directory.Delete(tempDir, recursive: true); | ||
| } | ||
| } | ||
| } |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These new tests modify Environment.CurrentDirectory, which is shared global state. According to the project's testing guidelines, tests that modify shared state must disable parallelization to avoid race conditions. The tests should use the same pattern as ConfigCommandTests and ConfigCommandStaticDynamicSeparationTests, which are in the "ConfigTests" collection with DisableParallelization = true. Add [Collection("ConfigTests")] attribute to the Agent365ConfigServiceTests class and ensure a ConfigTestCollection definition exists (it already exists in ConfigCommandTests.cs).
| [Fact] | ||
| public async Task SaveStateAsync_SavesGloballyWhenNoStaticConfigExists() | ||
| { | ||
| // Arrange - Use a directory without a static config | ||
| var tempDir = Path.Combine(Path.GetTempPath(), $"agent365-noproj-{Guid.NewGuid()}"); | ||
| Directory.CreateDirectory(tempDir); | ||
|
|
||
| try | ||
| { | ||
| var originalDir = Environment.CurrentDirectory; | ||
| Environment.CurrentDirectory = tempDir; | ||
|
|
||
| try | ||
| { | ||
| // Create a config to save | ||
| var config = new Agent365Config { TenantId = "12345678-1234-1234-1234-123456789012" }; | ||
| config.AgentBlueprintId = "bbbbbbbb-cccc-dddd-eeee-ffffffffffff"; | ||
|
|
||
| // Get global config path | ||
| var globalDir = ConfigService.GetGlobalConfigDirectory(); | ||
| var globalStatePath = Path.Combine(globalDir, ConfigConstants.DefaultStateFileName); | ||
|
|
||
| // Delete global state if it exists to ensure clean test | ||
| if (File.Exists(globalStatePath)) | ||
| { | ||
| File.Delete(globalStatePath); | ||
| } | ||
|
|
||
| // Act - Save state (should go to global directory, NOT local) | ||
| await _service.SaveStateAsync(config, ConfigConstants.DefaultStateFileName); | ||
|
|
||
| // Assert - State should be saved globally | ||
| Assert.True(File.Exists(globalStatePath), "Global state file should exist when no local config present"); | ||
|
|
||
| var globalContent = await File.ReadAllTextAsync(globalStatePath); | ||
| Assert.Contains("bbbbbbbb-cccc-dddd-eeee-ffffffffffff", globalContent); | ||
|
|
||
| // Assert - State should NOT be saved to current directory | ||
| var localStatePath = Path.Combine(tempDir, ConfigConstants.DefaultStateFileName); | ||
| Assert.False(File.Exists(localStatePath), "Local state file should NOT exist when no static config present"); | ||
| } | ||
| finally | ||
| { | ||
| Environment.CurrentDirectory = originalDir; | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| if (Directory.Exists(tempDir)) | ||
| { | ||
| Directory.Delete(tempDir, recursive: true); | ||
| } | ||
| } | ||
| } |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test writes to the global configuration directory but doesn't clean up the created file afterward. While the test deletes the file before running (line 303-306) to ensure a clean state, it's better practice to also clean up in the finally block. Add cleanup of the global state file in the finally block to avoid leaving test artifacts in the global directory.
Problem
SaveStateAsyncunconditionally synced to global directory (%LOCALAPPDATA%\Microsoft.Agents.A365.DevTools.Cli), causing state from different projects to overwrite each other. Users working with multiple blueprints had to manually delete global config between context switches.Changes
Modified
ConfigService.SaveStateAsync()to enforce proper precedence:a365.config.json): Save state locally only, no global syncImplementation
Tests
Added verification for:
Impact
Enables independent configuration per project without manual cleanup. Global directory now serves its intended purpose: portability for non-project CLI invocations.
Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
graph.microsoft.com/usr/bin/../../opt/az/bin/python3 /usr/bin/../../opt/az/bin/python3 -Im azure.cli rest --method GET --url REDACTED$filter=appId eq 'a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6'&$select=id --headers Authorization=Bearer fake-token-123(dns block)/usr/bin/../../opt/az/bin/python3 /usr/bin/../../opt/az/bin/python3 -Im azure.cli rest --method GET --url REDACTED$filter=appId eq 'a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6'&$select=id --headers Authorization=Bearer fake-token-123 e.cs ator.cs e.cs�� Locator.cs onsoleFormatter.cs(dns block)/usr/bin/../../opt/az/bin/python3 /usr/bin/../../opt/az/bin/python3 -Im azure.cli rest --method PATCH --url REDACTED --headers Content-Type=application/json Authorization=Bearer fake-token-123 --body {"publicClient":{"redirectUris":["http://localhost","http://localhost:8400/","ms-appx-web://microsoft.aad.brokerplugin/a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6"]}}(dns block)login.microsoftonline.com/usr/bin/../../opt/az/bin/python3 /usr/bin/../../opt/az/bin/python3 -Im azure.cli login --tenant 12345678-1234-1234-1234-123456789012(dns block)Co-authored-by: sellakumaran <147754920+sellakumaran@users.noreply.github.com> = get && echo "password=$GITHUB_TOKEN"; }; f` (dns block)
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.