firebase-tools 14.24.0 → 14.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/appUtils.js CHANGED
@@ -32,8 +32,6 @@ async function detectApps(dirPath) {
32
32
  const srcMainFolders = await detectFiles(dirPath, "src/main/");
33
33
  const xCodeProjects = await detectFiles(dirPath, "*.xcodeproj/");
34
34
  const adminAndWebApps = (await Promise.all(packageJsonFiles.map((p) => packageJsonToAdminOrWebApp(dirPath, p)))).flat();
35
- console.log("packageJsonFiles", packageJsonFiles);
36
- console.log("adminAndWebApps", adminAndWebApps);
37
35
  const flutterAppPromises = await Promise.all(pubSpecYamlFiles.map((f) => processFlutterDir(dirPath, f)));
38
36
  const flutterApps = flutterAppPromises.flat();
39
37
  const androidAppPromises = await Promise.all(srcMainFolders.map((f) => processAndroidDir(dirPath, f)));
@@ -228,7 +228,7 @@ class AppDistributionClient {
228
228
  }
229
229
  utils.logSuccess(`Testers removed from group successfully`);
230
230
  }
231
- async createReleaseTest(releaseName, devices, loginCredential, testCaseName) {
231
+ async createReleaseTest(releaseName, devices, aiInstruction, loginCredential, testCaseName) {
232
232
  try {
233
233
  const response = await this.appDistroV1AlphaClient.request({
234
234
  method: "POST",
@@ -237,6 +237,7 @@ class AppDistributionClient {
237
237
  deviceExecutions: devices.map((device) => ({ device })),
238
238
  loginCredential,
239
239
  testCase: testCaseName,
240
+ aiInstructions: aiInstruction,
240
241
  },
241
242
  });
242
243
  return response.body;
@@ -1,16 +1,55 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Distribution = exports.DistributionFileType = void 0;
3
+ exports.awaitTestResults = exports.Distribution = exports.upload = exports.DistributionFileType = void 0;
4
4
  const fs = require("fs-extra");
5
- const error_1 = require("../error");
6
5
  const logger_1 = require("../logger");
7
6
  const pathUtil = require("path");
7
+ const utils = require("../utils");
8
+ const types_1 = require("../appdistribution/types");
9
+ const error_1 = require("../error");
10
+ const TEST_MAX_POLLING_RETRIES = 40;
11
+ const TEST_POLLING_INTERVAL_MILLIS = 30000;
8
12
  var DistributionFileType;
9
13
  (function (DistributionFileType) {
10
14
  DistributionFileType["IPA"] = "ipa";
11
15
  DistributionFileType["APK"] = "apk";
12
16
  DistributionFileType["AAB"] = "aab";
13
17
  })(DistributionFileType = exports.DistributionFileType || (exports.DistributionFileType = {}));
18
+ async function upload(requests, appName, distribution) {
19
+ utils.logBullet("uploading binary...");
20
+ try {
21
+ const operationName = await requests.uploadRelease(appName, distribution);
22
+ const uploadResponse = await requests.pollUploadStatus(operationName);
23
+ const release = uploadResponse.release;
24
+ switch (uploadResponse.result) {
25
+ case types_1.UploadReleaseResult.RELEASE_CREATED:
26
+ utils.logSuccess(`uploaded new release ${release.displayVersion} (${release.buildVersion}) successfully!`);
27
+ break;
28
+ case types_1.UploadReleaseResult.RELEASE_UPDATED:
29
+ utils.logSuccess(`uploaded update to existing release ${release.displayVersion} (${release.buildVersion}) successfully!`);
30
+ break;
31
+ case types_1.UploadReleaseResult.RELEASE_UNMODIFIED:
32
+ utils.logSuccess(`re-uploaded already existing release ${release.displayVersion} (${release.buildVersion}) successfully!`);
33
+ break;
34
+ default:
35
+ utils.logSuccess(`uploaded release ${release.displayVersion} (${release.buildVersion}) successfully!`);
36
+ }
37
+ utils.logSuccess(`View this release in the Firebase console: ${release.firebaseConsoleUri}`);
38
+ utils.logSuccess(`Share this release with testers who have access: ${release.testingUri}`);
39
+ utils.logSuccess(`Download the release binary (link expires in 1 hour): ${release.binaryDownloadUri}`);
40
+ return uploadResponse.release.name;
41
+ }
42
+ catch (err) {
43
+ if ((0, error_1.getErrStatus)(err) === 404) {
44
+ throw new error_1.FirebaseError(`App Distribution could not find your app ${appName}. ` +
45
+ `Make sure to onboard your app by pressing the "Get started" ` +
46
+ "button on the App Distribution page in the Firebase console: " +
47
+ "https://console.firebase.google.com/project/_/appdistribution", { exit: 1 });
48
+ }
49
+ throw new error_1.FirebaseError(`Failed to upload release. ${(0, error_1.getErrMsg)(err)}`, { exit: 1 });
50
+ }
51
+ }
52
+ exports.upload = upload;
14
53
  class Distribution {
15
54
  constructor(path) {
16
55
  this.path = path;
@@ -49,3 +88,47 @@ class Distribution {
49
88
  }
50
89
  }
51
90
  exports.Distribution = Distribution;
91
+ async function awaitTestResults(releaseTests, requests) {
92
+ const releaseTestNames = new Set(releaseTests.map((rt) => rt.name).filter((n) => !!n));
93
+ for (let i = 0; i < TEST_MAX_POLLING_RETRIES; i++) {
94
+ utils.logBullet(`${releaseTestNames.size} automated test results are pending...`);
95
+ await delay(TEST_POLLING_INTERVAL_MILLIS);
96
+ for (const releaseTestName of releaseTestNames) {
97
+ const releaseTest = await requests.getReleaseTest(releaseTestName);
98
+ if (releaseTest.deviceExecutions.every((e) => e.state === "PASSED")) {
99
+ releaseTestNames.delete(releaseTestName);
100
+ if (releaseTestNames.size === 0) {
101
+ utils.logSuccess("Automated test(s) passed!");
102
+ return;
103
+ }
104
+ else {
105
+ continue;
106
+ }
107
+ }
108
+ for (const execution of releaseTest.deviceExecutions) {
109
+ const device = deviceToString(execution.device);
110
+ switch (execution.state) {
111
+ case "PASSED":
112
+ case "IN_PROGRESS":
113
+ continue;
114
+ case "FAILED":
115
+ throw new error_1.FirebaseError(`Automated test failed for ${device}: ${execution.failedReason}`, { exit: 1 });
116
+ case "INCONCLUSIVE":
117
+ throw new error_1.FirebaseError(`Automated test inconclusive for ${device}: ${execution.inconclusiveReason}`, { exit: 1 });
118
+ default:
119
+ throw new error_1.FirebaseError(`Unsupported automated test state for ${device}: ${execution.state}`, { exit: 1 });
120
+ }
121
+ }
122
+ }
123
+ }
124
+ throw new error_1.FirebaseError("It took longer than expected to run your test(s), please try again.", {
125
+ exit: 1,
126
+ });
127
+ }
128
+ exports.awaitTestResults = awaitTestResults;
129
+ function delay(ms) {
130
+ return new Promise((resolve) => setTimeout(resolve, ms));
131
+ }
132
+ function deviceToString(device) {
133
+ return `${device.model} (${device.version}/${device.orientation}/${device.locale})`;
134
+ }
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getLoginCredential = exports.parseTestDevices = exports.getAppName = exports.getProjectName = exports.ensureFileExists = exports.getEmails = exports.parseIntoStringArray = void 0;
3
+ exports.getLoginCredential = exports.parseTestDevices = exports.toAppName = exports.getAppName = exports.getProjectName = exports.ensureFileExists = exports.getEmails = exports.parseIntoStringArray = void 0;
4
4
  const fs = require("fs-extra");
5
5
  const error_1 = require("../error");
6
6
  const projectUtils_1 = require("../projectUtils");
7
- function parseIntoStringArray(value, file) {
7
+ function parseIntoStringArray(value, file = "") {
8
8
  if (!value && file) {
9
9
  ensureFileExists(file);
10
10
  value = fs.readFileSync(file, "utf8");
@@ -45,11 +45,14 @@ function getAppName(options) {
45
45
  if (!options.app) {
46
46
  throw new error_1.FirebaseError("set the --app option to a valid Firebase app id and try again");
47
47
  }
48
- const appId = options.app;
49
- return `projects/${appId.split(":")[1]}/apps/${appId}`;
48
+ return toAppName(options.app);
50
49
  }
51
50
  exports.getAppName = getAppName;
52
- function parseTestDevices(value, file) {
51
+ function toAppName(appId) {
52
+ return `projects/${appId.split(":")[1]}/apps/${appId}`;
53
+ }
54
+ exports.toAppName = toAppName;
55
+ function parseTestDevices(value, file = "") {
53
56
  if (!value && file) {
54
57
  ensureFileExists(file);
55
58
  value = fs.readFileSync(file, "utf8");
@@ -3,15 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.command = void 0;
4
4
  const fs = require("fs-extra");
5
5
  const command_1 = require("../command");
6
- const utils = require("../utils");
7
6
  const requireAuth_1 = require("../requireAuth");
8
- const client_1 = require("../appdistribution/client");
9
- const types_1 = require("../appdistribution/types");
10
7
  const error_1 = require("../error");
11
8
  const distribution_1 = require("../appdistribution/distribution");
12
9
  const options_parser_util_1 = require("../appdistribution/options-parser-util");
13
- const TEST_MAX_POLLING_RETRIES = 40;
14
- const TEST_POLLING_INTERVAL_MILLIS = 30000;
10
+ const types_1 = require("../appdistribution/types");
11
+ const client_1 = require("../appdistribution/client");
12
+ const utils = require("../utils");
15
13
  function getReleaseNotes(releaseNotes, releaseNotesFile) {
16
14
  if (releaseNotes) {
17
15
  return releaseNotes.replace(/\\n/g, "\n");
@@ -60,6 +58,9 @@ exports.command = new command_1.Command("appdistribution:distribute <release-bin
60
58
  usernameResourceName: options.testUsernameResource,
61
59
  passwordResourceName: options.testPasswordResource,
62
60
  });
61
+ await distribute(appName, distribution, testCases, testDevices, releaseNotes, testers, groups, options.testNonBlocking, loginCredential);
62
+ });
63
+ async function distribute(appName, distribution, testCases, testDevices, releaseNotes, testers, groups, testNonBlocking, loginCredential) {
63
64
  const requests = new client_1.AppDistributionClient();
64
65
  let aabInfo;
65
66
  if (distribution.distributionFileType() === distribution_1.DistributionFileType.AAB) {
@@ -68,7 +69,7 @@ exports.command = new command_1.Command("appdistribution:distribute <release-bin
68
69
  }
69
70
  catch (err) {
70
71
  if ((0, error_1.getErrStatus)(err) === 404) {
71
- throw new error_1.FirebaseError(`App Distribution could not find your app ${options.app}. ` +
72
+ throw new error_1.FirebaseError(`App Distribution could not find your app ${appName}. ` +
72
73
  `Make sure to onboard your app by pressing the "Get started" ` +
73
74
  "button on the App Distribution page in the Firebase console: " +
74
75
  "https://console.firebase.google.com/project/_/appdistribution", { exit: 1 });
@@ -96,39 +97,7 @@ exports.command = new command_1.Command("appdistribution:distribute <release-bin
96
97
  }
97
98
  }
98
99
  }
99
- utils.logBullet("uploading binary...");
100
- let releaseName;
101
- try {
102
- const operationName = await requests.uploadRelease(appName, distribution);
103
- const uploadResponse = await requests.pollUploadStatus(operationName);
104
- const release = uploadResponse.release;
105
- switch (uploadResponse.result) {
106
- case types_1.UploadReleaseResult.RELEASE_CREATED:
107
- utils.logSuccess(`uploaded new release ${release.displayVersion} (${release.buildVersion}) successfully!`);
108
- break;
109
- case types_1.UploadReleaseResult.RELEASE_UPDATED:
110
- utils.logSuccess(`uploaded update to existing release ${release.displayVersion} (${release.buildVersion}) successfully!`);
111
- break;
112
- case types_1.UploadReleaseResult.RELEASE_UNMODIFIED:
113
- utils.logSuccess(`re-uploaded already existing release ${release.displayVersion} (${release.buildVersion}) successfully!`);
114
- break;
115
- default:
116
- utils.logSuccess(`uploaded release ${release.displayVersion} (${release.buildVersion}) successfully!`);
117
- }
118
- utils.logSuccess(`View this release in the Firebase console: ${release.firebaseConsoleUri}`);
119
- utils.logSuccess(`Share this release with testers who have access: ${release.testingUri}`);
120
- utils.logSuccess(`Download the release binary (link expires in 1 hour): ${release.binaryDownloadUri}`);
121
- releaseName = uploadResponse.release.name;
122
- }
123
- catch (err) {
124
- if ((0, error_1.getErrStatus)(err) === 404) {
125
- throw new error_1.FirebaseError(`App Distribution could not find your app ${options.app}. ` +
126
- `Make sure to onboard your app by pressing the "Get started" ` +
127
- "button on the App Distribution page in the Firebase console: " +
128
- "https://console.firebase.google.com/project/_/appdistribution", { exit: 1 });
129
- }
130
- throw new error_1.FirebaseError(`Failed to upload release. ${(0, error_1.getErrMsg)(err)}`, { exit: 1 });
131
- }
100
+ const releaseName = await (0, distribution_1.upload)(requests, appName, distribution);
132
101
  if (aabInfo && !aabInfo.testCertificate) {
133
102
  aabInfo = await requests.getAabInfo(appName);
134
103
  if (aabInfo.testCertificate) {
@@ -147,59 +116,17 @@ exports.command = new command_1.Command("appdistribution:distribute <release-bin
147
116
  utils.logBullet("starting automated test (note: this feature is in beta)");
148
117
  const releaseTestPromises = [];
149
118
  if (!testCases.length) {
150
- releaseTestPromises.push(requests.createReleaseTest(releaseName, testDevices, loginCredential));
119
+ releaseTestPromises.push(requests.createReleaseTest(releaseName, testDevices, undefined, loginCredential));
151
120
  }
152
121
  else {
153
122
  for (const testCaseId of testCases) {
154
- releaseTestPromises.push(requests.createReleaseTest(releaseName, testDevices, loginCredential, `${appName}/testCases/${testCaseId}`));
123
+ releaseTestPromises.push(requests.createReleaseTest(releaseName, testDevices, undefined, loginCredential, `${appName}/testCases/${testCaseId}`));
155
124
  }
156
125
  }
157
126
  const releaseTests = await Promise.all(releaseTestPromises);
158
127
  utils.logSuccess(`${releaseTests.length} release test(s) started successfully`);
159
- if (!options.testNonBlocking) {
160
- await awaitTestResults(releaseTests, requests);
128
+ if (!testNonBlocking) {
129
+ await (0, distribution_1.awaitTestResults)(releaseTests, requests);
161
130
  }
162
131
  }
163
- });
164
- async function awaitTestResults(releaseTests, requests) {
165
- const releaseTestNames = new Set(releaseTests.map((rt) => rt.name));
166
- for (let i = 0; i < TEST_MAX_POLLING_RETRIES; i++) {
167
- utils.logBullet(`${releaseTestNames.size} automated test results are pending...`);
168
- await delay(TEST_POLLING_INTERVAL_MILLIS);
169
- for (const releaseTestName of releaseTestNames) {
170
- const releaseTest = await requests.getReleaseTest(releaseTestName);
171
- if (releaseTest.deviceExecutions.every((e) => e.state === "PASSED")) {
172
- releaseTestNames.delete(releaseTestName);
173
- if (releaseTestNames.size === 0) {
174
- utils.logSuccess("Automated test(s) passed!");
175
- return;
176
- }
177
- else {
178
- continue;
179
- }
180
- }
181
- for (const execution of releaseTest.deviceExecutions) {
182
- switch (execution.state) {
183
- case "PASSED":
184
- case "IN_PROGRESS":
185
- continue;
186
- case "FAILED":
187
- throw new error_1.FirebaseError(`Automated test failed for ${deviceToString(execution.device)}: ${execution.failedReason}`, { exit: 1 });
188
- case "INCONCLUSIVE":
189
- throw new error_1.FirebaseError(`Automated test inconclusive for ${deviceToString(execution.device)}: ${execution.inconclusiveReason}`, { exit: 1 });
190
- default:
191
- throw new error_1.FirebaseError(`Unsupported automated test state for ${deviceToString(execution.device)}: ${execution.state}`, { exit: 1 });
192
- }
193
- }
194
- }
195
- }
196
- throw new error_1.FirebaseError("It took longer than expected to run your test(s), please try again.", {
197
- exit: 1,
198
- });
199
- }
200
- function delay(ms) {
201
- return new Promise((resolve) => setTimeout(resolve, ms));
202
- }
203
- function deviceToString(device) {
204
- return `${device.model} (${device.version}/${device.orientation}/${device.locale})`;
205
132
  }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.apptestingPrompts = void 0;
4
+ const experiments_1 = require("../../../experiments");
5
+ const run_test_1 = require("./run_test");
6
+ exports.apptestingPrompts = [];
7
+ if ((0, experiments_1.isEnabled)("mcpalpha")) {
8
+ exports.apptestingPrompts.push(run_test_1.runTest);
9
+ }
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runTest = void 0;
4
+ const prompt_1 = require("../../prompt");
5
+ exports.runTest = (0, prompt_1.prompt)("apptesting", {
6
+ name: "run_test",
7
+ description: "Run a test with the Firebase App Testing agent",
8
+ omitPrefix: false,
9
+ arguments: [
10
+ {
11
+ name: "testDescription",
12
+ description: "Description of the test you want to run. The agent will use the description to generate a test case that will be used as input for the AI-guided test",
13
+ required: false,
14
+ },
15
+ ],
16
+ annotations: {
17
+ title: "Run an App Testing AI-guided test",
18
+ },
19
+ }, async ({ testDescription }, { accountEmail, projectId }) => {
20
+ return [
21
+ {
22
+ role: "user",
23
+ content: {
24
+ type: "text",
25
+ text: `
26
+ You are going to help a developer run a test for their mobile app
27
+ using the Firebase App Testing agent.
28
+
29
+ Active user: ${accountEmail || "<NONE>"}
30
+ Project ID: ${projectId || "<NONE>"}
31
+
32
+ ## Prerequisites
33
+
34
+ Here are a list of prerequisite steps that must be completed before running a test.
35
+
36
+ 1. **Make sure this is an Android app**. The App Testing agent only works with Android apps. If
37
+ this is not an Android app, instruct the user that this command can't be used with this app.
38
+ 2. **Make sure the user is logged in. No App Testing tools will work if the user is not logged in.**
39
+ a. Use the \`firebase_get_environment\` tool to verify that the user is logged in.
40
+ b. If the Firebase 'Active user' is set to <NONE>, instruct the user to run \`firebase login\`
41
+ before continuing. Ignore other fields that are set to <NONE>. We are just making sure the
42
+ user is logged in.
43
+ 3. **Get the Firebase app ID.**
44
+ The \`firebase_get_environment\` tool should return a list of detected app IDs, where the app
45
+ ID contains four colon (":") delimited parts: a version number (typically "1"), a project
46
+ number, a platform type ("android", "ios", or "web"). Ask the user confirm if there is only
47
+ a single app ID, or to choose one if there are multiple app IDs.
48
+
49
+ If the tool does not return a list of detected apps, just ask the user for it.
50
+
51
+ 4. **Confirm that the application ID of the app matches the bundle ID of the Firebase app**
52
+
53
+ The \`firebase_get_environment\` tool returns a list of detected app IDs mapped to their corresponding
54
+ bundle IDs. If the developer selected an app ID from the the list of detected app IDs, this already
55
+ confirms that the bundle ID matches the app ID. If not, get the application IDs of all the variants of
56
+ the app. Then get the bundle ID of the Firebase app by calling the \`firebase_list_apps\` tool and
57
+ confirming that the \`namespace\` field of the app with the selected app ID matches one of the application
58
+ IDs of the variants.
59
+
60
+ ## Test Case Generation
61
+
62
+ Once you have completed the required steps, you need the help the user generate a "test case", which is the input to the
63
+ app testing agent. A test case consists of multiple steps where each step contains the following fields:
64
+
65
+ * Goal (required): In one sentence or less, describe what you want the agent to do in this step.
66
+ * Hint (optional): Provide additional information to help Gemini understand and navigate your app.
67
+ * Success Criteria (optional): Your success criteria should be phrased as an observation, such as 'The screen shows a
68
+ success message' or 'The checkout page is visible'.
69
+
70
+ The developer has optionally specified the following description for their test:
71
+ * ${testDescription}
72
+
73
+ Sometimes, test descriptions that developers provide tend to be too vague and lack the necessary details for the
74
+ app testing agent to be able to reliably re-run the tests with consistent results. Test cases should follow these
75
+ guidelines to ensure that they are structured in a way to make the agent more reliable.
76
+
77
+ * Prefer multiple steps with smaller, detailed goals. Broader, more open-ended goals can lead to unreliable tests
78
+ since the app testing agent can more easily veer of course. It should only take a few actions to accomplish a goal.
79
+ For example, if a step has a list in it, it should probably be broken up into multiple steps. Steps do not need
80
+ to be too small though. The test case should provide a good balance between strict guidance and flexibility. As a
81
+ rule of thumb, each step should require between 2-5 actions.
82
+ * Include a hint and success criteria whenever possible. Specifically, try to always include a success criteria to help
83
+ the agent determine when the goal has been completed.
84
+ * Avoid functionality that the app testing agent struggles with. The app testing agent struggles with the following:
85
+ * Journeys that require specific timing (like observing that something should be visible for a certain number of
86
+ seconds), interacting with moving or transient elements, etc.
87
+ * Playing games or generally interacting with drawn visuals that would require pixel input
88
+ * Complex swipe interactions, multi-finger gestures, etc., which aren't supported
89
+
90
+ First, analyze the code to get an understanding of how the app works. Get all the available screens in the app and the
91
+ different actions for each screen. Understand what functionality is and isn't available to the app testing agent.
92
+ Only include specific details in the test case if you are certain they will be available to the agent, otherwise the
93
+ agent will likely fail if it tries to follow specific guidance that doesn't work (e.g. click the 'Play' button but the
94
+ button isn't visible to the app testing agent). Do not include Android resource ids in the test case. Include
95
+ explanations that prove that each step includes between 2-5 actions. Using that information as context and the guidelines
96
+ above, convert the test description provided by the user to make it easier for the agent to follow so that the tests can
97
+ be re-run reliably. If there is no test description, generate a test case that you think will be useful given the functionality
98
+ of the app. Generate an explanation on why you generated the new test case the way you did, and then generate the
99
+ new test case, which again is an array of steps where each step contains a goal, hint, and success criteria. Show this
100
+ to the user and have them confirm before moving forward.
101
+
102
+ ## Run Test
103
+
104
+ Use the \`apptesting_run_test\` tool to run an automated test with the following as input:
105
+ * The generated test case that as been confirmed by the user
106
+ * An APK. If there is no APK present, build the app to produce one. Make sure to build the variant of the app
107
+ with the same bundle ID as the Firebase app.
108
+ * The devices to test on. If the user doesn't specify any devices in the test description, you can leave this
109
+ blank and the test will run on a the default virtual device. If the user does specify a device,
110
+ Use the \`apptesting_check_status\` tool with \`getAvailableDevices\` set to true to get a list of available
111
+ devices.
112
+
113
+ Once the test has started, provide the developer a link to see the results of the test in the Firebase Console.
114
+ You should already know the value of \`appId\' and \`projectId\` from earlier (if you only know \`projectNumber\',
115
+ use the \`firebase_get_project\` tool to get \`projectId\`). \`packageName\` is the package name of the app we tested.
116
+ The \`apptesting_run_test\` tool returns a response with field \`name\` in the form
117
+ projects/{projectNumber}/apps/{appId}/releases/{releaseId}/tests/{releaseTestId}. Extract the values for \'releaseId\'
118
+ and \`releaseTestId\` and use provide a link to the results in the Firebase Console in the format:
119
+ \`https://console.firebase.google.com/u/0/project/{projectId}/apptesting/app/android:{packageName}/releases/{releaseId}/tests/{releaseTestId}\`.
120
+
121
+ You can check the status of the test using the \`apptesting_check_status\` tool with \`release_test_name\' set to
122
+ the name of the release test returned by the \`run_test\` tool.
123
+ `.trim(),
124
+ },
125
+ },
126
+ ];
127
+ });
@@ -21,6 +21,12 @@ mobile application by accessing their Firebase Crashlytics data.
21
21
 
22
22
  Active user: ${accountEmail || "<NONE>"}
23
23
 
24
+ General rules:
25
+ **ASK THE USER WHAT THEY WOULD LIKE TO DO BEFORE TAKING ACTION**
26
+ **ASK ONLY ONE QUESTION OF THE USER AT A TIME**
27
+ **MAKE SURE TO FOLLOW THE INSTRUCTIONS, ESPECIALLY WHERE THEY ASK YOU TO CHECK IN WITH THE USER**
28
+ **ADHERE TO SUGGESTED FORMATTING**
29
+
24
30
  ## Required first steps! Absolutely required! Incredibly important!
25
31
 
26
32
  1. **Make sure the user is logged in. No Crashlytics tools will work if the user is not logged in.**
@@ -30,40 +36,40 @@ Active user: ${accountEmail || "<NONE>"}
30
36
  user is logged in.
31
37
 
32
38
  2. **Get the app ID for the Firebase application.**
33
-
34
- Use the information below to help you find the developer's app ID. If you cannot find it after 2-3
35
- attempts, just ask the user for the value they want to use, providing the description of what the
36
- value looks like.
37
-
38
- * **Description:** The app ID we are looking for contains four colon (":") delimited parts: a version
39
- number (typically "1"), a project number, a platform type ("android", "ios", or "web"),
40
- and a sequence of hexadecimal characters. This can be found in the project settings in the Firebase Console
41
- or in the appropriate google services file for the application type.
42
- * For Android apps, you will typically find the app ID in a file called google-services.json under the
43
- mobilesdk_app_id key. The file is most often located in the app directory that contains the src directory.
44
- * For iOS apps, you will typically find the app ID in a property list file called GoogleService-Info.plist under the
45
- GOOGLE_APP_ID key. The plist file is most often located in the main project directory.
46
- * Sometimes developers will not check in the google services file because it is a shared or public
47
- repository. If you can't find the file, the files may be included in the .gitignore. Check again for the file
48
- removing restrictions around looking for tracked files.
49
- * Developers may have multiple google services files that map to different releases. In cases like this,
50
- developers may create different directories to hold each like alpha/google-services.json or alpha/GoogleService-Info.plist.
51
- In other cases, developers may change the suffix of the file to something like google-services-alpha.json or
52
- GoogleService-Alpha.plist. Look for as many google services files as you can find.
53
- * Sometimes developers may include the codebase for both the Android app and the iOS app in the same repository.
54
- * If there are multiple files or multiple app IDs in a single file, ask the user to choose one by providing
55
- a numbered list of all the package names.
56
- * Again, if you have trouble finding the app ID, just ask the user for it.
39
+ a. **PRIORITIZE REMEMBERED APP ID ENTRIES** If an entry for this directory exists in the remembered app ids, use the remembered app id
40
+ for this directory without presenting any additional options.
41
+ i. If there are multiple remembered app ids for this directory, ask the user to choose one by providing
42
+ a numbered list of all the package names. Tell them that these values came from memories and how they can modify those values.
43
+ b. **IF THERE IS NO REMEMBERED ENTRY FOR THIS DIRECTORY** Use the app IDs from the \`firebase_get_environment\` tool.
44
+ i. If you've already called this tool, use the previous response from context.
45
+ ii. If the 'Detected App IDs' is set to <NONE>, ask the user for the value they want to use.
46
+ iii. If there are multiple 'Detected App IDs', ask the user to choose one by providing
47
+ a numbered list of all the package names and app ids.
48
+ c. **IF THERE IS A REMEMBERED VALUE BUT IT DOES NOT MATCH ANY DETECTED APP IDS** Ask if the user would like to replace the value with one of
49
+ the detected values.
50
+ i. **Description:** A valid app ID to remember contains four colon (":") delimited parts: a version
51
+ number (typically "1"), a project number, a platform type ("android", "ios", or "web"),
52
+ and a sequence of hexadecimal characters.
53
+ ii. Replace the value for this directory with this valid app id, the android package name or ios bundle identifier, and the project directory.
54
+ c. **IF THERE IS NO REMEMBERED ENTRY FOR THIS DIRECTORY** Ask if the user would like to remember the app id selection
55
+ i. **Description:** A valid app ID to remember contains four colon (":") delimited parts: a version
56
+ number (typically "1"), a project number, a platform type ("android", "ios", or "web"),
57
+ and a sequence of hexadecimal characters.
58
+ ii. Store the valid app id value, the android package name or ios bundle identifier, and the project directory.
57
59
 
58
60
  ## Next steps
59
61
 
60
- Once you have confirmed that the user is logged in to Firebase, and confirmed the
61
- id for the application that they want to access, then you can ask the user what actions
62
- they would like to perform. Here are some possibilities and instructions follow below:
62
+ Once you have confirmed that the user is logged in to Firebase, confirmed the
63
+ id for the application that they want to access, and asked if they want to remember the app id for this directory,
64
+ ask the user what actions they would like to perform.
65
+
66
+ Use the following format to ask the user what actions they would like to perform:
63
67
 
64
68
  1. Prioritize the most impactful stability issues
65
69
  2. Diagnose and propose a fix for a crash
66
70
 
71
+ Wait for their response before taking action.
72
+
67
73
  ## Instructions for Using Crashlytics Data
68
74
 
69
75
  ### How to prioritize issues
@@ -4,6 +4,7 @@ exports.markdownDocsOfPrompts = exports.availablePrompts = void 0;
4
4
  const core_1 = require("./core");
5
5
  const dataconnect_1 = require("./dataconnect");
6
6
  const crashlytics_1 = require("./crashlytics");
7
+ const apptesting_1 = require("./apptesting");
7
8
  const prompts = {
8
9
  core: namespacePrompts(core_1.corePrompts, "core"),
9
10
  firestore: [],
@@ -14,6 +15,7 @@ const prompts = {
14
15
  functions: [],
15
16
  remoteconfig: [],
16
17
  crashlytics: namespacePrompts(crashlytics_1.crashlyticsPrompts, "crashlytics"),
18
+ apptesting: namespacePrompts(apptesting_1.apptestingPrompts, "apptesting"),
17
19
  apphosting: [],
18
20
  database: [],
19
21
  };
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.apptestingTools = void 0;
4
+ const experiments_1 = require("../../../experiments");
5
+ const tests_1 = require("./tests");
6
+ exports.apptestingTools = [];
7
+ if ((0, experiments_1.isEnabled)("mcpalpha")) {
8
+ exports.apptestingTools.push(...[tests_1.run_tests, tests_1.check_status]);
9
+ }
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.check_status = exports.run_tests = void 0;
4
+ const zod_1 = require("zod");
5
+ const filters_1 = require("../../../crashlytics/filters");
6
+ const distribution_1 = require("../../../appdistribution/distribution");
7
+ const tool_1 = require("../../tool");
8
+ const util_1 = require("../../util");
9
+ const options_parser_util_1 = require("../../../appdistribution/options-parser-util");
10
+ const client_1 = require("../../../appdistribution/client");
11
+ const googleapis_1 = require("googleapis");
12
+ const apiv2_1 = require("../../../apiv2");
13
+ const TestDeviceSchema = zod_1.z
14
+ .object({
15
+ model: zod_1.z.string(),
16
+ version: zod_1.z.string(),
17
+ locale: zod_1.z.string(),
18
+ orientation: zod_1.z.string(),
19
+ })
20
+ .describe(`Device to run automated test on. Can run 'gcloud firebase test android|ios models list' to see available devices.`);
21
+ const AIStepSchema = zod_1.z
22
+ .object({
23
+ goal: zod_1.z.string().describe("A goal to be accomplished during the test."),
24
+ hint: zod_1.z
25
+ .string()
26
+ .optional()
27
+ .describe("Hint text containing suggestions to help the agent accomplish the goal."),
28
+ successCriteria: zod_1.z
29
+ .string()
30
+ .optional()
31
+ .describe("A description of criteria the agent should use to determine if the goal has been successfully completed."),
32
+ })
33
+ .describe("Step within a test case; run during the execution of the test.");
34
+ const defaultDevices = [
35
+ {
36
+ model: "MediumPhone.arm",
37
+ version: "30",
38
+ locale: "en_US",
39
+ orientation: "portrait",
40
+ },
41
+ ];
42
+ exports.run_tests = (0, tool_1.tool)("apptesting", {
43
+ name: "run_test",
44
+ description: `Run a remote test.`,
45
+ inputSchema: zod_1.z.object({
46
+ appId: filters_1.ApplicationIdSchema,
47
+ releaseBinaryFile: zod_1.z.string().describe("Path to the binary release (APK)."),
48
+ testDevices: zod_1.z.array(TestDeviceSchema).default(defaultDevices),
49
+ testCase: zod_1.z.object({
50
+ steps: zod_1.z
51
+ .array(AIStepSchema)
52
+ .describe("Test case containing the steps that are run during its execution."),
53
+ }),
54
+ }),
55
+ annotations: {
56
+ title: "Run a Remote Test",
57
+ readOnlyHint: false,
58
+ },
59
+ }, async ({ appId, releaseBinaryFile, testDevices, testCase }) => {
60
+ const devices = testDevices || defaultDevices;
61
+ const client = new client_1.AppDistributionClient();
62
+ const releaseName = await (0, distribution_1.upload)(client, (0, options_parser_util_1.toAppName)(appId), new distribution_1.Distribution(releaseBinaryFile));
63
+ return (0, util_1.toContent)(await client.createReleaseTest(releaseName, devices, testCase));
64
+ });
65
+ exports.check_status = (0, tool_1.tool)("apptesting", {
66
+ name: "check_status",
67
+ description: "Check the status of an apptesting release test and/or get available devices that can be used for automated tests ",
68
+ inputSchema: zod_1.z.object({
69
+ release_test_name: zod_1.z
70
+ .string()
71
+ .optional()
72
+ .describe("The name of the release test returned by the run_test tool. If set, the tool will fetch the release test"),
73
+ getAvailableDevices: zod_1.z
74
+ .boolean()
75
+ .optional()
76
+ .describe("If set to true, the tool will get the available devices that can be used for automated tests using the app testing agent"),
77
+ }),
78
+ annotations: {
79
+ title: "Check Remote Test",
80
+ readOnlyHint: true,
81
+ },
82
+ }, async ({ release_test_name, getAvailableDevices }) => {
83
+ let devices = undefined;
84
+ let releaseTest = undefined;
85
+ if (release_test_name) {
86
+ const client = new client_1.AppDistributionClient();
87
+ releaseTest = await client.getReleaseTest(release_test_name);
88
+ }
89
+ if (getAvailableDevices) {
90
+ const testing = googleapis_1.google.testing("v1");
91
+ devices = await testing.testEnvironmentCatalog.get({
92
+ oauth_token: await (0, apiv2_1.getAccessToken)(),
93
+ environmentType: "ANDROID",
94
+ });
95
+ }
96
+ return (0, util_1.toContent)({
97
+ devices,
98
+ releaseTest,
99
+ });
100
+ });
@@ -10,8 +10,9 @@ const index_6 = require("./messaging/index");
10
10
  const index_7 = require("./remoteconfig/index");
11
11
  const index_8 = require("./crashlytics/index");
12
12
  const index_9 = require("./apphosting/index");
13
- const index_10 = require("./realtime_database/index");
14
- const index_11 = require("./functions/index");
13
+ const index_10 = require("./apptesting/index");
14
+ const index_11 = require("./realtime_database/index");
15
+ const index_12 = require("./functions/index");
15
16
  async function availableTools(ctx, activeFeatures) {
16
17
  const allTools = getAllTools(activeFeatures);
17
18
  const availabilities = await Promise.all(allTools.map((t) => {
@@ -43,11 +44,12 @@ const tools = {
43
44
  dataconnect: addFeaturePrefix("dataconnect", index_2.dataconnectTools),
44
45
  storage: addFeaturePrefix("storage", index_5.storageTools),
45
46
  messaging: addFeaturePrefix("messaging", index_6.messagingTools),
46
- functions: addFeaturePrefix("functions", index_11.functionsTools),
47
+ functions: addFeaturePrefix("functions", index_12.functionsTools),
47
48
  remoteconfig: addFeaturePrefix("remoteconfig", index_7.remoteConfigTools),
48
49
  crashlytics: addFeaturePrefix("crashlytics", index_8.crashlyticsTools),
50
+ apptesting: addFeaturePrefix("apptesting", index_10.apptestingTools),
49
51
  apphosting: addFeaturePrefix("apphosting", index_9.appHostingTools),
50
- database: addFeaturePrefix("realtimedatabase", index_10.realtimeDatabaseTools),
52
+ database: addFeaturePrefix("realtimedatabase", index_11.realtimeDatabaseTools),
51
53
  };
52
54
  function addFeaturePrefix(feature, tools) {
53
55
  return tools.map((tool) => (Object.assign(Object.assign({}, tool), { mcp: Object.assign(Object.assign({}, tool.mcp), { name: `${feature}_${tool.mcp.name}`, _meta: Object.assign(Object.assign({}, tool.mcp._meta), { feature }) }) })));
package/lib/mcp/types.js CHANGED
@@ -11,6 +11,7 @@ exports.SERVER_FEATURES = [
11
11
  "functions",
12
12
  "remoteconfig",
13
13
  "crashlytics",
14
+ "apptesting",
14
15
  "apphosting",
15
16
  "database",
16
17
  ];
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isAppTestingAvailable = void 0;
4
+ const api_1 = require("../../../api");
5
+ const appUtils_1 = require("../../../appUtils");
6
+ const ensureApiEnabled_1 = require("../../../ensureApiEnabled");
7
+ const timeout_1 = require("../../../timeout");
8
+ async function isAppTestingAvailable(ctx) {
9
+ const host = ctx.host;
10
+ const projectDir = ctx.config.projectDir;
11
+ const platforms = await (0, appUtils_1.getPlatformsFromFolder)(projectDir);
12
+ const supportedPlatforms = [appUtils_1.Platform.FLUTTER, appUtils_1.Platform.ANDROID, appUtils_1.Platform.IOS];
13
+ if (!platforms.some((p) => supportedPlatforms.includes(p))) {
14
+ host.log("debug", `Found no supported App Testing platforms.`);
15
+ return false;
16
+ }
17
+ try {
18
+ return await (0, timeout_1.timeoutFallback)((0, ensureApiEnabled_1.check)(ctx.projectId, (0, api_1.appDistributionOrigin)(), "", true), true, 3000);
19
+ }
20
+ catch (e) {
21
+ return true;
22
+ }
23
+ }
24
+ exports.isAppTestingAvailable = isAppTestingAvailable;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getDefaultFeatureAvailabilityCheck = void 0;
4
4
  const util_1 = require("../util");
5
5
  const availability_1 = require("./crashlytics/availability");
6
+ const availability_2 = require("./apptesting/availability");
6
7
  const DEFAULT_AVAILABILITY_CHECKS = {
7
8
  core: async (ctx) => true,
8
9
  firestore: (ctx) => (0, util_1.checkFeatureActive)("firestore", ctx.projectId, { config: ctx.config }),
@@ -14,6 +15,7 @@ const DEFAULT_AVAILABILITY_CHECKS = {
14
15
  remoteconfig: (ctx) => (0, util_1.checkFeatureActive)("remoteconfig", ctx.projectId, { config: ctx.config }),
15
16
  crashlytics: availability_1.isCrashlyticsAvailable,
16
17
  apphosting: (ctx) => (0, util_1.checkFeatureActive)("apphosting", ctx.projectId, { config: ctx.config }),
18
+ apptesting: availability_2.isAppTestingAvailable,
17
19
  database: (ctx) => (0, util_1.checkFeatureActive)("database", ctx.projectId, { config: ctx.config }),
18
20
  };
19
21
  function getDefaultFeatureAvailabilityCheck(feature) {
package/lib/mcp/util.js CHANGED
@@ -49,6 +49,7 @@ const SERVER_FEATURE_APIS = {
49
49
  functions: (0, api_1.functionsOrigin)(),
50
50
  remoteconfig: (0, api_1.remoteConfigApiOrigin)(),
51
51
  crashlytics: (0, api_1.crashlyticsApiOrigin)(),
52
+ apptesting: (0, api_1.appDistributionOrigin)(),
52
53
  apphosting: (0, api_1.apphostingOrigin)(),
53
54
  database: (0, api_1.realtimeOrigin)(),
54
55
  };
@@ -62,6 +63,7 @@ const DETECTED_API_FEATURES = {
62
63
  functions: undefined,
63
64
  remoteconfig: undefined,
64
65
  crashlytics: undefined,
66
+ apptesting: undefined,
65
67
  apphosting: undefined,
66
68
  database: undefined,
67
69
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firebase-tools",
3
- "version": "14.24.0",
3
+ "version": "14.24.1",
4
4
  "description": "Command-Line Interface for Firebase",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {