firebase-tools 14.24.0 → 14.24.2
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/api.js +3 -1
- package/lib/appUtils.js +0 -2
- package/lib/appdistribution/client.js +2 -1
- package/lib/appdistribution/distribution.js +85 -2
- package/lib/appdistribution/options-parser-util.js +8 -5
- package/lib/commands/appdistribution-distribute.js +12 -85
- package/lib/gcp/apptesting.js +18 -0
- package/lib/mcp/prompts/apptesting/index.js +9 -0
- package/lib/mcp/prompts/apptesting/run_test.js +127 -0
- package/lib/mcp/prompts/crashlytics/connect.js +33 -27
- package/lib/mcp/prompts/index.js +2 -0
- package/lib/mcp/tools/apptesting/index.js +9 -0
- package/lib/mcp/tools/apptesting/tests.js +99 -0
- package/lib/mcp/tools/index.js +6 -4
- package/lib/mcp/types.js +1 -0
- package/lib/mcp/util/apptesting/availability.js +24 -0
- package/lib/mcp/util/availability.js +2 -0
- package/lib/mcp/util.js +2 -0
- package/package.json +1 -1
package/lib/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.storageOrigin = exports.runtimeconfigOrigin = exports.rulesOrigin = exports.resourceManagerOrigin = exports.crashlyticsApiOrigin = exports.messagingApiOrigin = exports.remoteConfigApiOrigin = exports.rtdbMetadataOrigin = exports.rtdbManagementOrigin = exports.realtimeOrigin = exports.extensionsTOSOrigin = exports.extensionsPublisherOrigin = exports.extensionsOrigin = exports.iamOrigin = exports.identityOrigin = exports.hostingOrigin = exports.googleOrigin = exports.pubsubOrigin = exports.cloudTasksOrigin = exports.cloudschedulerOrigin = exports.cloudbuildOrigin = exports.functionsDefaultRegion = exports.runOrigin = exports.functionsV2Origin = exports.functionsOrigin = exports.firestoreOrigin = exports.firestoreOriginOrEmulator = exports.firedataOrigin = exports.firebaseExtensionsRegistryOrigin = exports.firebaseApiOrigin = exports.eventarcOrigin = exports.dynamicLinksKey = exports.dynamicLinksOrigin = exports.consoleOrigin = exports.authManagementOrigin = exports.authOrigin = exports.apphostingGitHubAppInstallationURL = exports.apphostingP4SADomain = exports.apphostingOrigin = exports.appDistributionOrigin = exports.artifactRegistryDomain = exports.developerConnectP4SADomain = exports.developerConnectOrigin = exports.containerRegistryDomain = exports.cloudMonitoringOrigin = exports.cloudloggingOrigin = exports.cloudbillingOrigin = exports.clientSecret = exports.clientId = exports.authProxyOrigin = void 0;
|
|
4
|
-
exports.setScopes = exports.getScopes = exports.appTestingOrigin = exports.cloudAiCompanionOrigin = exports.vertexAIOrigin = exports.cloudSQLAdminOrigin = exports.dataConnectLocalConnString = exports.dataconnectP4SADomain = exports.dataconnectOrigin = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = exports.studioApiOrigin = exports.serviceUsageOrigin = exports.cloudRunApiOrigin = exports.hostingApiOrigin = exports.firebaseStorageOrigin = void 0;
|
|
4
|
+
exports.setScopes = exports.getScopes = exports.cloudTestingOrigin = exports.appTestingOrigin = exports.cloudAiCompanionOrigin = exports.vertexAIOrigin = exports.cloudSQLAdminOrigin = exports.dataConnectLocalConnString = exports.dataconnectP4SADomain = exports.dataconnectOrigin = exports.githubClientSecret = exports.githubClientId = exports.computeOrigin = exports.secretManagerOrigin = exports.githubApiOrigin = exports.githubOrigin = exports.studioApiOrigin = exports.serviceUsageOrigin = exports.cloudRunApiOrigin = exports.hostingApiOrigin = exports.firebaseStorageOrigin = void 0;
|
|
5
5
|
const constants_1 = require("./emulator/constants");
|
|
6
6
|
const logger_1 = require("./logger");
|
|
7
7
|
const scopes = require("./scopes");
|
|
@@ -148,6 +148,8 @@ const cloudAiCompanionOrigin = () => utils.envOverride("CLOUD_AI_COMPANION_URL",
|
|
|
148
148
|
exports.cloudAiCompanionOrigin = cloudAiCompanionOrigin;
|
|
149
149
|
const appTestingOrigin = () => utils.envOverride("FIREBASE_APP_TESTING_URL", "https://firebaseapptesting.googleapis.com");
|
|
150
150
|
exports.appTestingOrigin = appTestingOrigin;
|
|
151
|
+
const cloudTestingOrigin = () => utils.envOverride("CLOUD_TESTING_URL", "https://testing.googleapis.com");
|
|
152
|
+
exports.cloudTestingOrigin = cloudTestingOrigin;
|
|
151
153
|
function getScopes() {
|
|
152
154
|
return Array.from(commandScopes);
|
|
153
155
|
}
|
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
|
-
|
|
49
|
-
return `projects/${appId.split(":")[1]}/apps/${appId}`;
|
|
48
|
+
return toAppName(options.app);
|
|
50
49
|
}
|
|
51
50
|
exports.getAppName = getAppName;
|
|
52
|
-
function
|
|
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
|
|
14
|
-
const
|
|
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 ${
|
|
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
|
-
|
|
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 (!
|
|
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,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.testEnvironmentCatalog = exports.client = exports.API_VERSION = void 0;
|
|
4
|
+
const apiv2_1 = require("../apiv2");
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
exports.API_VERSION = "v1";
|
|
7
|
+
exports.client = new apiv2_1.Client({
|
|
8
|
+
urlPrefix: (0, api_1.cloudTestingOrigin)(),
|
|
9
|
+
auth: true,
|
|
10
|
+
apiVersion: exports.API_VERSION,
|
|
11
|
+
});
|
|
12
|
+
async function testEnvironmentCatalog(projectId, environmentType) {
|
|
13
|
+
const name = `testEnvironmentCatalog/${environmentType}`;
|
|
14
|
+
const queryParams = { projectId };
|
|
15
|
+
const res = await exports.client.get(name, { queryParams });
|
|
16
|
+
return res.body;
|
|
17
|
+
}
|
|
18
|
+
exports.testEnvironmentCatalog = testEnvironmentCatalog;
|
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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,
|
|
61
|
-
id for the application that they want to access,
|
|
62
|
-
they would like to perform.
|
|
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
|
package/lib/mcp/prompts/index.js
CHANGED
|
@@ -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,99 @@
|
|
|
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 apptesting_1 = require("../../../gcp/apptesting");
|
|
12
|
+
const TestDeviceSchema = zod_1.z
|
|
13
|
+
.object({
|
|
14
|
+
model: zod_1.z.string(),
|
|
15
|
+
version: zod_1.z.string(),
|
|
16
|
+
locale: zod_1.z.string(),
|
|
17
|
+
orientation: zod_1.z.string(),
|
|
18
|
+
})
|
|
19
|
+
.describe(`Device to run automated test on. Can run 'gcloud firebase test android|ios models list' to see available devices.`);
|
|
20
|
+
const AIStepSchema = zod_1.z
|
|
21
|
+
.object({
|
|
22
|
+
goal: zod_1.z.string().describe("A goal to be accomplished during the test."),
|
|
23
|
+
hint: zod_1.z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Hint text containing suggestions to help the agent accomplish the goal."),
|
|
27
|
+
successCriteria: zod_1.z
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("A description of criteria the agent should use to determine if the goal has been successfully completed."),
|
|
31
|
+
})
|
|
32
|
+
.describe("Step within a test case; run during the execution of the test.");
|
|
33
|
+
const defaultDevices = [
|
|
34
|
+
{
|
|
35
|
+
model: "MediumPhone.arm",
|
|
36
|
+
version: "30",
|
|
37
|
+
locale: "en_US",
|
|
38
|
+
orientation: "portrait",
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
exports.run_tests = (0, tool_1.tool)("apptesting", {
|
|
42
|
+
name: "run_test",
|
|
43
|
+
description: `Run a remote test.`,
|
|
44
|
+
inputSchema: zod_1.z.object({
|
|
45
|
+
appId: filters_1.ApplicationIdSchema,
|
|
46
|
+
releaseBinaryFile: zod_1.z.string().describe("Path to the binary release (APK)."),
|
|
47
|
+
testDevices: zod_1.z.array(TestDeviceSchema).default(defaultDevices),
|
|
48
|
+
testCase: zod_1.z.object({
|
|
49
|
+
steps: zod_1.z
|
|
50
|
+
.array(AIStepSchema)
|
|
51
|
+
.describe("Test case containing the steps that are run during its execution."),
|
|
52
|
+
}),
|
|
53
|
+
}),
|
|
54
|
+
annotations: {
|
|
55
|
+
title: "Run a Remote Test",
|
|
56
|
+
readOnlyHint: false,
|
|
57
|
+
},
|
|
58
|
+
}, async ({ appId, releaseBinaryFile, testDevices, testCase }) => {
|
|
59
|
+
const devices = testDevices || defaultDevices;
|
|
60
|
+
const client = new client_1.AppDistributionClient();
|
|
61
|
+
const releaseName = await (0, distribution_1.upload)(client, (0, options_parser_util_1.toAppName)(appId), new distribution_1.Distribution(releaseBinaryFile));
|
|
62
|
+
return (0, util_1.toContent)(await client.createReleaseTest(releaseName, devices, testCase));
|
|
63
|
+
});
|
|
64
|
+
exports.check_status = (0, tool_1.tool)("apptesting", {
|
|
65
|
+
name: "check_status",
|
|
66
|
+
description: "Check the status of an apptesting release test and/or get available devices that can be used for automated tests ",
|
|
67
|
+
inputSchema: zod_1.z.object({
|
|
68
|
+
release_test_name: zod_1.z
|
|
69
|
+
.string()
|
|
70
|
+
.optional()
|
|
71
|
+
.describe("The name of the release test returned by the run_test tool. If set, the tool will fetch the release test"),
|
|
72
|
+
getAvailableDevices: zod_1.z
|
|
73
|
+
.boolean()
|
|
74
|
+
.optional()
|
|
75
|
+
.describe("If set to true, the tool will get the available devices that can be used for automated tests using the app testing agent"),
|
|
76
|
+
}),
|
|
77
|
+
annotations: {
|
|
78
|
+
title: "Check Remote Test",
|
|
79
|
+
readOnlyHint: true,
|
|
80
|
+
},
|
|
81
|
+
_meta: {
|
|
82
|
+
requiresAuth: true,
|
|
83
|
+
requiresProject: true,
|
|
84
|
+
},
|
|
85
|
+
}, async ({ release_test_name, getAvailableDevices }, { projectId }) => {
|
|
86
|
+
let devices = undefined;
|
|
87
|
+
let releaseTest = undefined;
|
|
88
|
+
if (release_test_name) {
|
|
89
|
+
const client = new client_1.AppDistributionClient();
|
|
90
|
+
releaseTest = await client.getReleaseTest(release_test_name);
|
|
91
|
+
}
|
|
92
|
+
if (getAvailableDevices) {
|
|
93
|
+
devices = await (0, apptesting_1.testEnvironmentCatalog)(projectId || "", "ANDROID");
|
|
94
|
+
}
|
|
95
|
+
return (0, util_1.toContent)({
|
|
96
|
+
devices,
|
|
97
|
+
releaseTest,
|
|
98
|
+
});
|
|
99
|
+
});
|
package/lib/mcp/tools/index.js
CHANGED
|
@@ -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("./
|
|
14
|
-
const index_11 = require("./
|
|
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",
|
|
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",
|
|
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
|
@@ -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
|
};
|