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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 === "mac" || 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}`);
}
});
Comment on lines 27 to 60
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 device conversion logic contains repetitive code for handling different platforms. The fallback values (browserVersion || "latest", browser || "chrome", browser || "safari") should be documented or centralized. Consider extracting these defaults into constants to ensure consistency across the codebase and make maintenance easier.

Copilot uses AI. Check for mistakes.

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

Expand Down
32 changes: 29 additions & 3 deletions src/tools/sdk-utils/common/device-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,19 @@ export async function validateDevices(
}

// Determine what data we need to fetch
const needsDesktop = devices.some((env) =>
// Normalize "mac" to "macos" for consistency
const normalizedDevices = devices.map((env) => {
const platform = (env[0] || "").toLowerCase();
if (platform === "mac") {
return ["macos", ...env.slice(1)];
}
return env;
});
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 "mac" to "macos" normalization is performed here for tuple-based validation. However, the object-based schema in DeviceSchema (line 85 of schema.ts) accepts both "mac" and "macos" via z.enum(["mac", "macos"]). This means the conversion from objects to tuples in sdkHandler.ts should also handle "mac" consistently. The current implementation does handle this (line 35), but this normalization logic creates redundancy. Consider documenting this behavior or refactoring to have a single source of truth for platform normalization.

Copilot uses AI. Check for mistakes.

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.

Trailing whitespace detected. Remove the trailing whitespace on this empty line for cleaner code formatting.

Suggested change

Copilot uses AI. Check for mistakes.
const needsDesktop = normalizedDevices.some((env) =>
["windows", "macos"].includes((env[0] || "").toLowerCase()),
);
const needsMobile = devices.some((env) =>
const needsMobile = normalizedDevices.some((env) =>
["android", "ios"].includes((env[0] || "").toLowerCase()),
);

Expand Down Expand Up @@ -275,7 +284,7 @@ export async function validateDevices(
iosIndex = createMobileIndex(iosEntries);
}

for (const env of devices) {
for (const env of normalizedDevices) {
const discriminator = (env[0] || "").toLowerCase();
let validatedEnv: ValidatedEnvironment;

Expand Down Expand Up @@ -621,6 +630,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
93 changes: 45 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,64 @@ 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().describe("Browser version, e.g. '132', 'latest', 'oldest'"),
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 browserVersion field is required in the Windows and macOS schemas but is treated as optional in the conversion logic (line 33, 40). This inconsistency could cause runtime errors when devices are validated against the schema. Consider making browserVersion optional in the schema using .optional() to match the conversion behavior, or ensure it's always provided in the conversion.

Copilot uses AI. Check for mistakes.
}),
z.object({
platform: z.literal("android"),
deviceName: z.string().describe("Device name, e.g. 'Samsung Galaxy S24'"),
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.

[nitpick] The device name description in the Android schema for DeviceSchema differs from the MobileDeviceSchema description (line 55). For consistency, consider using the same description: "Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'" to match the MobileDeviceSchema, which provides more examples.

Suggested change
deviceName: z.string().describe("Device name, e.g. 'Samsung Galaxy S24'"),
deviceName: z.string().describe("Device name, e.g. 'Samsung Galaxy S24', 'Google Pixel 8'"),

Copilot uses AI. Check for mistakes.
osVersion: z.string().describe("Android version, e.g. '14', '16', 'latest'"),
browser: z.string().describe("Browser name, e.g. 'chrome', 'samsung browser'"),
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 browser field is required in the Android schema, but the conversion logic in sdkHandler.ts (line 47) treats it as optional with a default value (device.browser || "chrome"). This inconsistency could cause runtime errors when devices without a browser field are validated. Consider making browser optional in the schema using .optional() to match the conversion behavior.

Suggested change
browser: z.string().describe("Browser name, e.g. 'chrome', 'samsung browser'"),
browser: z.string().describe("Browser name, e.g. 'chrome', 'samsung browser'").optional(),

Copilot uses AI. Check for mistakes.
}),
z.object({
platform: z.literal("ios"),
deviceName: z.string().describe("Device name, e.g. 'iPhone 12 Pro'"),
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.

[nitpick] The iOS device name example in DeviceSchema uses 'iPhone 12 Pro', while the MobileDeviceSchema uses 'iPhone 15', 'iPhone 14 Pro' (line 60). For consistency and to reflect more current devices, consider updating this to match the examples in MobileDeviceSchema.

Suggested change
deviceName: z.string().describe("Device name, e.g. 'iPhone 12 Pro'"),
deviceName: z.string().describe("Device name, e.g. 'iPhone 15', 'iPhone 14 Pro'"),

Copilot uses AI. Check for mistakes.
osVersion: z.string().describe("iOS version, e.g. '17', 'latest'"),
browser: z.string().describe("Browser name, typically 'safari'"),
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 browser field is required in the iOS schema, but the conversion logic in sdkHandler.ts (line 54) treats it as optional with a default value (device.browser || "safari"). This inconsistency could cause runtime errors when devices without a browser field are validated. Consider making browser optional in the schema using .optional() to match the conversion behavior.

Suggested change
browser: z.string().describe("Browser name, typically 'safari'"),
browser: z.string().describe("Browser name, typically 'safari'").optional(),

Copilot uses AI. Check for mistakes.
}),
z.object({
platform: z.enum(["mac", "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().describe("Browser version, e.g. 'latest'"),
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 browserVersion field is required in the macOS schema but is treated as optional in the conversion logic (line 40 in sdkHandler.ts uses device.browserVersion || "latest"). This inconsistency could cause runtime errors when devices are validated against the schema. Consider making browserVersion optional in the schema using .optional() to match the conversion behavior.

Suggested change
browserVersion: z.string().describe("Browser version, e.g. 'latest'"),
browserVersion: z.string().describe("Browser version, e.g. 'latest'").optional(),

Copilot uses AI. Check for mistakes.
}),
]);

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