diff --git a/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigService.cs b/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigService.cs index fee5718b..332e0003 100644 --- a/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigService.cs +++ b/src/Microsoft.Agents.A365.DevTools.Cli/Services/ConfigService.cs @@ -349,15 +349,35 @@ public async Task SaveStateAsync( // Serialize to JSON var json = JsonSerializer.Serialize(dynamicData, DefaultJsonOptions); - // Only update in current directory if it already exists - var currentDirPath = Path.Combine(Environment.CurrentDirectory, statePath); - if (File.Exists(currentDirPath)) + // If an absolute path is provided, use it directly (for testing and explicit control) + if (Path.IsPathRooted(statePath)) { try { - // Save the state to the local current directory + await File.WriteAllTextAsync(statePath, json); + _logger?.LogDebug("Saved dynamic state to absolute path: {StatePath}", statePath); + return; + } + catch (Exception ex) + { + _logger?.LogError(ex, "Failed to save dynamic state to: {StatePath}", statePath); + throw; + } + } + + // For relative paths, check if we're in a project directory (has local static config) + var staticConfigPath = Path.Combine(Environment.CurrentDirectory, ConfigConstants.DefaultConfigFileName); + bool hasLocalStaticConfig = File.Exists(staticConfigPath); + + if (hasLocalStaticConfig) + { + // We're in a project directory - save state locally only + // This ensures each project maintains its own independent configuration + var currentDirPath = Path.Combine(Environment.CurrentDirectory, statePath); + try + { await File.WriteAllTextAsync(currentDirPath, json); - _logger?.LogDebug("Saved dynamic state to: {StatePath}", currentDirPath); + _logger?.LogDebug("Saved dynamic state to local project directory: {StatePath}", currentDirPath); } catch (Exception ex) { @@ -365,9 +385,13 @@ public async Task SaveStateAsync( throw; } } - - // Always sync to global directory for portability - await SyncConfigToGlobalDirectoryAsync(statePath, json, throwOnError: true); + else + { + // Not in a project directory - save to global directory for portability + // This allows CLI commands to work when run from any directory + await SyncConfigToGlobalDirectoryAsync(statePath, json, throwOnError: true); + _logger?.LogDebug("Saved dynamic state to global directory (no local static config found)"); + } } /// diff --git a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/Agent365ConfigServiceTests.cs b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/Agent365ConfigServiceTests.cs index 4a4ada3a..c4c5d1ce 100644 --- a/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/Agent365ConfigServiceTests.cs +++ b/src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Services/Agent365ConfigServiceTests.cs @@ -208,6 +208,130 @@ public async Task SaveStateAsync_OverwritesExistingFile() Assert.DoesNotContain("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", secondContent); } + [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); + } + } + } + #endregion #region ValidateAsync Tests