Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
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
28 changes: 3 additions & 25 deletions src/tools/appautomate-utils/appium-sdk/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
AppSDKSupportedLanguageEnum,
AppSDKSupportedPlatformEnum,
} from "./index.js";
import { MobileDeviceSchema } from "../../sdk-utils/common/schema.js";

// App Automate specific device configurations
export const APP_DEVICE_CONFIGS = {
Expand Down Expand Up @@ -50,34 +51,11 @@ export const SETUP_APP_AUTOMATE_SCHEMA = {
),

devices: z
.array(
z.union([
// Android: [android, deviceName, osVersion]
z.tuple([
z
.literal(AppSDKSupportedPlatformEnum.android)
.describe("Platform identifier: 'android'"),
z
.string()
.describe(
"Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'",
),
z.string().describe("Android version, e.g. '14', '16', 'latest'"),
]),
// iOS: [ios, deviceName, osVersion]
z.tuple([
z
.literal(AppSDKSupportedPlatformEnum.ios)
.describe("Platform identifier: 'ios'"),
z.string().describe("Device name, e.g. 'iPhone 15', 'iPhone 14 Pro'"),
z.string().describe("iOS version, e.g. '17', '16', 'latest'"),
]),
]),
)
.array(MobileDeviceSchema)
.max(3)
.default([])
.describe(
"Tuples describing target mobile devices. Add device only when user asks explicitly for it. Defaults to [] . Example: [['android', 'Samsung Galaxy S24', '14'], ['ios', 'iPhone 15', '17']]",
"Mobile device objects array. Use the object format directly - no transformation needed. Add only when user explicitly requests devices. Examples: [{ platform: 'android', deviceName: 'Samsung Galaxy S24', osVersion: '14' }] or [{ platform: 'ios', deviceName: 'iPhone 15', osVersion: '17' }].",
),

appPath: z
Expand Down
13 changes: 8 additions & 5 deletions src/tools/appautomate-utils/appium-sdk/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { z } from "zod";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { BrowserStackConfig } from "../../../lib/types.js";
import { getBrowserStackAuth } from "../../../lib/get-auth.js";
import { validateAppAutomateDevices } from "../../sdk-utils/common/device-validator.js";
import {
validateAppAutomateDevices,
convertMobileDevicesToTuples,
} from "../../sdk-utils/common/device-validator.js";

import {
getAppUploadInstruction,
Expand Down Expand Up @@ -38,18 +41,18 @@ export async function setupAppAutomateHandler(
const testingFramework =
input.detectedTestingFramework as AppSDKSupportedTestingFramework;
const language = input.detectedLanguage as AppSDKSupportedLanguage;
const inputDevices = (input.devices as Array<Array<string>>) ?? [];
const appPath = input.appPath as string;
const framework = input.detectedFramework as SupportedFramework;

//Validating if supported framework or not
validateSupportforAppAutomate(framework, language, testingFramework);

// Use default mobile devices when array is empty
const devices =
// Convert device objects to tuples for validator
const inputDevices = input.devices || [];
const devices: Array<Array<string>> =
inputDevices.length === 0
? [["android", "Samsung Galaxy S24", "latest"]]
: inputDevices;
: convertMobileDevicesToTuples(inputDevices);
Copy link

Copilot AI Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default device [['android', 'Samsung Galaxy S24', 'latest']] is duplicated in both appautomate.ts (line 387) and appium-sdk/handler.ts (line 54). Consider defining this as a constant (e.g., DEFAULT_MOBILE_DEVICE) in a shared location to ensure consistency and ease maintenance.

Copilot uses AI. Check for mistakes.

// Validate devices against real BrowserStack device data
const validatedEnvironments = await validateAppAutomateDevices(devices);
Expand Down
29 changes: 3 additions & 26 deletions src/tools/appautomate-utils/native-execution/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from "zod";
import { AppTestPlatform } from "./types.js";
import { AppSDKSupportedPlatformEnum } from "../appium-sdk/types.js";
import { MobileDeviceSchema } from "../../sdk-utils/common/schema.js";

export const RUN_APP_AUTOMATE_DESCRIPTION = `Execute pre-built native mobile test suites (Espresso for Android, XCUITest for iOS) by direct upload to BrowserStack. ONLY for compiled .apk/.ipa test files. This is NOT for SDK integration or Appium tests. For Appium-based testing with SDK setup, use 'setupBrowserStackAppAutomateTests' instead.`;

Expand Down Expand Up @@ -30,34 +30,11 @@ export const RUN_APP_AUTOMATE_SCHEMA = {
"If in other directory, provide existing test file path",
),
devices: z
.array(
z.union([
// Android: [android, deviceName, osVersion]
z.tuple([
z
.literal(AppSDKSupportedPlatformEnum.android)
.describe("Platform identifier: 'android'"),
z
.string()
.describe(
"Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'",
),
z.string().describe("Android version, e.g. '14', '16', 'latest'"),
]),
// iOS: [ios, deviceName, osVersion]
z.tuple([
z
.literal(AppSDKSupportedPlatformEnum.ios)
.describe("Platform identifier: 'ios'"),
z.string().describe("Device name, e.g. 'iPhone 15', 'iPhone 14 Pro'"),
z.string().describe("iOS version, e.g. '17', '16', 'latest'"),
]),
]),
)
.array(MobileDeviceSchema)
.max(3)
.default([])
.describe(
"Tuples describing target mobile devices. Add device only when user asks explicitly for it. Defaults to [] . Example: [['android', 'Samsung Galaxy S24', '14'], ['ios', 'iPhone 15', '17']]",
"Mobile device objects array. Use the object format directly - no transformation needed. Add only when user explicitly requests devices. Examples: [{ platform: 'android', deviceName: 'Samsung Galaxy S24', osVersion: '14' }] or [{ platform: 'ios', deviceName: 'iPhone 15', osVersion: '17' }].",
),
project: z
.string()
Expand Down
15 changes: 13 additions & 2 deletions src/tools/appautomate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import { maybeCompressBase64 } from "../lib/utils.js";
import { remote } from "webdriverio";
import { AppTestPlatform } from "./appautomate-utils/native-execution/types.js";
import { setupAppAutomateHandler } from "./appautomate-utils/appium-sdk/handler.js";
import { validateAppAutomateDevices } from "./sdk-utils/common/device-validator.js";
import {
validateAppAutomateDevices,
convertMobileDevicesToTuples,
} from "./sdk-utils/common/device-validator.js";

import {
SETUP_APP_AUTOMATE_DESCRIPTION,
Expand Down Expand Up @@ -378,7 +381,15 @@ export default function addAppAutomationTools(
undefined,
config,
);
return await runAppTestsOnBrowserStack(args, config);
// Convert device objects to tuples for the handler
const devices: Array<Array<string>> =
(args.devices || []).length === 0
? [["android", "Samsung Galaxy S24", "latest"]]
: convertMobileDevicesToTuples(args.devices || []);
Copy link

Copilot AI Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition (args.devices || []).length === 0 checks the array length after defaulting to empty array, then uses args.devices || [] again in the conversion. This pattern is redundant. Simplify to: const devices: Array<Array<string>> = args.devices.length === 0 ? [['android', 'Samsung Galaxy S24', 'latest']] : convertMobileDevicesToTuples(args.devices); since the schema has .default([]) which ensures args.devices is never undefined.

Suggested change
(args.devices || []).length === 0
? [["android", "Samsung Galaxy S24", "latest"]]
: convertMobileDevicesToTuples(args.devices || []);
args.devices.length === 0
? [["android", "Samsung Galaxy S24", "latest"]]
: convertMobileDevicesToTuples(args.devices);

Copilot uses AI. Check for mistakes.
return await runAppTestsOnBrowserStack(
{ ...args, devices },
config,
);
} catch (error) {
trackMCP(
"runAppTestsOnBrowserStack",
Expand Down
41 changes: 36 additions & 5 deletions src/tools/sdk-utils/bstack/sdkHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,44 @@ export async function runBstackSDKOnly(
const authString = getBrowserStackAuth(config);
const [username, accessKey] = authString.split(":");

// Validate devices against real BrowserStack device data
const tupleTargets = (input as any).devices as
| Array<Array<string>>
| undefined;
// Convert device objects to tuples for validator
const devices = input.devices || [];
const tupleTargets: Array<Array<string>> = devices.map((device) => {
if (device.platform === "windows") {
return [
"windows",
device.osVersion,
device.browser,
device.browserVersion || "latest",
];
} else if (device.platform === "macos") {
return [
"macos",
device.osVersion,
device.browser,
device.browserVersion || "latest",
];
} else if (device.platform === "android") {
return [
"android",
device.deviceName,
device.osVersion,
device.browser || "chrome",
];
} else if (device.platform === "ios") {
return [
"ios",
device.deviceName,
device.osVersion,
device.browser || "safari",
];
} else {
throw new Error(`Unsupported platform: ${device.platform}`);
}
});

const validatedEnvironments = await validateDevices(
tupleTargets || [],
tupleTargets,
input.detectedBrowserAutomationFramework,
);

Expand Down
17 changes: 17 additions & 0 deletions src/tools/sdk-utils/common/device-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,23 @@ export async function validateAppAutomateDevices(
// SHARED UTILITY FUNCTIONS
// ============================================================================

/**
* Convert mobile device objects to tuples for validators
* @param devices Array of device objects with platform, deviceName, osVersion
* @returns Array of tuples [platform, deviceName, osVersion]
*/
export function convertMobileDevicesToTuples(
devices: Array<{ platform: string; deviceName: string; osVersion: string }>,
): Array<Array<string>> {
return devices.map((device) => {
if (device.platform === "android" || device.platform === "ios") {
return [device.platform, device.deviceName, device.osVersion];
} else {
throw new Error(`Unsupported platform: ${device.platform}`);
}
});
}

// Exact browser validation (preferred for structured fields)
function validateBrowserExact(
requestedBrowser: string,
Expand Down
109 changes: 61 additions & 48 deletions src/tools/sdk-utils/common/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,67 +48,80 @@ export const SetUpPercyParamsShape = {
),
};

// Shared mobile device schema for App Automate (no browser field)
export const MobileDeviceSchema = z.discriminatedUnion("platform", [
z.object({
platform: z.literal("android"),
deviceName: z.string().describe("Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'"),
osVersion: z.string().describe("Android version, e.g. '14', '16', 'latest'"),
}),
z.object({
platform: z.literal("ios"),
deviceName: z.string().describe("Device name, e.g. 'iPhone 15', 'iPhone 14 Pro'"),
osVersion: z.string().describe("iOS version, e.g. '17', '16', 'latest'"),
}),
]);

const DeviceSchema = z.discriminatedUnion("platform", [
z.object({
platform: z.literal("windows"),
osVersion: z.string().describe("Windows version, e.g. '10', '11'"),
browser: z.string().describe("Browser name, e.g. 'chrome', 'firefox', 'edge'"),
browserVersion: z
.string()
.optional()
.describe("Browser version, e.g. '132', 'latest', 'oldest'"),
}),
z.object({
platform: z.literal("android"),
deviceName: z
.string()
.describe("Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'"),
osVersion: z.string().describe("Android version, e.g. '14', '16', 'latest'"),
browser: z
.string()
.optional()
.describe("Browser name, e.g. 'chrome', 'samsung browser'"),
}),
z.object({
platform: z.literal("ios"),
deviceName: z
.string()
.describe("Device name, e.g. 'iPhone 15', 'iPhone 14 Pro'"),
osVersion: z.string().describe("iOS version, e.g. '17', 'latest'"),
browser: z
.string()
.optional()
.describe("Browser name, typically 'safari'"),
}),
z.object({
platform: z.literal("macos"),
osVersion: z.string().describe("macOS version name, e.g. 'Sequoia', 'Ventura'"),
browser: z.string().describe("Browser name, e.g. 'safari', 'chrome'"),
browserVersion: z
.string()
.optional()
.describe("Browser version, e.g. 'latest'"),
}),
]);

export const RunTestsOnBrowserStackParamsShape = {
projectName: z
.string()
.describe("A single name for your project to organize all your tests."),
detectedLanguage: z.nativeEnum(SDKSupportedLanguageEnum),
detectedBrowserAutomationFramework: z.nativeEnum(
SDKSupportedBrowserAutomationFrameworkEnum,
),
detectedBrowserAutomationFramework: z.nativeEnum(SDKSupportedBrowserAutomationFrameworkEnum),
detectedTestingFramework: z.nativeEnum(SDKSupportedTestingFrameworkEnum),
devices: z
.array(
z.union([
// Windows: [windows, osVersion, browser, browserVersion]
z.tuple([
z
.nativeEnum(WindowsPlatformEnum)
.describe("Platform identifier: 'windows'"),
z.string().describe("Windows version, e.g. '10', '11'"),
z.string().describe("Browser name, e.g. 'chrome', 'firefox', 'edge'"),
z
.string()
.describe("Browser version, e.g. '132', 'latest', 'oldest'"),
]),
// Android: [android, name, model, osVersion, browser]
z.tuple([
z
.literal(PlatformEnum.ANDROID)
.describe("Platform identifier: 'android'"),
z
.string()
.describe(
"Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'",
),
z.string().describe("Android version, e.g. '14', '16', 'latest'"),
z.string().describe("Browser name, e.g. 'chrome', 'samsung browser'"),
]),
// iOS: [ios, name, model, osVersion, browser]
z.tuple([
z.literal(PlatformEnum.IOS).describe("Platform identifier: 'ios'"),
z.string().describe("Device name, e.g. 'iPhone 12 Pro'"),
z.string().describe("iOS version, e.g. '17', 'latest'"),
z.string().describe("Browser name, typically 'safari'"),
]),
// macOS: [mac|macos, name, model, browser, browserVersion]
z.tuple([
z
.nativeEnum(MacOSPlatformEnum)
.describe("Platform identifier: 'mac' or 'macos'"),
z.string().describe("macOS version name, e.g. 'Sequoia', 'Ventura'"),
z.string().describe("Browser name, e.g. 'safari', 'chrome'"),
z.string().describe("Browser version, e.g. 'latest'"),
]),
]),
)
.array(DeviceSchema)
.max(3)
.default([])
.describe(
"Preferred tuples of target devices.Add device only when user asks explicitly for it. Defaults to [] . Example: [['windows', '11', 'chrome', 'latest']]",
"Device objects array. Use the object format directly - no transformation needed. Add only when user explicitly requests devices. Examples: [{ platform: 'windows', osVersion: '11', browser: 'chrome', browserVersion: 'latest' }] or [{ platform: 'android', deviceName: 'Samsung Galaxy S24', osVersion: '14', browser: 'chrome' }].",
),
};


Copy link

Copilot AI Nov 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra blank line detected. Remove one of the consecutive empty lines for consistent code formatting.

Suggested change

Copilot uses AI. Check for mistakes.
export const SetUpPercySchema = z.object(SetUpPercyParamsShape);

export const RunTestsOnBrowserStackSchema = z.object(
Expand Down
Loading