Skip to content

Commit

Permalink
feat: add kiota support in add plugin flow (#12620)
Browse files Browse the repository at this point in the history
  • Loading branch information
KennethBWSong authored Oct 30, 2024
1 parent ee7099d commit 4314bda
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 34 deletions.
1 change: 1 addition & 0 deletions packages/fx-core/src/component/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,5 @@ export const AadConstants = {
export const KiotaLastCommands = {
createPluginWithManifest: "createPluginWithManifest",
createDeclarativeCopilotWithManifest: "createDeclarativeCopilotWithManifest",
addPlugin: "addPlugin",
};
24 changes: 22 additions & 2 deletions packages/fx-core/src/core/FxCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -1850,10 +1856,24 @@ export class FxCore {
QuestionMW("addPlugin"),
ConcurrentLockerMW,
])
async addPlugin(inputs: Inputs): Promise<Result<undefined, FxError>> {
async addPlugin(inputs: Inputs): Promise<Result<undefined | any, FxError>> {
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);
Expand Down
14 changes: 10 additions & 4 deletions packages/fx-core/src/question/other.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
},
},
{
Expand Down
113 changes: 113 additions & 0 deletions packages/fx-core/tests/core/FxCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion packages/vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,30 +58,47 @@ export async function createPluginWithManifest(args?: any[]): Promise<Result<any
const outputFolder = args[3] ?? undefined;

const inputs = getSystemInputs();
if (lastCommand === KiotaLastCommands.createDeclarativeCopilotWithManifest) {
inputs.capabilities = CapabilityOptions.declarativeCopilot().id;
inputs[QuestionNames.WithPlugin] = "yes";
} else {
inputs.capabilities = CapabilityOptions.apiPlugin().id;
}
inputs[QuestionNames.ApiSpecLocation] = specPath;
inputs[QuestionNames.ApiPluginManifestPath] = pluginManifestPath;
inputs[QuestionNames.ApiPluginType] = ApiPluginStartOptions.apiSpec().id;
inputs[QuestionNames.ApiOperation] = pluginManifestPath;
inputs[QuestionNames.ProjectType] = ProjectTypeOptions.copilotExtension().id;
inputs[QuestionNames.Folder] = outputFolder;
const result = await runCommand(Stage.create, inputs);
let result;

if (result.isErr()) {
ExtTelemetry.sendTelemetryErrorEvent(TelemetryEvent.CreatePluginWithManifest, result.error);
return err(result.error);
}
if (args[2].manifestPath && args[2].lastCommand === KiotaLastCommands.addPlugin) {
// For add plugin
inputs[QuestionNames.TeamsAppManifestFilePath] = args[2].manifestPath;
result = await runCommand(Stage.addPlugin, inputs);

if (result.isErr()) {
ExtTelemetry.sendTelemetryErrorEvent(TelemetryEvent.AddPluginWithManifest, result.error);
return err(result.error);
}

const res = result.value as CreateProjectResult;
const projectPathUri = vscode.Uri.file(res.projectPath);
await openFolder(projectPathUri, true, res.warnings);
ExtTelemetry.sendTelemetryEvent(TelemetryEvent.CreatePluginWithManifest, {
[TelemetryProperty.Success]: TelemetrySuccess.Yes,
});
ExtTelemetry.sendTelemetryEvent(TelemetryEvent.AddPluginWithManifest, {
[TelemetryProperty.Success]: TelemetrySuccess.Yes,
});
} else {
if (lastCommand === KiotaLastCommands.createDeclarativeCopilotWithManifest) {
inputs.capabilities = CapabilityOptions.declarativeCopilot().id;
inputs[QuestionNames.WithPlugin] = "yes";
} else {
inputs.capabilities = CapabilityOptions.apiPlugin().id;
}
inputs[QuestionNames.ProjectType] = ProjectTypeOptions.copilotExtension().id;
inputs[QuestionNames.Folder] = outputFolder;
result = await runCommand(Stage.create, inputs);

if (result.isErr()) {
ExtTelemetry.sendTelemetryErrorEvent(TelemetryEvent.CreatePluginWithManifest, result.error);
return err(result.error);
}

const res = result.value as CreateProjectResult;
const projectPathUri = vscode.Uri.file(res.projectPath);
await openFolder(projectPathUri, true, res.warnings);
ExtTelemetry.sendTelemetryEvent(TelemetryEvent.CreatePluginWithManifest, {
[TelemetryProperty.Success]: TelemetrySuccess.Yes,
});
}
return ok({});
}
24 changes: 16 additions & 8 deletions packages/vscode-extension/src/handlers/lifecycleHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export async function createNewProjectHandler(...args: any[]): Promise<Result<an
res.projectPath === "" &&
res.lastCommand
) {
return handleTriggerKiotaCommand(args, res);
return handleTriggerKiotaCommand(args, res, TelemetryEvent.CreateProject);
}

if (res.shouldInvokeTeamsAgent) {
Expand Down Expand Up @@ -121,7 +121,17 @@ export async function addWebpartHandler(...args: unknown[]) {

export async function addPluginHandler(...args: unknown[]) {
ExtTelemetry.sendTelemetryEvent(TelemetryEvent.AddPluginStart, getTriggerFromProperty(args));
return await runCommand(Stage.addPlugin);
const result = await runCommand(Stage.addPlugin);
if (result.isErr()) {
return err(result.error);
}

const res = result.value;
if (featureFlagManager.getBooleanValue(FeatureFlags.KiotaIntegration) && res.lastCommand) {
return handleTriggerKiotaCommand(args, res, TelemetryEvent.AddPlugin);
} else {
return result;
}
}

/**
Expand Down Expand Up @@ -242,10 +252,7 @@ export async function copilotPluginAddAPIHandler(args: any[]) {
return result;
}

function handleTriggerKiotaCommand(
args: any[],
result: CreateProjectResult
): Result<CreateProjectResult, FxError> {
function handleTriggerKiotaCommand(args: any[], result: any, event: string): Result<any, FxError> {
if (!validateKiotaInstallation()) {
void vscode.window
.showInformationMessage(
Expand All @@ -271,7 +278,7 @@ function handleTriggerKiotaCommand(
}
});

ExtTelemetry.sendTelemetryEvent(TelemetryEvent.CreateProject, {
ExtTelemetry.sendTelemetryEvent(event, {
[TelemetryProperty.KiotaInstalled]: "No",
...getTriggerFromProperty(args),
});
Expand All @@ -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),
});
Expand Down
2 changes: 2 additions & 0 deletions packages/vscode-extension/src/telemetry/extTelemetryEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,8 @@ export enum TelemetryEvent {
Configuration = "vsc-configuration",

UpdateAddPluginTreeview = "update-add-plugin-tree-view",

AddPluginWithManifest = "add-plugin-with-manifest",
}

export enum TelemetryProperty {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
});
});
Loading

0 comments on commit 4314bda

Please sign in to comment.