Repository: microsoft/kiota (csharp)

Issue:
Kiota shouldn't create an empty declarativeCopilots array when generating an API plugin
Kiota 1.16.0+f7ce8b88eefa1c00cb6f87dd48335baac96d2cc9

```powershell
kiota plugin add --openapi ./openapi/openapi.yml --plugin-name "Budget Tracker" --type APIPlugin --output ./plugin
```

Resulting manifest.json snippet:

```json
"copilotExtensions": {
  "plugins": [
    {
      "id": "Budget Tracker",
      "file": "budget tracker-apiplugin.json"
    }
  ],
  "declarativeCopilots": []
}
```

Zipping this and attempting to upload to Teams dev portal gives the following error:

```
copilotExtensions.declarativeCopilots Array item count 0 is less than minimum count of 1.
```

Removing the empty `declarativeCopilots` allows the upload to work.

Pull request:
fixes #4974
follow up to #4937

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4214f5e20b..09d1aac844 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### Changed
 
 - Fixed a bug in dotnet where CS1587 warnings are generated in generated enums with descriptions [#4957](https://github.com/microsoft/kiota/issues/4957)
+- Fixed a bug where the copilot teams toolkit integration would serialize empty declarative copilots. [#4974](https://github.com/microsoft/kiota/issues/4974)
 
 ## [1.16.0] - 2024-07-05
 
diff --git a/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs b/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs
index d277469cb4..1cdab12172 100644
--- a/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs
+++ b/src/Kiota.Builder/Plugins/Models/AppManifestModel.cs
@@ -144,10 +144,16 @@ public string? Outline
 
 internal class CopilotExtensions
 {
-    public IList<Plugin> Plugins { get; set; } = [];
-    public IList<DeclarativeCopilot> DeclarativeCopilots { get; set; } = [];
+    public IList<Plugin>? Plugins
+    {
+        get; set;
+    }
+    public IList<DeclarativeCopilot>? DeclarativeCopilots
+    {
+        get; set;
+    }
     [JsonExtensionData]
-    public Dictionary<string, Object> AdditionalData { get; set; } = new();
+    public Dictionary<string, object> AdditionalData { get; set; } = [];
 }
 
 internal class Plugin
diff --git a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
index 0e38aa92b9..0bb6641c57 100644
--- a/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
+++ b/src/Kiota.Builder/Plugins/PluginsGenerationService.cs
@@ -152,12 +152,13 @@ private async Task<AppManifestModel> GetAppManifestModelAsync(string pluginFileN
 
         manifestModel.CopilotExtensions ??= new CopilotExtensions();// ensure its not null.
 
-        if (manifestModel.CopilotExtensions.Plugins.FirstOrDefault(pluginItem => Configuration.ClientClassName.Equals(pluginItem.Id, StringComparison.OrdinalIgnoreCase)) is { } plugin)
+        if (manifestModel.CopilotExtensions.Plugins is not null && manifestModel.CopilotExtensions.Plugins.FirstOrDefault(pluginItem => Configuration.ClientClassName.Equals(pluginItem.Id, StringComparison.OrdinalIgnoreCase)) is { } plugin)
         {
-            plugin.File = pluginFileName; // id is already consitent so make sure the file name is ok
+            plugin.File = pluginFileName; // id is already consistent so make sure the file name is ok
         }
         else
         {
+            manifestModel.CopilotExtensions.Plugins ??= [];
             // Add a new plugin entry
             manifestModel.CopilotExtensions.Plugins.Add(new Plugin
             {


Test patch:
diff --git a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
index 3390e7d49e..c04055de9e 100644
--- a/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
+++ b/tests/Kiota.Builder.Tests/Plugins/PluginsGenerationServiceTests.cs
@@ -112,11 +112,13 @@ public async Task GeneratesManifest()
 
         // Validate the manifest file
         var appManifestFile = await File.ReadAllTextAsync(Path.Combine(outputDirectory, AppManifestFileName));
-        var appManifestModelObject = JsonSerializer.Deserialize<AppManifestModel>(appManifestFile, PluginsGenerationService.AppManifestModelGenerationContext.AppManifestModel);
+        var appManifestModelObject = JsonSerializer.Deserialize(appManifestFile, PluginsGenerationService.AppManifestModelGenerationContext.AppManifestModel);
         Assert.Equal("com.microsoft.kiota.plugin.client", appManifestModelObject.PackageName);
         Assert.Equal("client", appManifestModelObject.Name.ShortName);
         Assert.Equal("Microsoft Kiota.", appManifestModelObject.Developer.Name);
         Assert.Equal("color.png", appManifestModelObject.Icons.Color);
+        Assert.NotNull(appManifestModelObject.CopilotExtensions.Plugins);
+        Assert.Single(appManifestModelObject.CopilotExtensions.Plugins);
         Assert.Equal("client", appManifestModelObject.CopilotExtensions.Plugins[0].Id);
         Assert.Equal(ManifestFileName, appManifestModelObject.CopilotExtensions.Plugins[0].File);
     }
@@ -505,10 +507,13 @@ public async Task GeneratesManifestAndUpdatesExistingAppManifest()
 
         // Validate the manifest file
         var appManifestFile = await File.ReadAllTextAsync(Path.Combine(outputDirectory, AppManifestFileName));
-        var appManifestModelObject = JsonSerializer.Deserialize<AppManifestModel>(appManifestFile, PluginsGenerationService.AppManifestModelGenerationContext.AppManifestModel);
+        var appManifestModelObject = JsonSerializer.Deserialize(appManifestFile, PluginsGenerationService.AppManifestModelGenerationContext.AppManifestModel);
         Assert.Null(appManifestModelObject.PackageName);// package wasn't present
         Assert.Equal("Name of your app", appManifestModelObject.Name.ShortName); // app name is same
         Assert.Equal("Publisher Name", originalAppManifestModelObject.Developer.Name); // developer name is same
+        Assert.NotNull(appManifestModelObject.CopilotExtensions);
+        Assert.NotNull(appManifestModelObject.CopilotExtensions.Plugins);
+        Assert.Single(appManifestModelObject.CopilotExtensions.Plugins);//one plugin present
         Assert.Equal("client", appManifestModelObject.CopilotExtensions.Plugins[0].Id);
         Assert.Equal(ManifestFileName, appManifestModelObject.CopilotExtensions.Plugins[0].File);
         var rootJsonElement = JsonDocument.Parse(appManifestFile).RootElement;
@@ -613,11 +618,13 @@ public async Task GeneratesManifestAndUpdatesExistingAppManifestWithExistingPlug
         // Assert manifest exists before generation and is parsable
         Assert.True(File.Exists(Path.Combine(outputDirectory, "manifest.json")));
         var originalManifestFile = await File.ReadAllTextAsync(Path.Combine(outputDirectory, AppManifestFileName));
-        var originalAppManifestModelObject = JsonSerializer.Deserialize<AppManifestModel>(originalManifestFile, PluginsGenerationService.AppManifestModelGenerationContext.AppManifestModel);
+        var originalAppManifestModelObject = JsonSerializer.Deserialize(originalManifestFile, PluginsGenerationService.AppManifestModelGenerationContext.AppManifestModel);
         Assert.Equal("com.microsoft.kiota.plugin.client", originalAppManifestModelObject.PackageName);// package was present
         Assert.NotNull(originalAppManifestModelObject.CopilotExtensions);
+        Assert.NotNull(originalAppManifestModelObject.CopilotExtensions.Plugins);
         Assert.Single(originalAppManifestModelObject.CopilotExtensions.Plugins);//one plugin present
         Assert.Equal("dummyFile.json", originalAppManifestModelObject.CopilotExtensions.Plugins[0].File); // no plugins present
+        Assert.NotNull(originalAppManifestModelObject.CopilotExtensions.DeclarativeCopilots);
         Assert.Single(originalAppManifestModelObject.CopilotExtensions.DeclarativeCopilots);// one declarative copilot present
         Assert.Equal("dummyFile.json", originalAppManifestModelObject.CopilotExtensions.DeclarativeCopilots[0].File); // no plugins present
 
@@ -632,7 +639,7 @@ public async Task GeneratesManifestAndUpdatesExistingAppManifestWithExistingPlug
 
         // Validate the manifest file
         var appManifestFile = await File.ReadAllTextAsync(Path.Combine(outputDirectory, AppManifestFileName));
-        var appManifestModelObject = JsonSerializer.Deserialize<AppManifestModel>(appManifestFile, PluginsGenerationService.AppManifestModelGenerationContext.AppManifestModel);
+        var appManifestModelObject = JsonSerializer.Deserialize(appManifestFile, PluginsGenerationService.AppManifestModelGenerationContext.AppManifestModel);
         Assert.Equal("com.microsoft.kiota.plugin.client", originalAppManifestModelObject.PackageName);// package was present
         Assert.Equal("client", appManifestModelObject.Name.ShortName); // app name is same
         Assert.Equal("Test Name", originalAppManifestModelObject.Developer.Name); // developer name is same
@@ -642,6 +649,24 @@ public async Task GeneratesManifestAndUpdatesExistingAppManifestWithExistingPlug
         Assert.Equal("dummyFile.json", appManifestModelObject.CopilotExtensions.DeclarativeCopilots[0].File); // no plugins present
     }
     [Fact]
+    public async Task DoesNotGenerateEmptyPluginOrDeclarativeCopilots()
+    {
+        var manifestModel = new AppManifestModel
+        {
+            CopilotExtensions = new CopilotExtensions
+            {
+            }
+        };
+
+        using var appManifestStream = new MemoryStream();
+        await JsonSerializer.SerializeAsync(appManifestStream, manifestModel, PluginsGenerationService.AppManifestModelGenerationContext.AppManifestModel);
+        appManifestStream.Seek(0, SeekOrigin.Begin);
+        var stringRepresentation = await new StreamReader(appManifestStream).ReadToEndAsync();
+
+        Assert.DoesNotContain("\"plugins\":", stringRepresentation);
+        Assert.DoesNotContain("\"declarativeCopilots\":", stringRepresentation);
+    }
+    [Fact]
     public async Task GeneratesManifestAndCleansUpInputDescription()
     {
         var simpleDescriptionContent = @"openapi: 3.0.0