Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 22, 2026

Problem

SaveStateAsync unconditionally 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:

  • Project directories (containing a365.config.json): Save state locally only, no global sync
  • Non-project directories: Save to global for CLI portability
  • Absolute paths: Use as-is for explicit control

Implementation

// Check for local static config presence
var staticConfigPath = Path.Combine(Environment.CurrentDirectory, ConfigConstants.DefaultConfigFileName);
bool hasLocalStaticConfig = File.Exists(staticConfigPath);

if (hasLocalStaticConfig)
{
    // Project directory - local state only
    await File.WriteAllTextAsync(currentDirPath, json);
}
else
{
    // Non-project - global for portability
    await SyncConfigToGlobalDirectoryAsync(statePath, json);
}

Tests

Added verification for:

  • Local-only save when static config exists
  • Global save when no local config present
  • No cross-contamination between projects

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
    • Triggering command: /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)
    • Triggering command: /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)
    • Triggering command: /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
    • Triggering command: /usr/bin/../../opt/az/bin/python3 /usr/bin/../../opt/az/bin/python3 -Im azure.cli login --tenant 12345678-1234-1234-1234-123456789012 (dns block)
    • Triggering command: `/usr/bin/../../opt/az/bin/python3 /usr/bin/../../opt/az/bin/python3 -Im azure.cli login --tenant 12345678-1234-1234-1234-123456789012 s global

Co-authored-by: sellakumaran <147754920+sellakumaran@users.noreply.github.com> = get && echo "password=$GITHUB_TOKEN"; }; f` (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Original prompt

This section details on the original issue you should resolve

<issue_title>a365 CLI does not respect local configration when trying to create multiple blueprints</issue_title>
<issue_description>Once A365 CLI creates a blueprint, its storing a config file in %LOCALAPPDATA%\Microsoft.Agents.A365.DevTools.Cli.
if I change my local config, or create a separate config in another directory via copy and modify, it ignores my local settings and config file and attempts to use the one in the %LOCALAPPDATA%\Microsoft.Agents.A365.DevTools.Cli directory.

In order to create a 2 blueprints on the same machine and be able to work with them, I must delete the content of the %LOCALAPPDATA%\Microsoft.Agents.A365.DevTools.Cli each time I switch contexts.

Would recommend using %LOCALAPPDATA%\Microsoft.Agents.A365.DevTools.Cli only for token cache \ logs and remove all other configuration writing from there to allow the developer to manage configurations in the file system directly. </issue_description>

<agent_instructions>Can you please review the code base for this issue?

We already have fixed this issue in the past and maybe there are some cases that are not fixed fully yet.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@sellakumaran Thank you @MattB-msft for reporting the issue. The local config should always override the LOCALAPPDATA version. I will take a look at the issue and get back.

💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 2 commits January 22, 2026 01:01
Co-authored-by: sellakumaran <147754920+sellakumaran@users.noreply.github.com>
Co-authored-by: sellakumaran <147754920+sellakumaran@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix A365 CLI to respect local configuration for blueprints Fix config precedence: respect local configuration in multi-blueprint workflows Jan 22, 2026
Copilot AI requested a review from sellakumaran January 22, 2026 01:12
@sellakumaran sellakumaran marked this pull request as ready for review January 23, 2026 23:25
@sellakumaran sellakumaran requested review from a team as code owners January 23, 2026 23:25
Copilot AI review requested due to automatic review settings January 23, 2026 23:25
Copy link
Contributor

Copilot AI left a 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 SaveStateAsync to 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)

Comment on lines +211 to +333
[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);
}
}
}
Copy link

Copilot AI Jan 23, 2026

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).

Copilot uses AI. Check for mistakes.
Comment on lines +280 to +333
[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);
}
}
}
Copy link

Copilot AI Jan 23, 2026

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

a365 CLI does not respect local configration when trying to create multiple blueprints

3 participants