Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/mcp/prompts/apptesting/run_test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { prompt } from "../../prompt";

export const runTest = prompt(
"apptesting",
{
name: "run_test",
description: "Run a test with the Firebase App Testing agent",
Expand Down
2 changes: 2 additions & 0 deletions src/mcp/tools/apptesting/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const AIStepSchema = z
.describe("Step within a test case; run during the execution of the test.");

export const run_tests = tool(
"apptesting",
{
name: "run_test",
description: `Run a remote test.`,
Expand Down Expand Up @@ -66,6 +67,7 @@ export const run_tests = tool(
);

export const check_test = tool(
"apptesting",
{
name: "check_test",
description: "Check the status of a remote test.",
Expand Down
1 change: 1 addition & 0 deletions src/mcp/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const DETECTED_API_FEATURES: Record<ServerFeature, boolean | undefined> = {
functions: undefined,
remoteconfig: undefined,
crashlytics: undefined,
apptesting: undefined,
apphosting: undefined,
database: undefined,
};
Expand Down
107 changes: 107 additions & 0 deletions src/mcp/util/apptesting/availability.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// import { expect } from "chai";
import * as mockfs from "mock-fs";
import sinon from "sinon";
// import { McpContext } from "../../types";
// import { Config } from "../../../config";
// import { FirebaseMcpServer } from "../..";
// import { RC } from "../../../rc";
import * as ensureApiEnabled from "../../../ensureApiEnabled";
import { FirebaseMcpServer } from "../..";
import { RC } from "../../../rc";
import { Config } from "../../../config";
import { McpContext } from "../../types";
import { isAppTestingAvailable } from "./availability";
import { expect } from "chai";
// import { isAppTestingAvailable } from "./availability";

describe.only("isAppTestingAvailable", () => {
const sandbox: sinon.SinonSandbox = sinon.createSandbox();
let checkStub: sinon.SinonStub;

beforeEach(() => {
checkStub = sandbox.stub(ensureApiEnabled, "check");
});

afterEach(() => {
sandbox.restore();
mockfs.restore();
});

const mockContext = (projectDir: string): McpContext => ({
projectId: "test-project",
accountEmail: null,
config: {
projectDir: projectDir,
} as Config,
host: new FirebaseMcpServer({}),
rc: {} as RC,
firebaseCliCommand: "firebase",
});

it("returns false for non mobile project", async () => {
checkStub.resolves(true);
mockfs({
"/test-dir": {
"package.json": '{ "name": "web-app" }',
"index.html": "<html></html>",
},
});
const result = await isAppTestingAvailable(mockContext("/test-dir"));
expect(result).to.be.false;
});

it("returns false if App Distribution API isn't enabled", async () => {
checkStub.resolves(false);
mockfs({
"/test-dir": {
android: {
"build.gradle": "",
src: { main: {} },
},
},
});
const result = await isAppTestingAvailable(mockContext("/test-dir"));
expect(result).to.be.false;
});

it("returns true for an Android project with API enabled", async () => {
checkStub.resolves(true);
mockfs({
"/test-dir": {
android: {
"build.gradle": "",
src: { main: {} },
},
},
});
const result = await isAppTestingAvailable(mockContext("/test-dir"));
expect(result).to.be.true;
});

it("returns true for an iOS project with API enabled", async () => {
checkStub.resolves(true);
mockfs({
"/test-dir": {
ios: {
Podfile: "",
"Project.xcodeproj": {},
},
},
});
const result = await isAppTestingAvailable(mockContext("/test-dir"));
expect(result).to.be.true;
});

it("returns true for an Flutter project with API enabled", async () => {
checkStub.resolves(true);
mockfs({
"/test-dir": {
"pubspec.yaml": "",
ios: { "Runner.xcodeproj": {} },
android: { src: { main: {} } },
},
});
const result = await isAppTestingAvailable(mockContext("/test-dir"));
expect(result).to.be.true;
});
});
36 changes: 36 additions & 0 deletions src/mcp/util/apptesting/availability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { appDistributionOrigin } from "../../../api";
import { getPlatformsFromFolder, Platform } from "../../../appUtils";
import { check } from "../../../ensureApiEnabled";
import { timeoutFallback } from "../../../timeout";
import { McpContext } from "../../types";

/**
* Returns whether or not App Testing should be enabled
*/
export async function isAppTestingAvailable(ctx: McpContext): Promise<boolean> {
const host = ctx.host;
const projectDir = ctx.config.projectDir;
const platforms = await getPlatformsFromFolder(projectDir);

// If this is not a mobile app, then App Testing won't be enabled
if (
!platforms.includes(Platform.FLUTTER) &&
!platforms.includes(Platform.ANDROID) &&
!platforms.includes(Platform.IOS)
) {
host.log("debug", `Found no supported App Testing platforms.`);
return false;
}

// Checkf if App Distribution API is active
try {
return await timeoutFallback(
check(ctx.projectId, appDistributionOrigin(), "", true),
true,
3000,
);
} catch (e) {
// If there was a network error, default to enabling the feature
return true;
}
}
2 changes: 1 addition & 1 deletion src/mcp/util/availability.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe("getDefaultFeatureAvailabilityCheck", () => {

// Test all other features that rely on checkFeatureActive
const featuresThatUseCheckActive = SERVER_FEATURES.filter(
(f) => f !== "core" && f !== "crashlytics",
(f) => f !== "core" && f !== "crashlytics" && f !== "apptesting",
);

for (const feature of featuresThatUseCheckActive) {
Expand Down
2 changes: 2 additions & 0 deletions src/mcp/util/availability.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { McpContext, ServerFeature } from "../types";
import { checkFeatureActive } from "../util";
import { isCrashlyticsAvailable } from "./crashlytics/availability";
import { isAppTestingAvailable } from "./apptesting/availability";

const DEFAULT_AVAILABILITY_CHECKS: Record<ServerFeature, (ctx: McpContext) => Promise<boolean>> = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand All @@ -22,6 +23,7 @@ const DEFAULT_AVAILABILITY_CHECKS: Record<ServerFeature, (ctx: McpContext) => Pr
crashlytics: isCrashlyticsAvailable,
apphosting: (ctx: McpContext): Promise<boolean> =>
checkFeatureActive("apphosting", ctx.projectId, { config: ctx.config }),
apptesting: isAppTestingAvailable,
database: (ctx: McpContext): Promise<boolean> =>
checkFeatureActive("database", ctx.projectId, { config: ctx.config }),
};
Expand Down
Loading