-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(mcp): AI Logic Init Feature (CLI Command and MCP Firebase Init Tool) #9185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 18 commits
2b87a86
74f3687
6c2b93a
31e6327
b68727f
1c2cc10
d411761
a1cd048
647eb87
3309d71
0a0af59
193d196
5f3a6b9
e3afdee
9814ef9
6bedcaf
265d339
8b0ff79
d032ff7
82d5443
c3b33c6
a7de790
ade68ce
f1906de
671cec2
9dc2299
c8c02ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
import { expect } from "chai"; | ||
import * as sinon from "sinon"; | ||
import * as fs from "fs-extra"; | ||
import * as init from "./index"; | ||
import * as utils from "./utils"; | ||
import { Setup } from "../.."; | ||
import { Config } from "../../../config"; | ||
import { Platform } from "../../../dataconnect/types"; | ||
Check failure on line 8 in src/init/features/ailogic/index.spec.ts
|
||
|
||
describe("init ailogic", () => { | ||
let sandbox: sinon.SinonSandbox; | ||
|
||
beforeEach(() => { | ||
sandbox = sinon.createSandbox(); | ||
}); | ||
|
||
afterEach(() => { | ||
sandbox.restore(); | ||
}); | ||
|
||
describe("askQuestions", () => { | ||
it("should complete without throwing", async () => { | ||
// Skip detailed testing of askQuestions for now - it involves complex prompt mocking | ||
const mockSetup = { featureInfo: {} } as Setup; | ||
const mockConfig = {} as Config; | ||
|
||
// This test just ensures the function signature is correct | ||
// Real functionality testing would require mocking dynamic imports | ||
expect(() => init.askQuestions(mockSetup, mockConfig)).to.not.throw(); | ||
}); | ||
}); | ||
|
||
describe("actuate", () => { | ||
let setup: Setup; | ||
let config: Config; | ||
let detectAppPlatformStub: sinon.SinonStub; | ||
let buildProvisionOptionsStub: sinon.SinonStub; | ||
let provisionAiLogicAppStub: sinon.SinonStub; | ||
let writeAppConfigFileStub: sinon.SinonStub; | ||
let extractProjectIdStub: sinon.SinonStub; | ||
let getConfigFilePathStub: sinon.SinonStub; | ||
let existsSyncStub: sinon.SinonStub; | ||
|
||
beforeEach(() => { | ||
setup = { | ||
config: {} as any, | ||
Check warning on line 46 in src/init/features/ailogic/index.spec.ts
|
||
rcfile: { projects: {}, targets: {}, etags: {} }, | ||
featureInfo: { | ||
ailogic: { | ||
appNamespace: "com.example.test", | ||
appPlatform: "android", | ||
overwriteConfig: false, | ||
}, | ||
}, | ||
projectId: "test-project", | ||
instructions: [], | ||
} as Setup; | ||
|
||
config = { | ||
projectDir: "/test/project", | ||
} as Config; | ||
|
||
// Stub all utility functions | ||
detectAppPlatformStub = sandbox.stub(utils, "detectAppPlatform"); | ||
buildProvisionOptionsStub = sandbox.stub(utils, "buildProvisionOptions"); | ||
provisionAiLogicAppStub = sandbox.stub(utils, "provisionAiLogicApp"); | ||
writeAppConfigFileStub = sandbox.stub(utils, "writeAppConfigFile"); | ||
extractProjectIdStub = sandbox.stub(utils, "extractProjectIdFromAppResource"); | ||
getConfigFilePathStub = sandbox.stub(utils, "getConfigFilePath"); | ||
existsSyncStub = sandbox.stub(fs, "existsSync"); | ||
}); | ||
|
||
it("should return early if no ailogic feature info", async () => { | ||
setup.featureInfo = {}; | ||
|
||
await init.actuate(setup, config); | ||
|
||
// No stubs should be called | ||
sinon.assert.notCalled(detectAppPlatformStub); | ||
sinon.assert.notCalled(provisionAiLogicAppStub); | ||
}); | ||
|
||
it("should use provided app platform", async () => { | ||
const configFilePath = "/test/project/google-services.json"; | ||
getConfigFilePathStub.returns(configFilePath); | ||
existsSyncStub.returns(false); | ||
buildProvisionOptionsStub.returns({ mock: "options" }); | ||
provisionAiLogicAppStub.returns({ | ||
appResource: "projects/test-project/apps/test-app", | ||
configData: "base64config", | ||
}); | ||
extractProjectIdStub.returns("test-project"); | ||
|
||
await init.actuate(setup, config); | ||
|
||
// Should not call detectAppPlatform since platform is provided | ||
sinon.assert.notCalled(detectAppPlatformStub); | ||
sinon.assert.calledWith(getConfigFilePathStub, "/test/project", "android"); | ||
sinon.assert.calledWith(buildProvisionOptionsStub, "test-project", "android", "com.example.test"); | ||
Check failure on line 99 in src/init/features/ailogic/index.spec.ts
|
||
}); | ||
|
||
it("should auto-detect platform when not provided", async () => { | ||
setup.featureInfo!.ailogic!.appPlatform = undefined; | ||
Check warning on line 103 in src/init/features/ailogic/index.spec.ts
|
||
const configFilePath = "/test/project/firebase-config.json"; | ||
|
||
detectAppPlatformStub.returns("web"); | ||
getConfigFilePathStub.returns(configFilePath); | ||
existsSyncStub.returns(false); | ||
buildProvisionOptionsStub.returns({ mock: "options" }); | ||
provisionAiLogicAppStub.returns({ | ||
appResource: "projects/test-project/apps/test-app", | ||
configData: "base64config", | ||
}); | ||
extractProjectIdStub.returns("test-project"); | ||
|
||
await init.actuate(setup, config); | ||
|
||
sinon.assert.calledWith(detectAppPlatformStub, "/test/project"); | ||
sinon.assert.calledWith(getConfigFilePathStub, "/test/project", "web"); | ||
sinon.assert.calledWith(buildProvisionOptionsStub, "test-project", "web", "com.example.test"); | ||
}); | ||
|
||
it("should throw error if config file exists and overwrite not enabled", async () => { | ||
const configFilePath = "/test/project/google-services.json"; | ||
getConfigFilePathStub.returns(configFilePath); | ||
existsSyncStub.returns(true); | ||
|
||
await expect(init.actuate(setup, config)).to.be.rejectedWith( | ||
"AI Logic setup failed: Config file /test/project/google-services.json already exists. Use overwrite_config: true to update it." | ||
Check failure on line 129 in src/init/features/ailogic/index.spec.ts
|
||
); | ||
}); | ||
|
||
it("should proceed if config file exists and overwrite is enabled", async () => { | ||
setup.featureInfo!.ailogic!.overwriteConfig = true; | ||
Check warning on line 134 in src/init/features/ailogic/index.spec.ts
|
||
const configFilePath = "/test/project/google-services.json"; | ||
|
||
getConfigFilePathStub.returns(configFilePath); | ||
existsSyncStub.returns(true); | ||
buildProvisionOptionsStub.returns({ mock: "options" }); | ||
provisionAiLogicAppStub.returns({ | ||
appResource: "projects/test-project/apps/test-app", | ||
configData: "base64config", | ||
}); | ||
extractProjectIdStub.returns("test-project"); | ||
|
||
await init.actuate(setup, config); | ||
|
||
sinon.assert.called(provisionAiLogicAppStub); | ||
sinon.assert.calledWith(writeAppConfigFileStub, configFilePath, "base64config"); | ||
}); | ||
|
||
it("should provision app and write config file", async () => { | ||
const configFilePath = "/test/project/google-services.json"; | ||
const mockResponse = { | ||
appResource: "projects/new-project/apps/test-app", | ||
configData: "base64configdata", | ||
}; | ||
|
||
getConfigFilePathStub.returns(configFilePath); | ||
existsSyncStub.returns(false); | ||
buildProvisionOptionsStub.returns({ mock: "options" }); | ||
provisionAiLogicAppStub.returns(mockResponse); | ||
extractProjectIdStub.returns("new-project"); | ||
|
||
await init.actuate(setup, config); | ||
|
||
sinon.assert.calledWith(provisionAiLogicAppStub, { mock: "options" }); | ||
sinon.assert.calledWith(extractProjectIdStub, "projects/new-project/apps/test-app"); | ||
sinon.assert.calledWith(writeAppConfigFileStub, configFilePath, "base64configdata"); | ||
expect(setup.projectId).to.equal("new-project"); | ||
}); | ||
|
||
it("should update .firebaserc with new project", async () => { | ||
const configFilePath = "/test/project/google-services.json"; | ||
|
||
getConfigFilePathStub.returns(configFilePath); | ||
existsSyncStub.returns(false); | ||
buildProvisionOptionsStub.returns({ mock: "options" }); | ||
provisionAiLogicAppStub.returns({ | ||
appResource: "projects/new-project/apps/test-app", | ||
configData: "base64config", | ||
}); | ||
extractProjectIdStub.returns("new-project"); | ||
|
||
await init.actuate(setup, config); | ||
|
||
expect(setup.rcfile!.projects!.default).to.equal("new-project"); | ||
Check warning on line 187 in src/init/features/ailogic/index.spec.ts
|
||
}); | ||
|
||
it("should add appropriate instructions", async () => { | ||
const configFilePath = "/test/project/google-services.json"; | ||
|
||
getConfigFilePathStub.returns(configFilePath); | ||
existsSyncStub.returns(false); | ||
buildProvisionOptionsStub.returns({ mock: "options" }); | ||
provisionAiLogicAppStub.returns({ | ||
appResource: "projects/test-project/apps/test-app", | ||
configData: "base64config", | ||
}); | ||
extractProjectIdStub.returns("test-project"); | ||
|
||
await init.actuate(setup, config); | ||
|
||
expect(setup.instructions).to.include("Firebase AI Logic has been enabled with a new android app."); | ||
Check failure on line 204 in src/init/features/ailogic/index.spec.ts
|
||
expect(setup.instructions).to.include(`Config file written to: ${configFilePath}`); | ||
expect(setup.instructions).to.include("If you have multiple app directories, copy the config file to the appropriate app folder."); | ||
Check failure on line 206 in src/init/features/ailogic/index.spec.ts
|
||
expect(setup.instructions).to.include("Note: A new Firebase app was created. You can use existing Firebase apps with AI Logic (current API limitation)."); | ||
Check failure on line 207 in src/init/features/ailogic/index.spec.ts
|
||
}); | ||
|
||
it("should handle provisioning errors gracefully", async () => { | ||
const configFilePath = "/test/project/google-services.json"; | ||
|
||
getConfigFilePathStub.returns(configFilePath); | ||
existsSyncStub.returns(false); | ||
buildProvisionOptionsStub.returns({ mock: "options" }); | ||
provisionAiLogicAppStub.throws(new Error("Provisioning API failed")); | ||
|
||
await expect(init.actuate(setup, config)).to.be.rejectedWith( | ||
"AI Logic setup failed: Provisioning API failed" | ||
Check failure on line 219 in src/init/features/ailogic/index.spec.ts
|
||
); | ||
}); | ||
|
||
it("should handle missing rcfile gracefully", async () => { | ||
setup.rcfile = undefined as any; | ||
const configFilePath = "/test/project/google-services.json"; | ||
|
||
getConfigFilePathStub.returns(configFilePath); | ||
existsSyncStub.returns(false); | ||
buildProvisionOptionsStub.returns({ mock: "options" }); | ||
provisionAiLogicAppStub.returns({ | ||
appResource: "projects/test-project/apps/test-app", | ||
configData: "base64config", | ||
}); | ||
extractProjectIdStub.returns("test-project"); | ||
|
||
// Should not throw an error | ||
await init.actuate(setup, config); | ||
|
||
sinon.assert.called(provisionAiLogicAppStub); | ||
}); | ||
}); | ||
}); | ||
Check failure on line 242 in src/init/features/ailogic/index.spec.ts
|
Uh oh!
There was an error while loading. Please reload this page.