Skip to content

Commit f3ff96d

Browse files
author
Meghanshu Bhatt
committed
merge pass approach and unit tests
1 parent 3f44957 commit f3ff96d

2 files changed

Lines changed: 159 additions & 16 deletions

File tree

src/Microsoft.Agents.A365.DevTools.Cli/Commands/SetupSubcommands/BlueprintSubcommand.cs

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -488,23 +488,16 @@ public static async Task<BlueprintCreationResult> CreateBlueprintImplementationA
488488
logger.LogDebug("Blueprint created: {Name} (Object ID: {ObjectId}, App ID: {AppId})",
489489
setupConfig.AgentBlueprintDisplayName, blueprintObjectId, blueprintAppId);
490490

491-
// Convert to camelCase and save
492-
var camelCaseConfig = new JsonObject
493-
{
494-
["managedIdentityPrincipalId"] = generatedConfig["managedIdentityPrincipalId"]?.DeepClone(),
495-
["agentBlueprintId"] = blueprintAppId,
496-
["agentBlueprintObjectId"] = blueprintObjectId,
497-
["displayName"] = setupConfig.AgentBlueprintDisplayName,
498-
["servicePrincipalId"] = blueprintResult.servicePrincipalId,
499-
["identifierUri"] = $"api://{blueprintAppId}",
500-
["tenantId"] = setupConfig.TenantId,
501-
["resourceConsents"] = generatedConfig["resourceConsents"]?.DeepClone() ?? new JsonArray(),
502-
["agentBlueprintClientSecret"] = generatedConfig["agentBlueprintClientSecret"]?.DeepClone(),
503-
["agentBlueprintClientSecretProtected"] = generatedConfig["agentBlueprintClientSecretProtected"]?.DeepClone(),
504-
};
491+
// Update generated config with blueprint details, preserving all existing fields
492+
generatedConfig["agentBlueprintId"] = blueprintAppId;
493+
generatedConfig["agentBlueprintObjectId"] = blueprintObjectId;
494+
generatedConfig["agentBlueprintServicePrincipalObjectId"] = blueprintResult.servicePrincipalId;
495+
if (generatedConfig["resourceConsents"] == null)
496+
{
497+
generatedConfig["resourceConsents"] = new JsonArray();
498+
}
505499

506-
await File.WriteAllTextAsync(generatedConfigPath, camelCaseConfig.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), cancellationToken);
507-
generatedConfig = camelCaseConfig;
500+
await File.WriteAllTextAsync(generatedConfigPath, generatedConfig.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), cancellationToken);
508501

509502
// ========================================================================
510503
// Phase 2.5: Create Client Secret (logging handled by method)

src/Tests/Microsoft.Agents.A365.DevTools.Cli.Tests/Commands/BlueprintSubcommandTests.cs

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
using System.CommandLine.Builder;
1212
using System.CommandLine.IO;
1313
using System.CommandLine.Parsing;
14+
using System.Text.Json;
15+
using System.Text.Json.Nodes;
1416
using Xunit;
1517

1618
namespace Microsoft.Agents.A365.DevTools.Cli.Tests.Commands;
@@ -1824,4 +1826,152 @@ public void ValidateMutuallyExclusiveOptions_WithCompatibleOptions_ShouldReturnT
18241826
}
18251827

18261828
#endregion
1829+
1830+
#region Generated Config Merge Preservation Tests
1831+
1832+
/// <summary>
1833+
/// Verifies that the blueprint intermediate save pattern (merge into existing JsonObject)
1834+
/// preserves all pre-existing fields such as agentBlueprintClientSecret, botId, etc.
1835+
/// Regression test for bug where a new JsonObject replaced the existing config,
1836+
/// dropping fields not explicitly listed in the allowlist.
1837+
/// </summary>
1838+
[Fact]
1839+
public async Task BlueprintIntermediateSave_ShouldPreserveExistingGeneratedConfigFields()
1840+
{
1841+
// Arrange - simulate a generated config with fields set by other subcommands
1842+
var tempDir = Path.Combine(Path.GetTempPath(), $"a365test_{Guid.NewGuid():N}");
1843+
Directory.CreateDirectory(tempDir);
1844+
var generatedConfigPath = Path.Combine(tempDir, "a365.generated.config.json");
1845+
1846+
try
1847+
{
1848+
var existingConfig = new JsonObject
1849+
{
1850+
["managedIdentityPrincipalId"] = "msi-principal-id-123",
1851+
["agentBlueprintId"] = "old-blueprint-id",
1852+
["agentBlueprintObjectId"] = "old-object-id",
1853+
["agentBlueprintServicePrincipalObjectId"] = "old-sp-id",
1854+
["agentBlueprintClientSecret"] = "encrypted-secret-value",
1855+
["agentBlueprintClientSecretProtected"] = true,
1856+
["botId"] = "bot-id-456",
1857+
["botMsaAppId"] = "bot-msa-app-id-789",
1858+
["botMessagingEndpoint"] = "https://myapp.azurewebsites.net/api/messages",
1859+
["completed"] = true,
1860+
["completedAt"] = "2026-01-01T00:00:00Z",
1861+
["resourceConsents"] = new JsonArray
1862+
{
1863+
new JsonObject { ["resourceName"] = "Microsoft Graph", ["consentGranted"] = true }
1864+
}
1865+
};
1866+
1867+
await File.WriteAllTextAsync(generatedConfigPath, existingConfig.ToJsonString(
1868+
new JsonSerializerOptions { WriteIndented = true }));
1869+
1870+
// Act - simulate the merge pattern used in BlueprintSubcommand
1871+
var generatedConfig = JsonNode.Parse(
1872+
await File.ReadAllTextAsync(generatedConfigPath))?.AsObject() ?? new JsonObject();
1873+
1874+
var newBlueprintAppId = "new-blueprint-app-id";
1875+
var newBlueprintObjectId = "new-object-id";
1876+
var newServicePrincipalId = "new-sp-id";
1877+
1878+
// This is the exact pattern from the fix
1879+
generatedConfig["agentBlueprintId"] = newBlueprintAppId;
1880+
generatedConfig["agentBlueprintObjectId"] = newBlueprintObjectId;
1881+
generatedConfig["agentBlueprintServicePrincipalObjectId"] = newServicePrincipalId;
1882+
if (generatedConfig["resourceConsents"] == null)
1883+
{
1884+
generatedConfig["resourceConsents"] = new JsonArray();
1885+
}
1886+
1887+
await File.WriteAllTextAsync(generatedConfigPath, generatedConfig.ToJsonString(
1888+
new JsonSerializerOptions { WriteIndented = true }));
1889+
1890+
// Assert - read back and verify ALL fields are preserved
1891+
var savedConfig = JsonNode.Parse(await File.ReadAllTextAsync(generatedConfigPath))!.AsObject();
1892+
1893+
// Updated fields should have new values
1894+
savedConfig["agentBlueprintId"]!.GetValue<string>().Should().Be("new-blueprint-app-id");
1895+
savedConfig["agentBlueprintObjectId"]!.GetValue<string>().Should().Be("new-object-id");
1896+
savedConfig["agentBlueprintServicePrincipalObjectId"]!.GetValue<string>().Should().Be("new-sp-id");
1897+
1898+
// Pre-existing fields must be preserved (the bug would wipe these)
1899+
savedConfig["agentBlueprintClientSecret"]!.GetValue<string>().Should().Be("encrypted-secret-value");
1900+
savedConfig["agentBlueprintClientSecretProtected"]!.GetValue<bool>().Should().BeTrue();
1901+
savedConfig["botId"]!.GetValue<string>().Should().Be("bot-id-456");
1902+
savedConfig["botMsaAppId"]!.GetValue<string>().Should().Be("bot-msa-app-id-789");
1903+
savedConfig["botMessagingEndpoint"]!.GetValue<string>().Should().Be("https://myapp.azurewebsites.net/api/messages");
1904+
savedConfig["managedIdentityPrincipalId"]!.GetValue<string>().Should().Be("msi-principal-id-123");
1905+
savedConfig["completed"]!.GetValue<bool>().Should().BeTrue();
1906+
savedConfig["completedAt"]!.GetValue<string>().Should().Be("2026-01-01T00:00:00Z");
1907+
1908+
// Resource consents should be preserved
1909+
savedConfig["resourceConsents"]!.AsArray().Should().HaveCount(1);
1910+
savedConfig["resourceConsents"]![0]!["resourceName"]!.GetValue<string>().Should().Be("Microsoft Graph");
1911+
}
1912+
finally
1913+
{
1914+
// Cleanup temp directory
1915+
if (Directory.Exists(tempDir))
1916+
{
1917+
Directory.Delete(tempDir, recursive: true);
1918+
}
1919+
}
1920+
}
1921+
1922+
/// <summary>
1923+
/// Verifies that the merge pattern initializes resourceConsents when it does not exist.
1924+
/// </summary>
1925+
[Fact]
1926+
public async Task BlueprintIntermediateSave_ShouldInitializeResourceConsents_WhenNull()
1927+
{
1928+
// Arrange - config without resourceConsents
1929+
var tempDir = Path.Combine(Path.GetTempPath(), $"a365test_{Guid.NewGuid():N}");
1930+
Directory.CreateDirectory(tempDir);
1931+
var generatedConfigPath = Path.Combine(tempDir, "a365.generated.config.json");
1932+
1933+
try
1934+
{
1935+
var existingConfig = new JsonObject
1936+
{
1937+
["managedIdentityPrincipalId"] = "msi-id",
1938+
["agentBlueprintClientSecret"] = "secret-123"
1939+
};
1940+
1941+
await File.WriteAllTextAsync(generatedConfigPath, existingConfig.ToJsonString(
1942+
new JsonSerializerOptions { WriteIndented = true }));
1943+
1944+
// Act
1945+
var generatedConfig = JsonNode.Parse(
1946+
await File.ReadAllTextAsync(generatedConfigPath))?.AsObject() ?? new JsonObject();
1947+
1948+
generatedConfig["agentBlueprintId"] = "app-id";
1949+
generatedConfig["agentBlueprintObjectId"] = "obj-id";
1950+
generatedConfig["agentBlueprintServicePrincipalObjectId"] = "sp-id";
1951+
if (generatedConfig["resourceConsents"] == null)
1952+
{
1953+
generatedConfig["resourceConsents"] = new JsonArray();
1954+
}
1955+
1956+
await File.WriteAllTextAsync(generatedConfigPath, generatedConfig.ToJsonString(
1957+
new JsonSerializerOptions { WriteIndented = true }));
1958+
1959+
// Assert
1960+
var savedConfig = JsonNode.Parse(await File.ReadAllTextAsync(generatedConfigPath))!.AsObject();
1961+
1962+
savedConfig["resourceConsents"].Should().NotBeNull();
1963+
savedConfig["resourceConsents"]!.AsArray().Should().BeEmpty();
1964+
savedConfig["agentBlueprintClientSecret"]!.GetValue<string>().Should().Be("secret-123");
1965+
savedConfig["managedIdentityPrincipalId"]!.GetValue<string>().Should().Be("msi-id");
1966+
}
1967+
finally
1968+
{
1969+
if (Directory.Exists(tempDir))
1970+
{
1971+
Directory.Delete(tempDir, recursive: true);
1972+
}
1973+
}
1974+
}
1975+
1976+
#endregion
18271977
}

0 commit comments

Comments
 (0)