Skip to content

Commit 380b001

Browse files
authored
Add custom auto-enablement for app testing (#9373)
* Add custom auto-enablement for app testing * Address gemini code assist comments * Fix intersection bug * Fix issues with test
1 parent c081d6d commit 380b001

File tree

7 files changed

+142
-1
lines changed

7 files changed

+142
-1
lines changed

src/mcp/prompts/apptesting/run_test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { prompt } from "../../prompt";
22

33
export const runTest = prompt(
4+
"apptesting",
45
{
56
name: "run_test",
67
description: "Run a test with the Firebase App Testing agent",

src/mcp/tools/apptesting/tests.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const AIStepSchema = z
3535
.describe("Step within a test case; run during the execution of the test.");
3636

3737
export const run_tests = tool(
38+
"apptesting",
3839
{
3940
name: "run_test",
4041
description: `Run a remote test.`,
@@ -66,6 +67,7 @@ export const run_tests = tool(
6667
);
6768

6869
export const check_test = tool(
70+
"apptesting",
6971
{
7072
name: "check_test",
7173
description: "Check the status of a remote test.",

src/mcp/util.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ const DETECTED_API_FEATURES: Record<ServerFeature, boolean | undefined> = {
8989
functions: undefined,
9090
remoteconfig: undefined,
9191
crashlytics: undefined,
92+
apptesting: undefined,
9293
apphosting: undefined,
9394
database: undefined,
9495
};
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import * as mockfs from "mock-fs";
2+
import * as sinon from "sinon";
3+
import { FirebaseMcpServer } from "../../index";
4+
import { RC } from "../../../rc";
5+
import { Config } from "../../../config";
6+
import { McpContext } from "../../types";
7+
import { isAppTestingAvailable } from "./availability";
8+
import { expect } from "chai";
9+
import * as ensureApiEnabled from "../../../ensureApiEnabled";
10+
11+
describe("isAppTestingAvailable", () => {
12+
let sandbox: sinon.SinonSandbox;
13+
let checkStub: sinon.SinonStub;
14+
15+
beforeEach(() => {
16+
sandbox = sinon.createSandbox();
17+
checkStub = sandbox.stub(ensureApiEnabled, "check");
18+
});
19+
20+
afterEach(() => {
21+
sandbox.restore();
22+
mockfs.restore();
23+
});
24+
25+
const mockContext = (projectDir: string): McpContext => ({
26+
projectId: "test-project",
27+
accountEmail: null,
28+
config: {
29+
projectDir: projectDir,
30+
} as Config,
31+
host: new FirebaseMcpServer({}),
32+
rc: {} as RC,
33+
firebaseCliCommand: "firebase",
34+
});
35+
36+
it("returns false for non mobile project", async () => {
37+
checkStub.resolves(true);
38+
mockfs({
39+
"/test-dir": {
40+
"package.json": '{ "name": "web-app" }',
41+
"index.html": "<html></html>",
42+
},
43+
});
44+
const result = await isAppTestingAvailable(mockContext("/test-dir"));
45+
expect(result).to.be.false;
46+
});
47+
48+
it("returns false if App Distribution API isn't enabled", async () => {
49+
checkStub.resolves(false);
50+
mockfs({
51+
"/test-dir": {
52+
android: {
53+
"build.gradle": "",
54+
src: { main: {} },
55+
},
56+
},
57+
});
58+
const result = await isAppTestingAvailable(mockContext("/test-dir"));
59+
expect(result).to.be.false;
60+
});
61+
62+
it("returns true for an Android project with API enabled", async () => {
63+
checkStub.resolves(true);
64+
mockfs({
65+
"/test-dir": {
66+
android: {
67+
"build.gradle": "",
68+
src: { main: {} },
69+
},
70+
},
71+
});
72+
const result = await isAppTestingAvailable(mockContext("/test-dir"));
73+
expect(result).to.be.true;
74+
});
75+
76+
it("returns true for an iOS project with API enabled", async () => {
77+
checkStub.resolves(true);
78+
mockfs({
79+
"/test-dir": {
80+
ios: {
81+
Podfile: "",
82+
"Project.xcodeproj": {},
83+
},
84+
},
85+
});
86+
const result = await isAppTestingAvailable(mockContext("/test-dir"));
87+
expect(result).to.be.true;
88+
});
89+
90+
it("returns true for an Flutter project with API enabled", async () => {
91+
checkStub.resolves(true);
92+
mockfs({
93+
"/test-dir": {
94+
"pubspec.yaml": "",
95+
ios: { "Runner.xcodeproj": {} },
96+
android: { src: { main: {} } },
97+
},
98+
});
99+
const result = await isAppTestingAvailable(mockContext("/test-dir"));
100+
expect(result).to.be.true;
101+
});
102+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { appDistributionOrigin } from "../../../api";
2+
import { getPlatformsFromFolder, Platform } from "../../../appUtils";
3+
import { check } from "../../../ensureApiEnabled";
4+
import { timeoutFallback } from "../../../timeout";
5+
import { McpContext } from "../../types";
6+
7+
/**
8+
* Returns whether or not App Testing should be enabled
9+
*/
10+
export async function isAppTestingAvailable(ctx: McpContext): Promise<boolean> {
11+
const host = ctx.host;
12+
const projectDir = ctx.config.projectDir;
13+
const platforms = await getPlatformsFromFolder(projectDir);
14+
15+
const supportedPlatforms = [Platform.FLUTTER, Platform.ANDROID, Platform.IOS];
16+
17+
if (!platforms.some((p) => supportedPlatforms.includes(p))) {
18+
host.log("debug", `Found no supported App Testing platforms.`);
19+
return false;
20+
}
21+
22+
// Checkf if App Distribution API is active
23+
try {
24+
return await timeoutFallback(
25+
check(ctx.projectId, appDistributionOrigin(), "", true),
26+
true,
27+
3000,
28+
);
29+
} catch (e) {
30+
// If there was a network error, default to enabling the feature
31+
return true;
32+
}
33+
}

src/mcp/util/availability.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ describe("getDefaultFeatureAvailabilityCheck", () => {
4242

4343
// Test all other features that rely on checkFeatureActive
4444
const featuresThatUseCheckActive = SERVER_FEATURES.filter(
45-
(f) => f !== "core" && f !== "crashlytics",
45+
(f) => f !== "core" && f !== "crashlytics" && f !== "apptesting",
4646
);
4747

4848
for (const feature of featuresThatUseCheckActive) {

src/mcp/util/availability.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { McpContext, ServerFeature } from "../types";
22
import { checkFeatureActive } from "../util";
33
import { isCrashlyticsAvailable } from "./crashlytics/availability";
4+
import { isAppTestingAvailable } from "./apptesting/availability";
45

56
const DEFAULT_AVAILABILITY_CHECKS: Record<ServerFeature, (ctx: McpContext) => Promise<boolean>> = {
67
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -22,6 +23,7 @@ const DEFAULT_AVAILABILITY_CHECKS: Record<ServerFeature, (ctx: McpContext) => Pr
2223
crashlytics: isCrashlyticsAvailable,
2324
apphosting: (ctx: McpContext): Promise<boolean> =>
2425
checkFeatureActive("apphosting", ctx.projectId, { config: ctx.config }),
26+
apptesting: isAppTestingAvailable,
2527
database: (ctx: McpContext): Promise<boolean> =>
2628
checkFeatureActive("database", ctx.projectId, { config: ctx.config }),
2729
};

0 commit comments

Comments
 (0)