diff --git a/src/init/features/ailogic/index.spec.ts b/src/init/features/ailogic/index.spec.ts index 390ed5884d2..7129835fade 100644 --- a/src/init/features/ailogic/index.spec.ts +++ b/src/init/features/ailogic/index.spec.ts @@ -51,6 +51,7 @@ describe("init ailogic", () => { expect(mockSetup.featureInfo).to.have.property("ailogic"); expect(mockSetup.featureInfo?.ailogic).to.deep.equal({ appId: "1:123456789:android:abcdef123456", + displayName: "Test Android App", }); }); diff --git a/src/init/features/ailogic/index.ts b/src/init/features/ailogic/index.ts index ba1e7a47085..9001d42e06e 100644 --- a/src/init/features/ailogic/index.ts +++ b/src/init/features/ailogic/index.ts @@ -11,6 +11,7 @@ import { export interface AiLogicInfo { appId: string; + displayName?: string; } function checkForApps(apps: AppMetadata[]): void { @@ -69,25 +70,29 @@ export async function askQuestions(setup: Setup): Promise { setup.featureInfo.ailogic = { appId: selectedApp.appId, + displayName: selectedApp.displayName, }; } -function getAppOptions(appInfo: AppInfo): ProvisionAppOptions { +function getAppOptions(appInfo: AppInfo, displayName?: string): ProvisionAppOptions { switch (appInfo.platform) { case AppPlatform.IOS: return { platform: AppPlatform.IOS, appId: appInfo.appId, + displayName, }; case AppPlatform.ANDROID: return { platform: AppPlatform.ANDROID, appId: appInfo.appId, + displayName, }; case AppPlatform.WEB: return { platform: AppPlatform.WEB, appId: appInfo.appId, + displayName, }; default: throw new FirebaseError(`Unsupported platform ${appInfo.platform}`, { exit: 1 }); @@ -115,10 +120,9 @@ export async function actuate(setup: Setup): Promise { // Build provision options and call API directly const provisionOptions: ProvisionFirebaseAppOptions = { project: { - displayName: "Firebase Project", parent: { type: "existing_project", projectId: setup.projectId }, }, - app: getAppOptions(appInfo), + app: getAppOptions(appInfo, ailogicInfo.displayName), features: { firebaseAiLogicInput: {}, }, diff --git a/src/init/features/ailogic/utils.spec.ts b/src/init/features/ailogic/utils.spec.ts index 76524475227..bad040466da 100644 --- a/src/init/features/ailogic/utils.spec.ts +++ b/src/init/features/ailogic/utils.spec.ts @@ -139,10 +139,10 @@ describe("ailogic utils", () => { }); describe("validateAppExists", () => { - let getAppConfigStub: sinon.SinonStub; + let listFirebaseAppsStub: sinon.SinonStub; beforeEach(() => { - getAppConfigStub = sandbox.stub(apps, "getAppConfig"); + listFirebaseAppsStub = sandbox.stub(apps, "listFirebaseApps"); }); it("should not throw when app exists for web platform", async () => { @@ -151,10 +151,14 @@ describe("ailogic utils", () => { appId: "1:123456789:web:abcdef", platform: AppPlatform.WEB, }; - getAppConfigStub.resolves({ mockConfig: true }); + const mockApps = [ + { appId: "1:123456789:web:abcdef", displayName: "Test App", platform: AppPlatform.WEB }, + ]; + listFirebaseAppsStub.resolves(mockApps); - await expect(utils.validateAppExists(appInfo)).to.not.be.rejected; - sinon.assert.calledWith(getAppConfigStub, "1:123456789:web:abcdef", AppPlatform.WEB); + const result = await utils.validateAppExists(appInfo, "test-project"); + expect(result).to.deep.equal(mockApps[0]); + sinon.assert.calledWith(listFirebaseAppsStub, "test-project", AppPlatform.WEB); }); it("should not throw when app exists for ios platform", async () => { @@ -163,10 +167,14 @@ describe("ailogic utils", () => { appId: "1:123456789:ios:abcdef", platform: AppPlatform.IOS, }; - getAppConfigStub.resolves({ mockConfig: true }); + const mockApps = [ + { appId: "1:123456789:ios:abcdef", displayName: "Test iOS App", platform: AppPlatform.IOS }, + ]; + listFirebaseAppsStub.resolves(mockApps); - await expect(utils.validateAppExists(appInfo)).to.not.be.rejected; - sinon.assert.calledWith(getAppConfigStub, "1:123456789:ios:abcdef", AppPlatform.IOS); + const result = await utils.validateAppExists(appInfo, "test-project"); + expect(result).to.deep.equal(mockApps[0]); + sinon.assert.calledWith(listFirebaseAppsStub, "test-project", AppPlatform.IOS); }); it("should not throw when app exists for android platform", async () => { @@ -175,10 +183,18 @@ describe("ailogic utils", () => { appId: "1:123456789:android:abcdef", platform: AppPlatform.ANDROID, }; - getAppConfigStub.resolves({ mockConfig: true }); + const mockApps = [ + { + appId: "1:123456789:android:abcdef", + displayName: "Test Android App", + platform: AppPlatform.ANDROID, + }, + ]; + listFirebaseAppsStub.resolves(mockApps); - await expect(utils.validateAppExists(appInfo)).to.not.be.rejected; - sinon.assert.calledWith(getAppConfigStub, "1:123456789:android:abcdef", AppPlatform.ANDROID); + const result = await utils.validateAppExists(appInfo, "test-project"); + expect(result).to.deep.equal(mockApps[0]); + sinon.assert.calledWith(listFirebaseAppsStub, "test-project", AppPlatform.ANDROID); }); it("should throw when app does not exist", async () => { @@ -187,11 +203,11 @@ describe("ailogic utils", () => { appId: "1:123456789:web:nonexistent", platform: AppPlatform.WEB, }; - getAppConfigStub.throws(new Error("App not found")); + listFirebaseAppsStub.resolves([]); - await expect(utils.validateAppExists(appInfo)).to.be.rejectedWith( + await expect(utils.validateAppExists(appInfo, "test-project")).to.be.rejectedWith( FirebaseError, - "App 1:123456789:web:nonexistent does not exist or is not accessible.", + "App 1:123456789:web:nonexistent does not exist in project test-project.", ); }); }); diff --git a/src/init/features/ailogic/utils.ts b/src/init/features/ailogic/utils.ts index cf2fea5f1d0..bdd87cb96cc 100644 --- a/src/init/features/ailogic/utils.ts +++ b/src/init/features/ailogic/utils.ts @@ -1,4 +1,4 @@ -import { AppPlatform, getAppConfig } from "../../../management/apps"; +import { AppPlatform, listFirebaseApps, AppMetadata } from "../../../management/apps"; import { FirebaseError } from "../../../error"; import { FirebaseProjectMetadata } from "../../../types/project"; @@ -81,10 +81,23 @@ export function validateProjectNumberMatch( /** * Validate that app exists */ -export async function validateAppExists(appInfo: AppInfo): Promise { +export async function validateAppExists(appInfo: AppInfo, projectId: string): Promise { try { - await getAppConfig(appInfo.appId, appInfo.platform); + // Get apps list to find the specific app with metadata + const apps = await listFirebaseApps(projectId, appInfo.platform); + const app = apps.find((a) => a.appId === appInfo.appId); + + if (!app) { + throw new FirebaseError(`App ${appInfo.appId} does not exist in project ${projectId}.`, { + exit: 1, + }); + } + + return app; } catch (error) { + if (error instanceof FirebaseError) { + throw error; + } throw new FirebaseError(`App ${appInfo.appId} does not exist or is not accessible.`, { exit: 1, original: error instanceof Error ? error : new Error(String(error)), diff --git a/src/management/provisioning/provision.spec.ts b/src/management/provisioning/provision.spec.ts index b97d7545f52..a0d2bc9cac2 100644 --- a/src/management/provisioning/provision.spec.ts +++ b/src/management/provisioning/provision.spec.ts @@ -215,7 +215,7 @@ describe("Provision module", () => { it("should build basic request with minimal options", () => { const options: ProvisionFirebaseAppOptions = { project: { displayName: PROJECT_DISPLAY_NAME }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, }; const result = buildProvisionRequest(options); @@ -233,7 +233,7 @@ describe("Provision module", () => { displayName: PROJECT_DISPLAY_NAME, parent: { type: "existing_project", projectId: "my-project-123" }, }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, }; const result = buildProvisionRequest(options); @@ -249,7 +249,7 @@ describe("Provision module", () => { it("should include location when specified", () => { const options: ProvisionFirebaseAppOptions = { project: { displayName: PROJECT_DISPLAY_NAME }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, features: { location: LOCATION }, }; @@ -266,7 +266,7 @@ describe("Provision module", () => { it("should include requestId when specified", () => { const options: ProvisionFirebaseAppOptions = { project: { displayName: PROJECT_DISPLAY_NAME }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, requestId: REQUEST_ID, }; @@ -288,6 +288,7 @@ describe("Provision module", () => { bundleId: BUNDLE_ID, appStoreId: "12345", teamId: "TEAM123", + displayName: PROJECT_DISPLAY_NAME, }, }; @@ -311,6 +312,7 @@ describe("Provision module", () => { packageName: PACKAGE_NAME, sha1Hashes: ["sha1hash1", "sha1hash2"], sha256Hashes: ["sha256hash1"], + displayName: PROJECT_DISPLAY_NAME, }, }; @@ -329,7 +331,7 @@ describe("Provision module", () => { it("should build Web-specific request correctly", () => { const options: ProvisionFirebaseAppOptions = { project: { displayName: PROJECT_DISPLAY_NAME }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, }; const result = buildProvisionRequest(options); @@ -345,7 +347,7 @@ describe("Provision module", () => { const aiFeatures = { enableAiLogic: true, model: "gemini-pro" }; const options: ProvisionFirebaseAppOptions = { project: { displayName: PROJECT_DISPLAY_NAME }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, features: { firebaseAiLogicInput: aiFeatures }, }; @@ -411,7 +413,7 @@ describe("Provision module", () => { it("should provision Web app successfully", async () => { const options: ProvisionFirebaseAppOptions = { project: { displayName: PROJECT_DISPLAY_NAME }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, }; // Mock API call @@ -434,7 +436,7 @@ describe("Provision module", () => { displayName: PROJECT_DISPLAY_NAME, parent: { type: "existing_project", projectId: "parent-project-123" }, }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, }; // Mock API call with parent verification @@ -459,7 +461,7 @@ describe("Provision module", () => { displayName: PROJECT_DISPLAY_NAME, parent: { type: "organization", organizationId: "987654321" }, }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, }; // Mock API call with parent verification @@ -484,7 +486,7 @@ describe("Provision module", () => { displayName: PROJECT_DISPLAY_NAME, parent: { type: "folder", folderId: "123456789" }, }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, }; // Mock API call with parent verification @@ -506,7 +508,7 @@ describe("Provision module", () => { it("should provision with requestId for idempotency", async () => { const options: ProvisionFirebaseAppOptions = { project: { displayName: PROJECT_DISPLAY_NAME }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, requestId: REQUEST_ID, }; @@ -529,7 +531,7 @@ describe("Provision module", () => { it("should provision with custom location", async () => { const options: ProvisionFirebaseAppOptions = { project: { displayName: PROJECT_DISPLAY_NAME }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, features: { location: LOCATION }, }; @@ -553,7 +555,7 @@ describe("Provision module", () => { const aiFeatures = { enableAiLogic: true, model: "gemini-pro" }; const options: ProvisionFirebaseAppOptions = { project: { displayName: PROJECT_DISPLAY_NAME }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, features: { firebaseAiLogicInput: aiFeatures }, }; @@ -644,7 +646,7 @@ describe("Provision module", () => { const baseOptions: ProvisionFirebaseAppOptions = { project: { displayName: PROJECT_DISPLAY_NAME }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, }; it("should work without requestId (undefined)", async () => { @@ -770,7 +772,7 @@ describe("Provision module", () => { describe("provisionFirebaseApp - Error Cases", () => { const baseOptions: ProvisionFirebaseAppOptions = { project: { displayName: PROJECT_DISPLAY_NAME }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, }; it("should reject if API call fails with 404", async () => { @@ -976,7 +978,7 @@ describe("Provision module", () => { it("should require webAppId for Web apps", async () => { const options: ProvisionFirebaseAppOptions = { project: { displayName: PROJECT_DISPLAY_NAME }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, }; // Mock API call to verify webAppId is included as appNamespace @@ -1094,7 +1096,7 @@ describe("Provision module", () => { const baseOptions: ProvisionFirebaseAppOptions = { project: { displayName: PROJECT_DISPLAY_NAME }, - app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID }, + app: { platform: AppPlatform.WEB, webAppId: WEB_APP_ID, displayName: PROJECT_DISPLAY_NAME }, }; it("should call correct API endpoint", async () => { diff --git a/src/management/provisioning/provision.ts b/src/management/provisioning/provision.ts index 0e0df7f88ce..463aebe294a 100644 --- a/src/management/provisioning/provision.ts +++ b/src/management/provisioning/provision.ts @@ -86,7 +86,7 @@ export function buildProvisionRequest( return { appNamespace: buildAppNamespace(options.app), - displayName: options.project.displayName, + displayName: options.app.displayName, ...(options.project.parent && { parent: buildParentString(options.project.parent) }), ...(options.features?.location && { location: options.features.location }), ...(options.requestId && { requestId: options.requestId }), diff --git a/src/management/provisioning/types.ts b/src/management/provisioning/types.ts index f0774fffadf..4ea2ce62659 100644 --- a/src/management/provisioning/types.ts +++ b/src/management/provisioning/types.ts @@ -3,6 +3,7 @@ import { AppPlatform } from "../apps"; interface BaseProvisionAppOptions { platform: AppPlatform; appId?: string; + displayName?: string; } interface IosAppOptions extends BaseProvisionAppOptions { diff --git a/src/mcp/tools/core/init.ts b/src/mcp/tools/core/init.ts index 6b7ffca2a78..eb933d8db7f 100644 --- a/src/mcp/tools/core/init.ts +++ b/src/mcp/tools/core/init.ts @@ -208,11 +208,12 @@ export const init = tool( const appInfo = parseAppId(features.ailogic.app_id); const projectInfo = await getFirebaseProject(projectId); validateProjectNumberMatch(appInfo, projectInfo); - await validateAppExists(appInfo); + const appData = await validateAppExists(appInfo, projectId); featuresList.push("ailogic"); featureInfo.ailogic = { appId: features.ailogic.app_id, + displayName: appData.displayName, }; } const setup: Setup = {