diff --git a/packages/spec-parser/src/manifestUpdater.ts b/packages/spec-parser/src/manifestUpdater.ts index 4d5e7a4457..02ca0781d4 100644 --- a/packages/spec-parser/src/manifestUpdater.ts +++ b/packages/spec-parser/src/manifestUpdater.ts @@ -40,16 +40,29 @@ export class ManifestUpdater { ): Promise<[TeamsAppManifest, PluginManifestSchema, WarningResult[]]> { const manifest: TeamsAppManifest = await fs.readJSON(manifestPath); const apiPluginRelativePath = ManifestUpdater.getRelativePath(manifestPath, apiPluginFilePath); - manifest.copilotExtensions = manifest.copilotExtensions || {}; - // Insert plugins in manifest.json if it is plugin for Copilot. - if (!options.isGptPlugin) { - manifest.copilotExtensions.plugins = [ - { - file: apiPluginRelativePath, - id: ConstantString.DefaultPluginId, - }, - ]; - ManifestUpdater.updateManifestDescription(manifest, spec); + + if (manifest.copilotExtensions) { + if (!options.isGptPlugin) { + manifest.copilotExtensions.plugins = [ + { + file: apiPluginRelativePath, + id: ConstantString.DefaultPluginId, + }, + ]; + ManifestUpdater.updateManifestDescription(manifest, spec); + } + } else { + (manifest as any).copilotAgents = (manifest as any).copilotAgents || {}; + + if (!options.isGptPlugin) { + (manifest as any).copilotAgents.plugins = [ + { + file: apiPluginRelativePath, + id: ConstantString.DefaultPluginId, + }, + ]; + ManifestUpdater.updateManifestDescription(manifest, spec); + } } const appName = this.removeEnvs(manifest.name.short); diff --git a/packages/spec-parser/test/manifestUpdater.test.ts b/packages/spec-parser/test/manifestUpdater.test.ts index e2acbbcfe3..f3eea845de 100644 --- a/packages/spec-parser/test/manifestUpdater.test.ts +++ b/packages/spec-parser/test/manifestUpdater.test.ts @@ -62,7 +62,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -190,7 +190,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -348,7 +348,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -489,7 +489,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -679,7 +679,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -929,7 +929,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -1102,7 +1102,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -1251,7 +1251,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -1390,7 +1390,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -1534,7 +1534,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -1666,7 +1666,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -1827,7 +1827,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -1983,7 +1983,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -2110,7 +2110,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -2245,7 +2245,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -2368,7 +2368,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "Original Full Description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -2552,7 +2552,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "Original Full Description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -2704,7 +2704,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "Original Full Description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -2860,7 +2860,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -3035,7 +3035,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -3171,7 +3171,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -3337,7 +3337,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name${{TestEnv}}", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -3469,7 +3469,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -3557,7 +3557,7 @@ describe("updateManifestWithAiPlugin", () => { const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, - copilotExtensions: { + copilotAgents: { plugins: [ { file: "ai-plugin.json", @@ -3830,6 +3830,172 @@ describe("updateManifestWithAiPlugin", () => { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "Original Short Description", full: "Original Full Description" }, }; + const expectedManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: "My API", full: "My API description" }, + copilotAgents: { + plugins: [ + { + file: "ai-plugin.json", + id: "plugin_1", + }, + ], + }, + }; + const originalPluginManifest = { + $schema: ConstantString.PluginManifestSchema, + schema_version: "v2.1", + name_for_human: "Original Name", + namespace: "originalname", + description_for_human: "My API description", + runtimes: [ + { + type: "OpenApi", + auth: { + type: "None", + }, + spec: { + url: relativePath, + }, + run_for_functions: ["createPet"], + }, + ], + }; + + const expectedPlugins: PluginManifestSchema = { + $schema: ConstantString.PluginManifestSchema, + schema_version: "v2.1", + name_for_human: "Original Name", + namespace: "originalname", + description_for_human: "My API description", + functions: [ + { + name: "createPet", + description: "Create a new pet in the store", + }, + ], + runtimes: [ + { + type: "OpenApi", + auth: { + type: "None", + }, + spec: { + url: "spec/outputSpec.yaml", + }, + run_for_functions: ["createPet"], + }, + ], + }; + sinon.stub(fs, "readJSON").callsFake(async (path) => { + if (path === manifestPath) { + return Promise.resolve(originalManifest); + } else if (path === existingPluginManifestPath) { + return Promise.resolve(originalPluginManifest); + } else { + return Promise.resolve({}); + } + }); + sinon + .stub(fs, "pathExists") + .withArgs(manifestPath) + .resolves(true) + .withArgs(pluginFilePath) + .resolves(false) + .withArgs(existingPluginManifestPath) + .resolves(true); + + const options: ParseOptions = { + allowMethods: ["get", "post"], + }; + const [manifest, apiPlugin, warnings] = await ManifestUpdater.updateManifestWithAiPlugin( + manifestPath, + outputSpecPath, + pluginFilePath, + spec, + options, + undefined, + { + manifestPath: existingPluginManifestPath, + specPath: specPath, + } + ); + + expect(manifest).to.deep.equal(expectedManifest); + expect(apiPlugin).to.deep.equal(expectedPlugins); + expect(warnings).to.deep.equal([]); + }); + + it("should update existing manifest and use old copilotExtensions property if it exist", async () => { + const spec: any = { + openapi: "3.0.2", + info: { + title: "My API", + description: "My API description", + }, + servers: [ + { + url: "/v3", + }, + ], + paths: { + "/pets": { + post: { + operationId: "createPet", + summary: "Create a pet", + description: "Create a new pet in the store", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + description: "Name of the pet", + }, + age: { + type: "string", + description: "Date time of the pet", + format: "date-time", + }, + status: { + type: "string", + description: "Status of the pet", + enum: ["available", "pending", "sold"], + }, + arrayProp: { + type: "array", + items: { + type: "string", + description: "Prop of the pet", + format: "date-time", + default: "2021-01-01T00:00:00Z", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const manifestPath = "/path/to/your/manifest.json"; + const outputSpecPath = "/path/to/your/spec/outputSpec.yaml"; + const pluginFilePath = "/path/to/your/ai-plugin.json"; + const existingPluginManifestPath = "/path/to/your/pluginManifest.json"; + const specPath = "/path/to/your/spec.yaml"; + const relativePath = ManifestUpdater.getRelativePath(existingPluginManifestPath, specPath); + const originalManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: "Original Short Description", full: "Original Full Description" }, + copilotExtensions: { + plugins: [{ file: "ai-plugin-old.json", id: "plugin_1" }], + }, + }; const expectedManifest = { name: { short: "Original Name", full: "Original Full Name" }, description: { short: "My API", full: "My API description" }, @@ -3925,6 +4091,178 @@ describe("updateManifestWithAiPlugin", () => { expect(apiPlugin).to.deep.equal(expectedPlugins); expect(warnings).to.deep.equal([]); }); + + it("should not change manifest if it is declarative copilot", async () => { + const spec: any = { + openapi: "3.0.2", + info: { + title: "My API", + description: "My API description", + }, + servers: [ + { + url: "/v3", + }, + ], + paths: { + "/pets": { + post: { + operationId: "createPet", + summary: "Create a pet", + description: "Create a new pet in the store", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + required: ["name"], + properties: { + name: { + type: "string", + description: "Name of the pet", + }, + age: { + type: "string", + description: "Date time of the pet", + format: "date-time", + }, + status: { + type: "string", + description: "Status of the pet", + enum: ["available", "pending", "sold"], + }, + arrayProp: { + type: "array", + items: { + type: "string", + description: "Prop of the pet", + format: "date-time", + default: "2021-01-01T00:00:00Z", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }; + const manifestPath = "/path/to/your/manifest.json"; + const outputSpecPath = "/path/to/your/spec/outputSpec.yaml"; + const pluginFilePath = "/path/to/your/ai-plugin.json"; + const existingPluginManifestPath = "/path/to/your/pluginManifest.json"; + const specPath = "/path/to/your/spec.yaml"; + const relativePath = ManifestUpdater.getRelativePath(existingPluginManifestPath, specPath); + const originalManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: "Original Short Description", full: "Original Full Description" }, + copilotExtensions: { + declarativeCopilots: [ + { + id: "repairDeclarativeCopilot", + file: "repairDeclarativeCopilot.json", + }, + ], + }, + }; + const expectedManifest = { + name: { short: "Original Name", full: "Original Full Name" }, + description: { short: "Original Short Description", full: "Original Full Description" }, + copilotExtensions: { + declarativeCopilots: [ + { + id: "repairDeclarativeCopilot", + file: "repairDeclarativeCopilot.json", + }, + ], + }, + }; + const originalPluginManifest = { + $schema: ConstantString.PluginManifestSchema, + schema_version: "v2.1", + name_for_human: "Original Name", + namespace: "originalname", + description_for_human: "My API description", + runtimes: [ + { + type: "OpenApi", + auth: { + type: "None", + }, + spec: { + url: relativePath, + }, + run_for_functions: ["createPet"], + }, + ], + }; + + const expectedPlugins: PluginManifestSchema = { + $schema: ConstantString.PluginManifestSchema, + schema_version: "v2.1", + name_for_human: "Original Name", + namespace: "originalname", + description_for_human: "My API description", + functions: [ + { + name: "createPet", + description: "Create a new pet in the store", + }, + ], + runtimes: [ + { + type: "OpenApi", + auth: { + type: "None", + }, + spec: { + url: "spec/outputSpec.yaml", + }, + run_for_functions: ["createPet"], + }, + ], + }; + sinon.stub(fs, "readJSON").callsFake(async (path) => { + if (path === manifestPath) { + return Promise.resolve(originalManifest); + } else if (path === existingPluginManifestPath) { + return Promise.resolve(originalPluginManifest); + } else { + return Promise.resolve({}); + } + }); + sinon + .stub(fs, "pathExists") + .withArgs(manifestPath) + .resolves(true) + .withArgs(pluginFilePath) + .resolves(false) + .withArgs(existingPluginManifestPath) + .resolves(true); + + const options: ParseOptions = { + allowMethods: ["get", "post"], + isGptPlugin: true, + }; + const [manifest, apiPlugin, warnings] = await ManifestUpdater.updateManifestWithAiPlugin( + manifestPath, + outputSpecPath, + pluginFilePath, + spec, + options, + undefined, + { + manifestPath: existingPluginManifestPath, + specPath: specPath, + } + ); + + expect(manifest).to.deep.equal(expectedManifest); + expect(apiPlugin).to.deep.equal(expectedPlugins); + expect(warnings).to.deep.equal([]); + }); }); describe("manifestUpdater", () => {