From 4314bda26a084845279b24445e58fbc255eec3b4 Mon Sep 17 00:00:00 2001 From: Bowen Song Date: Wed, 30 Oct 2024 13:33:25 +0800 Subject: [PATCH] feat: add kiota support in add plugin flow (#12620) --- packages/fx-core/src/component/constants.ts | 1 + packages/fx-core/src/core/FxCore.ts | 24 +++- packages/fx-core/src/question/other.ts | 14 ++- packages/fx-core/tests/core/FxCore.test.ts | 113 ++++++++++++++++++ packages/vscode-extension/src/extension.ts | 2 +- .../createPluginWithManifestHandler.ts | 55 ++++++--- .../src/handlers/lifecycleHandlers.ts | 24 ++-- .../src/telemetry/extTelemetryEvents.ts | 2 + .../createPluginWithManifestHandler.test.ts | 34 ++++++ .../test/handlers/lifecycleHandlers.test.ts | 37 ++++++ 10 files changed, 272 insertions(+), 34 deletions(-) diff --git a/packages/fx-core/src/component/constants.ts b/packages/fx-core/src/component/constants.ts index 2439ec8bb8..df05aeaa83 100644 --- a/packages/fx-core/src/component/constants.ts +++ b/packages/fx-core/src/component/constants.ts @@ -114,4 +114,5 @@ export const AadConstants = { export const KiotaLastCommands = { createPluginWithManifest: "createPluginWithManifest", createDeclarativeCopilotWithManifest: "createDeclarativeCopilotWithManifest", + addPlugin: "addPlugin", }; diff --git a/packages/fx-core/src/core/FxCore.ts b/packages/fx-core/src/core/FxCore.ts index a12dd458f6..682a30d780 100644 --- a/packages/fx-core/src/core/FxCore.ts +++ b/packages/fx-core/src/core/FxCore.ts @@ -65,7 +65,12 @@ import { MetadataV3, VersionSource, VersionState } from "../common/versionMetada import { ActionInjector } from "../component/configManager/actionInjector"; import { ILifecycle, LifecycleName } from "../component/configManager/interface"; import { YamlParser } from "../component/configManager/parser"; -import { AadConstants, SingleSignOnOptionItem, ViewAadAppHelpLinkV5 } from "../component/constants"; +import { + AadConstants, + KiotaLastCommands, + SingleSignOnOptionItem, + ViewAadAppHelpLinkV5, +} from "../component/constants"; import { coordinator } from "../component/coordinator"; import { UpdateAadAppArgs } from "../component/driver/aad/interface/updateAadAppArgs"; import { UpdateAadAppDriver } from "../component/driver/aad/update"; @@ -167,6 +172,7 @@ import { SyncManifestArgs } from "../component/driver/teamsApp/interfaces/SyncMa import { SyncManifestDriver } from "../component/driver/teamsApp/syncManifest"; import { generateDriverContext } from "../common/utils"; import { addExistingPlugin } from "../component/generator/copilotExtension/helper"; +import { featureFlagManager, FeatureFlags } from "../common/featureFlags"; export class FxCore { constructor(tools: Tools) { @@ -1850,10 +1856,24 @@ export class FxCore { QuestionMW("addPlugin"), ConcurrentLockerMW, ]) - async addPlugin(inputs: Inputs): Promise> { + async addPlugin(inputs: Inputs): Promise> { if (!inputs.projectPath) { throw new Error("projectPath is undefined"); // should never happen } + + // Call Kiota to select the OpenAPI spec file + if ( + inputs.platform === Platform.VSCode && + featureFlagManager.getBooleanValue(FeatureFlags.KiotaIntegration) && + inputs[QuestionNames.ApiPluginType] === ApiPluginStartOptions.apiSpec().id && + !!!inputs[QuestionNames.ApiPluginManifestPath] + ) { + return ok({ + lastCommand: KiotaLastCommands.addPlugin, + manifestPath: inputs[QuestionNames.ManifestPath], + }); + } + const context = createContext(); const teamsManifestPath = inputs[QuestionNames.ManifestPath]; const appPackageFolder = path.dirname(teamsManifestPath); diff --git a/packages/fx-core/src/question/other.ts b/packages/fx-core/src/question/other.ts index cac14898f2..af271df663 100644 --- a/packages/fx-core/src/question/other.ts +++ b/packages/fx-core/src/question/other.ts @@ -758,14 +758,20 @@ export function addPluginQuestionNode(): IQTreeNode { }, { data: apiSpecLocationQuestion(), - condition: { - equals: ApiPluginStartOptions.apiSpec().id, + condition: (inputs: Inputs) => { + return ( + !featureFlagManager.getBooleanValue(FeatureFlags.KiotaIntegration) && + inputs[QuestionNames.ApiPluginType] === ApiPluginStartOptions.apiSpec().id + ); }, }, { data: apiOperationQuestion(true, true), - condition: { - equals: ApiPluginStartOptions.apiSpec().id, + condition: (inputs: Inputs) => { + return ( + !featureFlagManager.getBooleanValue(FeatureFlags.KiotaIntegration) && + inputs[QuestionNames.ApiPluginType] === ApiPluginStartOptions.apiSpec().id + ); }, }, { diff --git a/packages/fx-core/tests/core/FxCore.test.ts b/packages/fx-core/tests/core/FxCore.test.ts index 403d2645a5..71aaa9dd3d 100644 --- a/packages/fx-core/tests/core/FxCore.test.ts +++ b/packages/fx-core/tests/core/FxCore.test.ts @@ -5528,6 +5528,119 @@ describe("addPlugin", async () => { } }); + it("call kiota: success redirect to Kiota", async () => { + const mockedEnvRestore = mockedEnv({ + [FeatureFlagName.KiotaIntegration]: "true", + }); + + const appName = await mockV3Project(); + const inputs: Inputs = { + platform: Platform.VSCode, + [QuestionNames.Folder]: os.tmpdir(), + [QuestionNames.ApiPluginType]: ApiPluginStartOptions.apiSpec().id, + [QuestionNames.TeamsAppManifestFilePath]: "manifest.json", + projectPath: path.join(os.tmpdir(), appName), + }; + + const core = new FxCore(tools); + + const result = await core.addPlugin(inputs); + assert.isTrue(result.isOk()); + if (result.isOk()) { + assert.equal(result.value.lastCommand, "addPlugin"); + assert.equal(result.value.manifestPath, "manifest.json"); + } + + mockedEnvRestore(); + }); + + it("call kiota: success create project with input", async () => { + const mockedEnvRestore = mockedEnv({ + [FeatureFlagName.KiotaIntegration]: "true", + }); + + const appName = await mockV3Project(); + const inputs: Inputs = { + platform: Platform.VSCode, + [QuestionNames.Folder]: os.tmpdir(), + [QuestionNames.ApiPluginType]: ApiPluginStartOptions.apiSpec().id, + [QuestionNames.TeamsAppManifestFilePath]: "manifest.json", + [QuestionNames.ApiPluginManifestPath]: "ai-plugin.json", + [QuestionNames.ApiSpecLocation]: "spec.yaml", + [QuestionNames.ApiOperation]: "ai-plugin.json", + projectPath: path.join(os.tmpdir(), appName), + }; + + const manifest = new TeamsAppManifest(); + manifest.copilotExtensions = { + declarativeCopilots: [ + { + 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!); + } + + mockedEnvRestore(); + }); + describe("projectVersionCheck", async () => { it("invalid project", async () => { sandbox.stub(projectHelper, "isValidProjectV3").returns(false); diff --git a/packages/vscode-extension/src/extension.ts b/packages/vscode-extension/src/extension.ts index 4e15adb9ab..196deb727b 100644 --- a/packages/vscode-extension/src/extension.ts +++ b/packages/vscode-extension/src/extension.ts @@ -535,7 +535,7 @@ function registerInternalCommands(context: vscode.ExtensionContext) { if (featureFlagManager.getBooleanValue(FeatureFlags.KiotaIntegration)) { const createPluginWithManifestCommand = vscode.commands.registerCommand( "fx-extension.createprojectfromkiota", - (...args) => Correlator.run(createPluginWithManifest, args) + (args) => Correlator.run(createPluginWithManifest, args) ); context.subscriptions.push(createPluginWithManifestCommand); } diff --git a/packages/vscode-extension/src/handlers/createPluginWithManifestHandler.ts b/packages/vscode-extension/src/handlers/createPluginWithManifestHandler.ts index 268905c6b5..e2abb0d282 100644 --- a/packages/vscode-extension/src/handlers/createPluginWithManifestHandler.ts +++ b/packages/vscode-extension/src/handlers/createPluginWithManifestHandler.ts @@ -58,30 +58,47 @@ export async function createPluginWithManifest(args?: any[]): Promise { +function handleTriggerKiotaCommand(args: any[], result: any, event: string): Result { if (!validateKiotaInstallation()) { void vscode.window .showInformationMessage( @@ -271,7 +278,7 @@ function handleTriggerKiotaCommand( } }); - ExtTelemetry.sendTelemetryEvent(TelemetryEvent.CreateProject, { + ExtTelemetry.sendTelemetryEvent(event, { [TelemetryProperty.KiotaInstalled]: "No", ...getTriggerFromProperty(args), }); @@ -286,9 +293,10 @@ function handleTriggerKiotaCommand( source: "ttk", ttkContext: { lastCommand: result.lastCommand, + manifestPath: result.manifestPath, }, }); - ExtTelemetry.sendTelemetryEvent(TelemetryEvent.CreateProject, { + ExtTelemetry.sendTelemetryEvent(event, { [TelemetryProperty.KiotaInstalled]: "Yes", ...getTriggerFromProperty(args), }); diff --git a/packages/vscode-extension/src/telemetry/extTelemetryEvents.ts b/packages/vscode-extension/src/telemetry/extTelemetryEvents.ts index da014a72d2..2ea3f3c691 100644 --- a/packages/vscode-extension/src/telemetry/extTelemetryEvents.ts +++ b/packages/vscode-extension/src/telemetry/extTelemetryEvents.ts @@ -300,6 +300,8 @@ export enum TelemetryEvent { Configuration = "vsc-configuration", UpdateAddPluginTreeview = "update-add-plugin-tree-view", + + AddPluginWithManifest = "add-plugin-with-manifest", } export enum TelemetryProperty { diff --git a/packages/vscode-extension/test/handlers/createPluginWithManifestHandler.test.ts b/packages/vscode-extension/test/handlers/createPluginWithManifestHandler.test.ts index a6d29c2aee..9f5512c6a2 100644 --- a/packages/vscode-extension/test/handlers/createPluginWithManifestHandler.test.ts +++ b/packages/vscode-extension/test/handlers/createPluginWithManifestHandler.test.ts @@ -170,4 +170,38 @@ describe("createPluginWithManifestHandler", () => { chai.assert.equal(res.error.name, "fakeError"); } }); + + it("happy path: add plugin", async () => { + const core = new MockCore(); + sandbox.stub(globalVariables, "core").value(core); + const res = await createPluginWithManifest([ + "specPath", + "pluginManifestPath", + { + lastCommand: "addPlugin", + manifestPath: "manifestPath", + }, + ]); + chai.assert.isTrue(res.isOk()); + }); + + it("should throw error if add plugin core return error", async () => { + const core = new MockCore(); + sandbox.stub(globalVariables, "core").value(core); + sandbox + .stub(globalVariables.core, "addPlugin") + .resolves(err(new UserError("core", "fakeError", "fakeErrorMessage"))); + const res = await createPluginWithManifest([ + "specPath", + "pluginManifestPath", + { + lastCommand: "addPlugin", + manifestPath: "manifestPath", + }, + ]); + chai.assert.isTrue(res.isErr()); + if (res.isErr()) { + chai.assert.equal(res.error.name, "fakeError"); + } + }); }); diff --git a/packages/vscode-extension/test/handlers/lifecycleHandlers.test.ts b/packages/vscode-extension/test/handlers/lifecycleHandlers.test.ts index 68551672f1..100c683100 100644 --- a/packages/vscode-extension/test/handlers/lifecycleHandlers.test.ts +++ b/packages/vscode-extension/test/handlers/lifecycleHandlers.test.ts @@ -550,5 +550,42 @@ describe("Lifecycle handlers", () => { sinon.assert.calledOnce(addPluginHanlder); }); + + it("success: success call kiota", async () => { + const mockedEnvRestore = mockedEnv({ + [FeatureFlagName.KiotaIntegration]: "true", + }); + sandbox.stub(globalVariables, "core").value(new MockCore()); + sandbox + .stub(globalVariables.core, "addPlugin") + .resolves(ok({ lastCommand: "addPlugin", manifestPath: "manifest.json" })); + sandbox.stub(shared, "runCommand").resolves( + ok({ + projectPath: "", + lastCommand: "command", + }) + ); + sandbox.stub(vscode.extensions, "getExtension").returns({ + id: "mockedId", + extensionUri: vscode.Uri.parse("file://mockedUri"), + isActive: true, + extensionPath: "mockedPath", + extensionKind: vscode.ExtensionKind.UI, + exports: {}, + packageJSON: { + version: "1.18.100000002", + }, + activate: () => Promise.resolve(), + }); + const executeCommand = sandbox.stub(vscode.commands, "executeCommand").resolves(); + const logError = sandbox.stub(VsCodeLogInstance, "error").resolves(); + + const result = await addPluginHandler(); + + assert.isTrue(result.isOk()); + assert.isTrue(executeCommand.calledOnce); + assert.isTrue(logError.notCalled); + mockedEnvRestore(); + }); }); });