diff --git a/packages/fx-core/src/common/projectTypeChecker.ts b/packages/fx-core/src/common/projectTypeChecker.ts index 0a265a6fac..30f9703577 100644 --- a/packages/fx-core/src/common/projectTypeChecker.ts +++ b/packages/fx-core/src/common/projectTypeChecker.ts @@ -279,6 +279,20 @@ export function getCapabilities(manifest: any): string[] { ) { capabilities.push("copilotGpt"); } + if ( + manifest.copilotAgents?.plugins && + manifest.copilotAgents.plugins.length > 0 && + !capabilities.includes("plugin") + ) { + capabilities.push("plugin"); + } + if ( + manifest.copilotAgents?.declarativeAgents && + manifest.copilotAgents.declarativeAgents.length > 0 && + !capabilities.includes("copilotGpt") + ) { + capabilities.push("copilotGpt"); + } return capabilities; } export const projectTypeChecker = new ProjectTypeChecker(); diff --git a/packages/fx-core/src/component/driver/teamsApp/createAppPackage.ts b/packages/fx-core/src/component/driver/teamsApp/createAppPackage.ts index d84d143200..79ab167922 100644 --- a/packages/fx-core/src/component/driver/teamsApp/createAppPackage.ts +++ b/packages/fx-core/src/component/driver/teamsApp/createAppPackage.ts @@ -139,6 +139,19 @@ export class CreateAppPackageDriver implements StepDriver { } } } + if (manifest.localizationInfo && manifest.localizationInfo.defaultLanguageFile) { + const file = manifest.localizationInfo.defaultLanguageFile; + const fileName = `${appDirectory}/${file}`; + if (!(await fs.pathExists(fileName))) { + return err( + new FileNotFoundError( + actionName, + fileName, + "https://aka.ms/teamsfx-actions/teamsapp-zipAppPackage" + ) + ); + } + } const zip = new AdmZip(); zip.addFile(Constants.MANIFEST_FILE, Buffer.from(JSON.stringify(manifest, null, 4))); @@ -166,6 +179,16 @@ export class CreateAppPackageDriver implements StepDriver { zip.addLocalFile(fileName, dir === "." ? "" : dir); } } + if (manifest.localizationInfo && manifest.localizationInfo.defaultLanguageFile) { + const file = manifest.localizationInfo.defaultLanguageFile; + const fileName = path.resolve(appDirectory, file); + const relativePath = path.relative(appDirectory, fileName); + if (relativePath.startsWith("..")) { + return err(new InvalidFileOutsideOfTheDirectotryError(fileName)); + } + const dir = path.dirname(file); + zip.addLocalFile(fileName, dir === "." ? "" : dir); + } // API ME, API specification and Adaptive card templates if ( @@ -218,9 +241,11 @@ export class CreateAppPackageDriver implements StepDriver { } } - const plugins = manifest.copilotExtensions?.plugins; - // API plugin + const plugins = manifest.copilotExtensions + ? manifest.copilotExtensions.plugins + : manifest.copilotAgents?.plugins; if (plugins?.length && plugins[0].file) { + // API plugin const addFilesRes = await this.addPlugin( zip, plugins[0].file, @@ -233,8 +258,9 @@ export class CreateAppPackageDriver implements StepDriver { } } - const declarativeCopilots = manifest.copilotExtensions?.declarativeCopilots; - + const declarativeCopilots = manifest.copilotExtensions + ? manifest.copilotExtensions.declarativeCopilots + : manifest.copilotAgents?.declarativeAgents; // Copilot GPT if (declarativeCopilots?.length && declarativeCopilots[0].file) { const copilotGptManifestFile = path.resolve(appDirectory, declarativeCopilots[0].file); diff --git a/packages/fx-core/src/component/driver/teamsApp/utils/CopilotGptManifestUtils.ts b/packages/fx-core/src/component/driver/teamsApp/utils/CopilotGptManifestUtils.ts index 3f8a33622a..21e109f74c 100644 --- a/packages/fx-core/src/component/driver/teamsApp/utils/CopilotGptManifestUtils.ts +++ b/packages/fx-core/src/component/driver/teamsApp/utils/CopilotGptManifestUtils.ts @@ -152,7 +152,9 @@ export class CopilotGptManifestUtils { if (teamsManifestRes.isErr()) { return err(teamsManifestRes.error); } - const filePath = teamsManifestRes.value.copilotExtensions?.declarativeCopilots?.[0].file; + const filePath = teamsManifestRes.value.copilotExtensions + ? teamsManifestRes.value.copilotExtensions.declarativeCopilots?.[0].file + : teamsManifestRes.value.copilotAgents?.declarativeAgents?.[0].file; if (!filePath) { return err( AppStudioResultFactory.UserError( diff --git a/packages/fx-core/src/component/driver/teamsApp/utils/ManifestUtils.ts b/packages/fx-core/src/component/driver/teamsApp/utils/ManifestUtils.ts index f8c1bb3970..aa223e2968 100644 --- a/packages/fx-core/src/component/driver/teamsApp/utils/ManifestUtils.ts +++ b/packages/fx-core/src/component/driver/teamsApp/utils/ManifestUtils.ts @@ -315,7 +315,9 @@ export class ManifestUtils { manifest: TeamsAppManifest, manifestPath: string ): Promise> { - const pluginFile = manifest.copilotExtensions?.plugins?.[0]?.file; + const pluginFile = manifest.copilotExtensions + ? manifest.copilotExtensions.plugins?.[0]?.file + : manifest.copilotAgents?.plugins?.[0]?.file; if (pluginFile) { const plugin = path.resolve(path.dirname(manifestPath), pluginFile); const doesFileExist = await fs.pathExists(plugin); diff --git a/packages/fx-core/src/component/driver/teamsApp/validate.ts b/packages/fx-core/src/component/driver/teamsApp/validate.ts index b7f4fe2465..1d26c57f53 100644 --- a/packages/fx-core/src/component/driver/teamsApp/validate.ts +++ b/packages/fx-core/src/component/driver/teamsApp/validate.ts @@ -99,9 +99,11 @@ export class ValidateManifestDriver implements StepDriver { let declarativeCopilotValidationResult; let pluginValidationResult; let pluginPath = ""; - if (manifest.copilotExtensions) { + if (manifest.copilotExtensions || manifest.copilotAgents) { // plugin - const plugins = manifest.copilotExtensions.plugins; + const plugins = manifest.copilotExtensions + ? manifest.copilotExtensions.plugins + : manifest.copilotAgents!.plugins; if (plugins?.length && plugins[0].file) { pluginPath = path.join(path.dirname(manifestPath), plugins[0].file); @@ -122,15 +124,17 @@ export class ValidateManifestDriver implements StepDriver { } // Declarative Copilot - const declaraitveCopilots = manifest.copilotExtensions.declarativeCopilots; - if (declaraitveCopilots?.length && declaraitveCopilots[0].file) { + const declarativeCopilots = manifest.copilotExtensions + ? manifest.copilotExtensions.declarativeCopilots + : manifest.copilotAgents!.declarativeAgents; + if (declarativeCopilots?.length && declarativeCopilots[0].file) { const declarativeCopilotPath = path.join( path.dirname(manifestPath), - declaraitveCopilots[0].file + declarativeCopilots[0].file ); const declarativeCopilotValidationRes = await copilotGptManifestUtils.validateAgainstSchema( - declaraitveCopilots[0], + declarativeCopilots[0], declarativeCopilotPath, context ); diff --git a/packages/fx-core/src/core/FxCore.ts b/packages/fx-core/src/core/FxCore.ts index 78b3062433..a12dd458f6 100644 --- a/packages/fx-core/src/core/FxCore.ts +++ b/packages/fx-core/src/core/FxCore.ts @@ -1867,7 +1867,9 @@ export class FxCore { } const teamsManifest = manifestRes.value; - const declarativeGpt = teamsManifest.copilotExtensions?.declarativeCopilots?.[0]; + const declarativeGpt = teamsManifest.copilotExtensions + ? teamsManifest.copilotExtensions.declarativeCopilots?.[0] + : teamsManifest.copilotAgents?.declarativeAgents?.[0]; if (!declarativeGpt?.file) { return err( AppStudioResultFactory.UserError( diff --git a/packages/fx-core/tests/common/projectTypeChecker.test.ts b/packages/fx-core/tests/common/projectTypeChecker.test.ts index 1b79d57676..8e84d43df6 100644 --- a/packages/fx-core/tests/common/projectTypeChecker.test.ts +++ b/packages/fx-core/tests/common/projectTypeChecker.test.ts @@ -112,6 +112,10 @@ describe("ProjectTypeChecker", () => { plugins: [1], declarativeCopilots: [1], }, + copilotAgents: { + plugins: [1], + declarativeAgents: [1], + }, }; const capabilities = getCapabilities(manifest); assert.deepEqual(capabilities, [ @@ -124,6 +128,16 @@ describe("ProjectTypeChecker", () => { "copilotGpt", ]); }); + it("copilot agents", async () => { + const manifest = { + copilotAgents: { + plugins: [1], + declarativeAgents: [1], + }, + }; + const capabilities = getCapabilities(manifest); + assert.deepEqual(capabilities, ["plugin", "copilotGpt"]); + }); it("empty manifest", async () => { const manifest = { staticTabs: [], diff --git a/packages/fx-core/tests/component/driver/teamsApp/copilotGptManifest.test.ts b/packages/fx-core/tests/component/driver/teamsApp/copilotGptManifest.test.ts index c5008765d6..57734739b8 100644 --- a/packages/fx-core/tests/component/driver/teamsApp/copilotGptManifest.test.ts +++ b/packages/fx-core/tests/component/driver/teamsApp/copilotGptManifest.test.ts @@ -428,6 +428,65 @@ describe("copilotGptManifestUtils", () => { } }); + it("get manifest success - copilot agent", async () => { + sandbox.stub(manifestUtils, "_readAppManifest").resolves( + ok({ + copilotAgents: { + declarativeAgents: [ + { + file: "test", + id: "1", + }, + ], + }, + } as any) + ); + sandbox.stub(path, "dirname").returns("testFolder"); + sandbox.stub(path, "resolve").returns("testFolder/test"); + + const res = await copilotGptManifestUtils.getManifestPath("testPath"); + + chai.assert.isTrue(res.isOk()); + if (res.isOk()) { + chai.assert.equal(res.value, "testFolder/test"); + } + }); + + it("declarativeAgents error 1", async () => { + sandbox.stub(manifestUtils, "_readAppManifest").resolves( + ok({ + copilotAgents: {}, + } as any) + ); + const res = await copilotGptManifestUtils.getManifestPath("testPath"); + chai.assert.isTrue(res.isErr()); + if (res.isErr()) { + chai.assert.isTrue(res.error instanceof UserError); + } + }); + + it("declarativeAgents error 2", async () => { + sandbox.stub(manifestUtils, "_readAppManifest").resolves(ok({} as any)); + const res = await copilotGptManifestUtils.getManifestPath("testPath"); + chai.assert.isTrue(res.isErr()); + if (res.isErr()) { + chai.assert.isTrue(res.error instanceof UserError); + } + }); + + it("declarativeCopilots error 1", async () => { + sandbox.stub(manifestUtils, "_readAppManifest").resolves( + ok({ + copilotExtensions: {}, + } as any) + ); + const res = await copilotGptManifestUtils.getManifestPath("testPath"); + chai.assert.isTrue(res.isErr()); + if (res.isErr()) { + chai.assert.isTrue(res.error instanceof UserError); + } + }); + it("read Teams manifest error", async () => { sandbox .stub(manifestUtils, "_readAppManifest") diff --git a/packages/fx-core/tests/component/driver/teamsApp/createAppPackage.test.ts b/packages/fx-core/tests/component/driver/teamsApp/createAppPackage.test.ts index 652d0d8688..cb52f92db9 100644 --- a/packages/fx-core/tests/component/driver/teamsApp/createAppPackage.test.ts +++ b/packages/fx-core/tests/component/driver/teamsApp/createAppPackage.test.ts @@ -5,6 +5,7 @@ import "mocha"; import * as sinon from "sinon"; import chai from "chai"; import fs from "fs-extra"; +import * as path from "path"; import mockedEnv, { RestoreFn } from "mocked-env"; import { CreateAppPackageDriver } from "../../../../src/component/driver/teamsApp/createAppPackage"; import { CreateAppPackageArgs } from "../../../../src/component/driver/teamsApp/interfaces/CreateAppPackageArgs"; @@ -192,6 +193,38 @@ describe("teamsApp/createAppPackage", async () => { } }); + it("should throw error if file not exists case 6", async () => { + const args: CreateAppPackageArgs = { + manifestPath: + "./tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/v3.manifest.template.json", + outputZipPath: + "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage/appPackage.dev.zip", + outputJsonPath: + "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage/manifest.dev.json", + }; + sinon.stub(fs, "pathExists").callsFake((filePath) => { + if (filePath.includes("fake.json")) { + return false; + } else { + return true; + } + }); + + const manifest = new TeamsAppManifest(); + manifest.localizationInfo = { + additionalLanguages: [{ file: "aaa", languageTag: "zh" }], + defaultLanguageTag: "en", + defaultLanguageFile: "fake.json", + }; + sinon.stub(manifestUtils, "getManifestV3").resolves(ok(manifest)); + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + chai.assert(result.isErr()); + if (result.isErr()) { + chai.assert.isTrue(result.error instanceof FileNotFoundError); + } + }); + describe("api plugin error case", async () => { it("should throw error if pluginFile not exists for API plugin", async () => { const args: CreateAppPackageArgs = { @@ -522,6 +555,7 @@ describe("teamsApp/createAppPackage", async () => { file: "resources/de.json", }, ], + defaultLanguageFile: "resources/de.json", }; sinon.stub(manifestUtils, "getManifestV3").resolves(ok(manifest)); @@ -893,6 +927,82 @@ describe("teamsApp/createAppPackage", async () => { } }); + it("version >= 1.9: happy path - API plugin", async () => { + const args: CreateAppPackageArgs = { + manifestPath: + "./tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/v3.manifest.template.json", + outputZipPath: + "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage/appPackage.dev.zip", + outputFolder: "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage", + }; + + const manifest = new TeamsAppManifest(); + manifest.copilotAgents = { + plugins: [ + { + file: "resources/ai-plugin.json", + id: "plugin1", + }, + ], + declarativeAgents: [ + { + file: "resources/de.json", + id: "dc1", + }, + ], + }; + manifest.icons = { + color: "resources/color.png", + outline: "resources/outline.png", + }; + sinon.stub(manifestUtils, "getManifestV3").resolves(ok(manifest)); + sinon.stub(fs, "chmod").callsFake(async () => {}); + const writeFileStub = sinon.stub(fs, "writeFile").callsFake(async () => {}); + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + if (result.isErr()) { + console.log(result.error); + } + chai.assert.isTrue(result.isOk()); + const outputExist = await fs.pathExists(args.outputZipPath); + chai.assert.isTrue(outputExist); + chai.assert.isTrue(writeFileStub.calledThrice); + if (outputExist) { + const zip = new AdmZip(args.outputZipPath); + let aiPluginContent = ""; + let openapiContent = ""; + let declarativeAgentsContent = ""; + + const entries = zip.getEntries(); + entries.forEach((e) => { + const name = e.entryName; + if (name.endsWith("ai-plugin.json")) { + const data = e.getData(); + aiPluginContent = data.toString("utf8"); + } + + if (name.endsWith("openai.yml")) { + const data = e.getData(); + openapiContent = data.toString("utf8"); + } + + if (name.endsWith("de.json")) { + const data = e.getData(); + declarativeAgentsContent = data.toString("utf8"); + } + }); + + chai.assert( + openapiContent && + aiPluginContent && + openapiContent.search("APP_NAME_SUFFIX") < 0 && + aiPluginContent.search(openapiServerPlaceholder) < 0 && + declarativeAgentsContent + ); + await fs.remove(args.outputZipPath); + } + }); + it("invalid color file", async () => { const args: CreateAppPackageArgs = { manifestPath: @@ -1326,5 +1436,147 @@ describe("teamsApp/createAppPackage", async () => { ); } }); + + it("relative path error 1", async () => { + const args: CreateAppPackageArgs = { + manifestPath: + "./tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/v3.manifest.template.json", + outputZipPath: + "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage/appPackage.dev.zip", + outputFolder: "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage", + }; + + const manifest = new TeamsAppManifest(); + manifest.localizationInfo = { + defaultLanguageTag: "en", + additionalLanguages: [ + { + languageTag: "de", + file: "../migrate.manifest.json", + }, + ], + defaultLanguageFile: "resources/de.json", + }; + manifest.icons = { + color: "resources/color.png", + outline: "resources/outline.png", + }; + sinon.stub(manifestUtils, "getManifestV3").resolves(ok(manifest)); + sinon.stub(fs, "pathExists").resolves(true); + sinon.stub(fs, "chmod").callsFake(async () => {}); + const writeFileStub = sinon.stub(fs, "writeFile").callsFake(async () => {}); + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + if (result.isErr()) { + chai.assert.isTrue(result.error instanceof InvalidFileOutsideOfTheDirectotryError); + } + }); + + it("relative path error 2", async () => { + const args: CreateAppPackageArgs = { + manifestPath: + "./tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/v3.manifest.template.json", + outputZipPath: + "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage/appPackage.dev.zip", + outputFolder: "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage", + }; + + const manifest = new TeamsAppManifest(); + manifest.localizationInfo = { + defaultLanguageTag: "en", + additionalLanguages: [ + { + languageTag: "de", + file: "resources/de.json", + }, + ], + defaultLanguageFile: "../migrate.manifest.json", + }; + manifest.icons = { + color: "resources/color.png", + outline: "resources/outline.png", + }; + sinon.stub(manifestUtils, "getManifestV3").resolves(ok(manifest)); + sinon.stub(fs, "pathExists").resolves(true); + sinon.stub(fs, "chmod").callsFake(async () => {}); + const writeFileStub = sinon.stub(fs, "writeFile").callsFake(async () => {}); + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + if (result.isErr()) { + chai.assert.isTrue(result.error instanceof InvalidFileOutsideOfTheDirectotryError); + } + }); + + it("zip same level dir", async () => { + const args: CreateAppPackageArgs = { + manifestPath: + "./tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/v3.manifest.template.json", + outputZipPath: + "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage/appPackage.dev.zip", + outputFolder: "./tests/plugins/resource/appstudio/resources-multi-env/build/appPackage", + }; + + const manifest = new TeamsAppManifest(); + manifest.composeExtensions = [ + { + composeExtensionType: "apiBased", + apiSpecificationFile: "resources/openai.yml", + commands: [ + { + id: "GET /repairs", + apiResponseRenderingTemplateFile: "resources/repairs.json", + title: "fake", + }, + ], + botId: "", + }, + ]; + manifest.icons = { + color: "resources/color.png", + outline: "resources/outline.png", + }; + manifest.localizationInfo = { + defaultLanguageTag: "en", + additionalLanguages: [ + { + languageTag: "de", + file: "de.json", + }, + ], + defaultLanguageFile: "de.json", + }; + sinon.stub(manifestUtils, "getManifestV3").resolves(ok(manifest)); + + sinon.stub(fs, "chmod").callsFake(async () => {}); + const writeFileStub = sinon.stub(fs, "writeFile").callsFake(async () => {}); + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + chai.assert(result.isOk()); + chai.assert(writeFileStub.calledOnce); + if (await fs.pathExists(args.outputZipPath)) { + const zip = new AdmZip(args.outputZipPath); + + let openapiContent = ""; + + const entries = zip.getEntries(); + for (const e of entries) { + const name = e.entryName; + + if (name.endsWith("openai.yml")) { + const data = e.getData(); + openapiContent = data.toString("utf8"); + break; + } + } + + chai.assert( + openapiContent != undefined && + openapiContent.length > 0 && + openapiContent.search(fakeUrl) >= 0 && + openapiContent.search(openapiServerPlaceholder) < 0 + ); + await fs.remove(args.outputZipPath); + } + }); }); }); diff --git a/packages/fx-core/tests/component/driver/teamsApp/manifestUtils.test.ts b/packages/fx-core/tests/component/driver/teamsApp/manifestUtils.test.ts index a20dbe66f6..f05da7055e 100644 --- a/packages/fx-core/tests/component/driver/teamsApp/manifestUtils.test.ts +++ b/packages/fx-core/tests/component/driver/teamsApp/manifestUtils.test.ts @@ -14,6 +14,7 @@ import { Platform, ManifestCapability, IBot, + UserError, } from "@microsoft/teamsfx-api"; import { getBotsTplBasedOnVersion, @@ -251,6 +252,92 @@ describe("ManifestUtils", () => { const result = await manifestUtils.addCapabilities(inputs, capabilities); assert.isTrue(result.isOk()); }); + it("getPluginFilePath success", async () => { + const mockManifest = { + copilotAgents: { + plugins: [ + { + id: "id-fake", + file: "fake", + }, + ], + }, + }; + sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(mockManifest as any)); + sinon.stub(fs, "pathExists").resolves(true); + const res = await manifestUtils.getPluginFilePath(mockManifest as any, "fake"); + assert.isTrue(res.isOk()); + }); + it("getPluginFilePath error 1", async () => { + const mockManifest = {}; + sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(mockManifest as any)); + const res = await manifestUtils.getPluginFilePath(mockManifest as any, "fake"); + assert.isTrue(res.isErr()); + if (res.isErr()) { + assert.isTrue(res.error instanceof UserError); + } + }); + it("getPluginFilePath error 2", async () => { + const mockManifest = { + copilotAgents: {}, + }; + sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(mockManifest as any)); + const res = await manifestUtils.getPluginFilePath(mockManifest as any, "fake"); + assert.isTrue(res.isErr()); + if (res.isErr()) { + assert.isTrue(res.error instanceof UserError); + } + }); + it("getPluginFilePath error 3", async () => { + const mockManifest = { + copilotAgents: { + plugins: [], + }, + }; + sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(mockManifest as any)); + const res = await manifestUtils.getPluginFilePath(mockManifest as any, "fake"); + assert.isTrue(res.isErr()); + if (res.isErr()) { + assert.isTrue(res.error instanceof UserError); + } + }); + it("getPluginFilePath error 4", async () => { + const mockManifest = { + copilotAgents: { + plugins: [undefined], + }, + }; + sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(mockManifest as any)); + const res = await manifestUtils.getPluginFilePath(mockManifest as any, "fake"); + assert.isTrue(res.isErr()); + if (res.isErr()) { + assert.isTrue(res.error instanceof UserError); + } + }); + it("getPluginFilePath error 5", async () => { + const mockManifest = { + copilotExtensions: {}, + }; + sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(mockManifest as any)); + const res = await manifestUtils.getPluginFilePath(mockManifest as any, "fake"); + assert.isTrue(res.isErr()); + if (res.isErr()) { + assert.isTrue(res.error instanceof UserError); + } + }); + it("getPluginFilePath error 6", async () => { + const mockManifest = { + copilotExtensions: { + plugins: [], + }, + }; + sinon.stub(manifestUtils, "_readAppManifest").resolves(ok(mockManifest as any)); + const res = await manifestUtils.getPluginFilePath(mockManifest as any, "fake"); + assert.isTrue(res.isErr()); + if (res.isErr()) { + assert.isTrue(res.error instanceof UserError); + } + }); }); function mockInputManifestFile(manifestUtils: ManifestUtils, manifestVersion: string) { diff --git a/packages/fx-core/tests/component/driver/teamsApp/validate.test.ts b/packages/fx-core/tests/component/driver/teamsApp/validate.test.ts index 34368e65ad..0bcfa6706a 100644 --- a/packages/fx-core/tests/component/driver/teamsApp/validate.test.ts +++ b/packages/fx-core/tests/component/driver/teamsApp/validate.test.ts @@ -281,6 +281,109 @@ describe("teamsApp/validateManifest", async () => { } }); + it("validate with errors returned - copilot agent", async () => { + const teamsManifest: TeamsAppManifest = new TeamsAppManifest(); + teamsManifest.copilotAgents = { + declarativeAgents: [ + { + id: "fakeId", + file: "fakeFile", + }, + ], + plugins: [ + { + id: "fakeId", + file: "fakeFile", + }, + ], + }; + + sinon.stub(manifestUtils, "getManifestV3").resolves(ok(teamsManifest)); + sinon.stub(ManifestUtil, "validateManifest").resolves([]); + sinon.stub(pluginManifestUtils, "validateAgainstSchema").resolves( + ok({ + id: "fakeId", + filePath: "fakeFile", + validationResult: ["error1"], + }) + ); + sinon.stub(pluginManifestUtils, "logValidationErrors").returns("errorMessage1"); + + sinon.stub(copilotGptManifestUtils, "validateAgainstSchema").resolves( + ok({ + id: "fakeId", + filePath: "fakeFile", + validationResult: ["error2"], + actionValidationResult: [ + { + id: "fakeId", + filePath: "fakeFile", + validationResult: ["error3"], + }, + ], + }) + ); + sinon.stub(copilotGptManifestUtils, "logValidationErrors").returns("errorMessage2"); + + const args: ValidateManifestArgs = { + manifestPath: + "./tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/v3.manifest.template.json", + showMessage: true, + }; + + mockedDriverContext.platform = Platform.VSCode; + mockedDriverContext.projectPath = "test"; + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + chai.assert(result.isErr()); + if (result.isErr()) { + chai.assert.equal(result.error.name, AppStudioError.ValidationFailedError.name); + } + }); + + it("skip plugin validation", async () => { + const teamsManifest: TeamsAppManifest = new TeamsAppManifest(); + teamsManifest.copilotAgents = {}; + + sinon.stub(manifestUtils, "getManifestV3").resolves(ok(teamsManifest)); + sinon.stub(ManifestUtil, "validateManifest").resolves([]); + sinon.stub(pluginManifestUtils, "validateAgainstSchema").resolves( + ok({ + id: "fakeId", + filePath: "fakeFile", + validationResult: ["error1"], + }) + ); + sinon.stub(pluginManifestUtils, "logValidationErrors").returns("errorMessage1"); + + sinon.stub(copilotGptManifestUtils, "validateAgainstSchema").resolves( + ok({ + id: "fakeId", + filePath: "fakeFile", + validationResult: ["error2"], + actionValidationResult: [ + { + id: "fakeId", + filePath: "fakeFile", + validationResult: ["error3"], + }, + ], + }) + ); + sinon.stub(copilotGptManifestUtils, "logValidationErrors").returns("errorMessage2"); + + const args: ValidateManifestArgs = { + manifestPath: + "./tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/v3.manifest.template.json", + showMessage: true, + }; + + mockedDriverContext.platform = Platform.VSCode; + mockedDriverContext.projectPath = "test"; + + const result = (await teamsAppDriver.execute(args, mockedDriverContext)).result; + chai.assert(result.isOk()); + }); it("plugin manifest validation error", async () => { const teamsManifest: TeamsAppManifest = new TeamsAppManifest(); teamsManifest.copilotExtensions = { diff --git a/packages/fx-core/tests/core/FxCore.test.ts b/packages/fx-core/tests/core/FxCore.test.ts index 14703ba55b..403d2645a5 100644 --- a/packages/fx-core/tests/core/FxCore.test.ts +++ b/packages/fx-core/tests/core/FxCore.test.ts @@ -4850,6 +4850,219 @@ describe("addPlugin", async () => { } }); + it("from API spec: empty declarativeCopilots 1", async () => { + const appName = await mockV3Project(); + const inputs: Inputs = { + platform: Platform.VSCode, + [QuestionNames.Folder]: os.tmpdir(), + [QuestionNames.TeamsAppManifestFilePath]: "manifest.json", + [QuestionNames.ApiSpecLocation]: "test.yaml", + [QuestionNames.ApiOperation]: ["GET /user/{userId}"], + [QuestionNames.ApiPluginType]: ApiPluginStartOptions.apiSpec().id, + projectPath: path.join(os.tmpdir(), appName), + }; + const manifest = new TeamsAppManifest(); + manifest.copilotExtensions = {}; + sandbox.stub(validationUtils, "validateInputs").resolves(undefined); + sandbox.stub(manifestUtils, "_readAppManifest").resolves(ok(manifest)); + sandbox.stub(manifestUtils, "_writeAppManifest").resolves(ok(undefined)); + sandbox.stub(pluginGeneratorHelper, "generateScaffoldingSummary").resolves(""); + sandbox.stub(fs, "pathExists").callsFake(async (path: string) => { + if (path.endsWith("openapi_1.yaml")) { + return true; + } + if (path.endsWith("ai-plugin_1.json")) { + return true; + } + if (path.endsWith("openapi_2.yaml")) { + return false; + } + if (path.endsWith("ai-plugin_2.json")) { + return false; + } + return true; + }); + sandbox + .stub(copilotGptManifestUtils, "readCopilotGptManifestFile") + .resolves(ok({} as DeclarativeCopilotManifestSchema)); + sandbox.stub(copilotGptManifestUtils, "getManifestPath").resolves(ok("dcManifest.json")); + sandbox + .stub(copilotGptManifestUtils, "addAction") + .resolves(ok({} as DeclarativeCopilotManifestSchema)); + + const core = new FxCore(tools); + sandbox.stub(CopilotPluginHelper, "generateFromApiSpec").resolves(ok({ warnings: [] })); + + const showMessageStub = sandbox + .stub(tools.ui, "showMessage") + .callsFake((level, message, modal, items) => { + if (level == "info") { + return Promise.resolve( + ok(getLocalizedString("core.addPlugin.success.viewPluginManifest")) + ); + } else if (level === "warn") { + return Promise.resolve(ok("Add")); + } else { + throw new NotImplementedError("TEST", "showMessage"); + } + }); + + const openFileStub = sandbox.stub(tools.ui, "openFile").resolves(); + + const result = await core.addPlugin(inputs); + assert.isTrue(result.isErr()); + if (result.isErr()) { + assert.isTrue(result.error instanceof UserError); + } + }); + + it("from API spec: empty declarativeCopilots 2", async () => { + const appName = await mockV3Project(); + const inputs: Inputs = { + platform: Platform.VSCode, + [QuestionNames.Folder]: os.tmpdir(), + [QuestionNames.TeamsAppManifestFilePath]: "manifest.json", + [QuestionNames.ApiSpecLocation]: "test.yaml", + [QuestionNames.ApiOperation]: ["GET /user/{userId}"], + [QuestionNames.ApiPluginType]: ApiPluginStartOptions.apiSpec().id, + projectPath: path.join(os.tmpdir(), appName), + }; + const manifest = new TeamsAppManifest(); + manifest.copilotExtensions = { + declarativeCopilots: [], + }; + sandbox.stub(validationUtils, "validateInputs").resolves(undefined); + sandbox.stub(manifestUtils, "_readAppManifest").resolves(ok(manifest)); + sandbox.stub(manifestUtils, "_writeAppManifest").resolves(ok(undefined)); + sandbox.stub(pluginGeneratorHelper, "generateScaffoldingSummary").resolves(""); + sandbox.stub(fs, "pathExists").callsFake(async (path: string) => { + if (path.endsWith("openapi_1.yaml")) { + return true; + } + if (path.endsWith("ai-plugin_1.json")) { + return true; + } + if (path.endsWith("openapi_2.yaml")) { + return false; + } + if (path.endsWith("ai-plugin_2.json")) { + return false; + } + return true; + }); + sandbox + .stub(copilotGptManifestUtils, "readCopilotGptManifestFile") + .resolves(ok({} as DeclarativeCopilotManifestSchema)); + sandbox.stub(copilotGptManifestUtils, "getManifestPath").resolves(ok("dcManifest.json")); + sandbox + .stub(copilotGptManifestUtils, "addAction") + .resolves(ok({} as DeclarativeCopilotManifestSchema)); + + const core = new FxCore(tools); + sandbox.stub(CopilotPluginHelper, "generateFromApiSpec").resolves(ok({ warnings: [] })); + + const showMessageStub = sandbox + .stub(tools.ui, "showMessage") + .callsFake((level, message, modal, items) => { + if (level == "info") { + return Promise.resolve( + ok(getLocalizedString("core.addPlugin.success.viewPluginManifest")) + ); + } else if (level === "warn") { + return Promise.resolve(ok("Add")); + } else { + throw new NotImplementedError("TEST", "showMessage"); + } + }); + + const openFileStub = sandbox.stub(tools.ui, "openFile").resolves(); + + const result = await core.addPlugin(inputs); + assert.isTrue(result.isErr()); + if (result.isErr()) { + assert.isTrue(result.error instanceof UserError); + } + }); + + it("from API spec: add action success - copilot agent", async () => { + const appName = await mockV3Project(); + const inputs: Inputs = { + platform: Platform.VSCode, + [QuestionNames.Folder]: os.tmpdir(), + [QuestionNames.TeamsAppManifestFilePath]: "manifest.json", + [QuestionNames.ApiSpecLocation]: "test.yaml", + [QuestionNames.ApiOperation]: ["GET /user/{userId}"], + [QuestionNames.ApiPluginType]: ApiPluginStartOptions.apiSpec().id, + projectPath: path.join(os.tmpdir(), appName), + }; + const manifest = new TeamsAppManifest(); + manifest.copilotAgents = { + declarativeAgents: [ + { + file: "test1.json", + id: "action_1", + }, + ], + }; + sandbox.stub(validationUtils, "validateInputs").resolves(undefined); + sandbox.stub(manifestUtils, "_readAppManifest").resolves(ok(manifest)); + sandbox.stub(manifestUtils, "_writeAppManifest").resolves(ok(undefined)); + sandbox.stub(pluginGeneratorHelper, "generateScaffoldingSummary").resolves(""); + sandbox.stub(fs, "pathExists").callsFake(async (path: string) => { + if (path.endsWith("openapi_1.yaml")) { + return true; + } + if (path.endsWith("ai-plugin_1.json")) { + return true; + } + if (path.endsWith("openapi_2.yaml")) { + return false; + } + if (path.endsWith("ai-plugin_2.json")) { + return false; + } + return true; + }); + sandbox + .stub(copilotGptManifestUtils, "readCopilotGptManifestFile") + .resolves(ok({} as DeclarativeCopilotManifestSchema)); + sandbox.stub(copilotGptManifestUtils, "getManifestPath").resolves(ok("dcManifest.json")); + sandbox + .stub(copilotGptManifestUtils, "addAction") + .resolves(ok({} as DeclarativeCopilotManifestSchema)); + + const core = new FxCore(tools); + sandbox.stub(CopilotPluginHelper, "generateFromApiSpec").resolves(ok({ warnings: [] })); + + const showMessageStub = sandbox + .stub(tools.ui, "showMessage") + .callsFake((level, message, modal, items) => { + if (level == "info") { + return Promise.resolve( + ok(getLocalizedString("core.addPlugin.success.viewPluginManifest")) + ); + } else if (level === "warn") { + return Promise.resolve(ok("Add")); + } else { + throw new NotImplementedError("TEST", "showMessage"); + } + }); + + const openFileStub = sandbox.stub(tools.ui, "openFile").resolves(); + + const result = await core.addPlugin(inputs); + if (result.isErr()) { + console.log(result.error); + } + assert.isTrue(result.isOk()); + assert.isTrue(showMessageStub.calledTwice); + assert.isTrue(openFileStub.calledOnce); + + if (await fs.pathExists(inputs.projectPath!)) { + await fs.remove(inputs.projectPath!); + } + }); + it("from API spec: add action with warnings from CLI", async () => { const appName = await mockV3Project(); const inputs: Inputs = { diff --git a/packages/fx-core/tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/de.json b/packages/fx-core/tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/de.json new file mode 100644 index 0000000000..6df6948350 --- /dev/null +++ b/packages/fx-core/tests/plugins/resource/appstudio/resources-multi-env/templates/appPackage/de.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/teams/v1.12/MicrosoftTeams.Localization.schema.json", + "name.short": "Name App", + "name.full": "Name App", + "description.short": "Description Short Deutsch", + "description.full": "Description Long Deutsch", + "staticTabs[0].name": "Home" +} \ No newline at end of file diff --git a/packages/manifest/src/index.ts b/packages/manifest/src/index.ts index 253f768e21..466d23d581 100644 --- a/packages/manifest/src/index.ts +++ b/packages/manifest/src/index.ts @@ -195,6 +195,23 @@ export class ManifestUtil { if (copilotGpts && copilotGpts.length > 0) capabilities.push("copilotGpt"); } + if ((manifest as TeamsAppManifest).copilotAgents?.plugins) { + const apiPlugins = (manifest as TeamsAppManifest).copilotAgents?.plugins; + if ( + apiPlugins && + apiPlugins.length > 0 && + apiPlugins[0].file && + !capabilities.includes("plugin") + ) + capabilities.push("plugin"); + } + + if ((manifest as TeamsAppManifest).copilotAgents?.declarativeAgents) { + const copilotGpts = (manifest as TeamsAppManifest).copilotAgents?.declarativeAgents; + if (copilotGpts && copilotGpts.length > 0 && !capabilities.includes("copilotGpt")) + capabilities.push("copilotGpt"); + } + return properties; } diff --git a/packages/manifest/src/manifest.ts b/packages/manifest/src/manifest.ts index 5aa110207e..236bbcf54b 100644 --- a/packages/manifest/src/manifest.ts +++ b/packages/manifest/src/manifest.ts @@ -364,6 +364,7 @@ export interface ILocalizationInfo { * The language tag of the strings in this top level manifest file. */ defaultLanguageTag: string; + defaultLanguageFile?: string; additionalLanguages?: { languageTag: string; /** @@ -584,4 +585,15 @@ export class TeamsAppManifest implements AppManifest { */ declarativeCopilots?: IDeclarativeCopilot[]; }; + + copilotAgents?: { + /** + * Pointer to plugins. + */ + plugins?: IPlugin[]; + /** + * Pointer to declarative Copilot. + */ + declarativeAgents?: IDeclarativeCopilot[]; + }; } diff --git a/packages/vscode-extension/src/utils/autoOpenHelper.ts b/packages/vscode-extension/src/utils/autoOpenHelper.ts index 609d5af8d5..eae8110ee5 100644 --- a/packages/vscode-extension/src/utils/autoOpenHelper.ts +++ b/packages/vscode-extension/src/utils/autoOpenHelper.ts @@ -170,7 +170,12 @@ export async function ShowScaffoldingWarningSummary( createWarnings, teamsManifest, path.relative(workspacePath, apiSpecFilePathRes.value[0]), - path.join(AppPackageFolderName, teamsManifest.copilotExtensions!.plugins![0].file), + path.join( + AppPackageFolderName, + teamsManifest.copilotExtensions + ? teamsManifest.copilotExtensions.plugins![0].file + : teamsManifest.copilotAgents!.plugins![0].file + ), workspacePath ); } diff --git a/packages/vscode-extension/test/utils/autoOpenHelper.test.ts b/packages/vscode-extension/test/utils/autoOpenHelper.test.ts index 1e719fe17d..c70b98348d 100644 --- a/packages/vscode-extension/test/utils/autoOpenHelper.test.ts +++ b/packages/vscode-extension/test/utils/autoOpenHelper.test.ts @@ -6,10 +6,16 @@ import * as globalVariables from "../../src/globalVariables"; import * as globalState from "@microsoft/teamsfx-core/build/common/globalState"; import * as runIconHandlers from "../../src/debug/runIconHandler"; import * as appDefinitionUtils from "../../src/utils/appDefinitionUtils"; -import { ok } from "@microsoft/teamsfx-api"; +import { ok, TeamsAppManifest } from "@microsoft/teamsfx-api"; import { ExtTelemetry } from "../../src/telemetry/extTelemetry"; -import { showLocalDebugMessage } from "../../src/utils/autoOpenHelper"; +import { + showLocalDebugMessage, + ShowScaffoldingWarningSummary, +} from "../../src/utils/autoOpenHelper"; +import VscodeLogInstance from "../../src/commonlib/log"; import * as readmeHandlers from "../../src/handlers/readmeHandlers"; +import { manifestUtils, pluginManifestUtils } from "@microsoft/teamsfx-core"; +import * as apiSpec from "@microsoft/teamsfx-core/build/component/generator/apiSpec/helper"; describe("autoOpenHelper", () => { const sandbox = sinon.createSandbox(); @@ -428,4 +434,112 @@ describe("autoOpenHelper", () => { chai.assert.isTrue(showMessageStub.called); chai.assert.isTrue(openReadMeHandlerStub.called); }); + + it("ShowScaffoldingWarningSummary() - copilot agents", async () => { + const workspacePath = "/path/to/workspace"; + + const manifest: TeamsAppManifest = { + manifestVersion: "version", + id: "mock-app-id", + name: { short: "short-name" }, + description: { short: "", full: "" }, + version: "version", + icons: { outline: "outline.png", color: "color.png" }, + accentColor: "#ffffff", + developer: { + privacyUrl: "", + websiteUrl: "", + termsOfUseUrl: "", + name: "", + }, + staticTabs: [ + { + name: "name0", + entityId: "index0", + scopes: ["personal"], + contentUrl: "localhost/content", + websiteUrl: "localhost/website", + }, + ], + copilotAgents: { + plugins: [ + { + id: "plugin-id", + file: "copilot-plugin-file", + }, + ], + }, + }; + sandbox.stub(manifestUtils, "_readAppManifest").resolves(ok(manifest)); + sandbox + .stub(pluginManifestUtils, "getApiSpecFilePathFromTeamsManifest") + .resolves(ok(["/path/to/api/spec"])); + sandbox.stub(apiSpec, "generateScaffoldingSummary").resolves("fake summary"); + sandbox.stub(VscodeLogInstance, "info").callsFake((message: string) => { + if (message !== "fake summary") { + throw new Error(`Unexpected message: ${message}`); + } + }); + const fakeOutputChannel = { + show: sandbox.stub().resolves(), + }; + sandbox.stub(VscodeLogInstance, "outputChannel").value(fakeOutputChannel); + sandbox.stub(ExtTelemetry, "sendTelemetryEvent").resolves(); + // Call the function + await ShowScaffoldingWarningSummary(workspacePath, ""); + }); + + it("ShowScaffoldingWarningSummary() - copilot extensions", async () => { + const workspacePath = "/path/to/workspace"; + + const manifest: TeamsAppManifest = { + manifestVersion: "version", + id: "mock-app-id", + name: { short: "short-name" }, + description: { short: "", full: "" }, + version: "version", + icons: { outline: "outline.png", color: "color.png" }, + accentColor: "#ffffff", + developer: { + privacyUrl: "", + websiteUrl: "", + termsOfUseUrl: "", + name: "", + }, + staticTabs: [ + { + name: "name0", + entityId: "index0", + scopes: ["personal"], + contentUrl: "localhost/content", + websiteUrl: "localhost/website", + }, + ], + copilotExtensions: { + plugins: [ + { + id: "plugin-id", + file: "copilot-plugin-file", + }, + ], + }, + }; + sandbox.stub(manifestUtils, "_readAppManifest").resolves(ok(manifest)); + sandbox + .stub(pluginManifestUtils, "getApiSpecFilePathFromTeamsManifest") + .resolves(ok(["/path/to/api/spec"])); + sandbox.stub(apiSpec, "generateScaffoldingSummary").resolves("fake summary"); + sandbox.stub(VscodeLogInstance, "info").callsFake((message: string) => { + if (message !== "fake summary") { + throw new Error(`Unexpected message: ${message}`); + } + }); + const fakeOutputChannel = { + show: sandbox.stub().resolves(), + }; + sandbox.stub(VscodeLogInstance, "outputChannel").value(fakeOutputChannel); + sandbox.stub(ExtTelemetry, "sendTelemetryEvent").resolves(); + // Call the function + await ShowScaffoldingWarningSummary(workspacePath, ""); + }); });