Skip to content

Commit f35d6d9

Browse files
committed
Add custom auto-enablement for app testing
1 parent c081d6d commit f35d6d9

File tree

7 files changed

+150
-1
lines changed

7 files changed

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

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)