Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"chai": "^6.2.2",
"eslint": "^10.5.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-unicorn": "^78.0.0",
"eslint-plugin-unicorn": "^79.0.0",
"husky": "^9.1.7",
"mocha": "^12.0.0",
"prettier": "^3.8.4",
Expand Down Expand Up @@ -96,7 +96,8 @@
"minimatch@<3.1.4": "3.1.4",
"minimatch@>=5.0.0 <5.1.8": "5.1.8",
"minimatch@>=9.0.0 <9.0.7": "9.0.7",
"minimatch@>=10.0.0 <10.2.3": "10.2.3",
"minimatch@>=10.0.0 <11.0.1": "11.0.1",
"glob@>=9.0.0 <14.0.2": "14.0.2",
"picomatch@<2.3.2": "2.3.2",
"picomatch@>=4.0.0 <4.0.4": "4.0.4",
"serialize-javascript": ">=7.0.5",
Expand All @@ -108,7 +109,7 @@
"brace-expansion@>=3.0.0 <5.0.12": "5.0.12",
"ws@>=8.0.0 <8.21.0": "8.21.0",
"esbuild@<0.28.1": ">=0.28.1",
"qs@<6.16.0": "6.16.0",
"qs@<6.16.1": "6.16.1",
"fast-uri@>=3.0.0 <3.1.9": "3.1.9",
"hono@>=4.0.0 <4.13.11": "4.13.11",
"source-map-js@<1.2.2": "1.2.2",
Expand Down
392 changes: 191 additions & 201 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

12 changes: 7 additions & 5 deletions src/commands/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,11 +490,6 @@ export const cloudCommand = defineCommand({
throw new CliError('You must provide a flow file');
}

deviceValidationService.validateiOSDevice(iOSVersion, iOSDevice, compatibilityData, {
debug,
logger: (m: string) => out(m),
});

// Render DB-driven notices (deprecation/warn/info/marketing) the API
// returned with the compatibility data. Replaces the previously hardcoded
// iOS-16 deprecation warning — that is now a seeded notice gated on the
Expand Down Expand Up @@ -529,6 +524,13 @@ export const cloudCommand = defineCommand({
learnMoreUrl: n.learnMoreUrl,
}));

// Validated after the notices, so a notice aimed at a removed device or
// OS version (e.g. removed-ios-17-2026-11-02) is shown before the
// rejection it explains.
deviceValidationService.validateiOSDevice(iOSVersion, iOSDevice, compatibilityData, {
debug,
logger: (m: string) => out(m),
});
deviceValidationService.validateAndroidDevice(
androidApiLevel,
androidDevice,
Expand Down
4 changes: 2 additions & 2 deletions src/mcp/tools/run-cloud-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ export function registerRunCloudTest(server: McpServer): void {
.string()
.optional()
.describe('ID of a previously uploaded binary. Mutually exclusive with appFile.'),
iosVersion: z.string().optional().describe('iOS version, e.g. "17"'),
iosDevice: z.string().optional().describe('iOS device, e.g. "iphone-14"'),
iosVersion: z.string().optional().describe('iOS version, e.g. "26"'),
iosDevice: z.string().optional().describe('iOS device, e.g. "iphone-16"'),
androidApiLevel: z.string().optional().describe('Android API level, e.g. "34"'),
androidDevice: z.string().optional().describe('Android device, e.g. "pixel-10"'),
googlePlay: z.boolean().optional().describe('Use a Google Play-enabled Android image'),
Expand Down
4 changes: 2 additions & 2 deletions src/services/device-validation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ export class DeviceValidationService {
`[DEBUG] Supported iOS versions: ${supportedVersions.join(', ')}`,
],
// Mirrors the API's global defaults, as for Android above.
defaultDevice: 'iphone-14',
defaultVersion: '17',
defaultDevice: 'iphone-16',
defaultVersion: '26',
device: iOSDevice,
deviceArg: argNames.iOSDevice,
deviceKind: 'iOS',
Expand Down
4 changes: 1 addition & 3 deletions src/types/domain/device.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,17 @@
*/

export enum EiOSDevices {
'ipad-pro-6th-gen' = 'ipad-pro-6th-gen',
'ipad-pro-m5-11' = 'ipad-pro-m5-11',
'ipad-pro-m5-13' = 'ipad-pro-m5-13',
'iphone-14' = 'iphone-14',
'iphone-15' = 'iphone-15',
'iphone-16' = 'iphone-16',
'iphone-16-plus' = 'iphone-16-plus',
'iphone-16-pro' = 'iphone-16-pro',
'iphone-16-pro-max' = 'iphone-16-pro-max',
'iphone-17' = 'iphone-17',
'iphone-18-pro' = 'iphone-18-pro',
'iphone-18-pro-max' = 'iphone-18-pro-max',
'iphone-air' = 'iphone-air',
'iphone-duo' = 'iphone-duo',
}

export enum EAndroidDevices {
Expand Down
8 changes: 4 additions & 4 deletions test/integration/cloud.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ appId: com.example.app

describe('device management path', () => {
it('should accept valid iOS device configuration', async () => {
const command = `${CLI} cloud ${iosAppFile} ${testFlowFile} --api-key ${mockApiKey} --api-url ${mockApiUrl} --ios-device iphone-14 --ios-version 17 --async --json`;
const command = `${CLI} cloud ${iosAppFile} ${testFlowFile} --api-key ${mockApiKey} --api-url ${mockApiUrl} --ios-device iphone-14 --ios-version 18 --async --json`;

const { stdout } = await exec(command, { timeout: 30_000 });
expectAsyncRunJson(stdout);
Expand Down Expand Up @@ -175,18 +175,18 @@ appId: com.example.app
});

it('asks for the version when a lone device needs a non-default one', async () => {
const command = `${CLI} cloud ${iosAppFile} ${testFlowFile} --api-key ${mockApiKey} --api-url ${mockApiUrl} --ios-device iphone-17 --dry-run`;
const command = `${CLI} cloud ${iosAppFile} ${testFlowFile} --api-key ${mockApiKey} --api-url ${mockApiUrl} --ios-device iphone-14 --dry-run`;

const { output } = await runExpectingFailure(command);
expect(output).to.include('iphone-17 only supports these iOS versions');
expect(output).to.include('iphone-14 only supports these iOS versions');
expect(output).to.include('--ios-version');
});
});

// #1105 device matrix: repeated --ios-device-matrix / --android-device-matrix cells.
describe('device matrix', () => {
it('accepts a repeated --ios-device-matrix matrix and still yields one upload', async () => {
const command = `${CLI} cloud ${iosAppFile} ${testFlowFile} --api-key ${mockApiKey} --api-url ${mockApiUrl} --ios-device-matrix iphone-16:18 --ios-device-matrix iphone-16-pro:26 --async --json`;
const command = `${CLI} cloud ${iosAppFile} ${testFlowFile} --api-key ${mockApiKey} --api-url ${mockApiUrl} --ios-device-matrix iphone-16:18 --ios-device-matrix iphone-17:26 --async --json`;

const { stdout } = await exec(command, { timeout: 30_000 });
// Still one upload with one uploadId — the matrix is a property of the
Expand Down
4 changes: 2 additions & 2 deletions test/unit/device-matrix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ describe('parseDeviceMatrix', () => {

it('parses each --ios-device-matrix as exactly one cell (no cross-product)', () => {
const matrix = parseDeviceMatrix(
['iphone-16:18', 'iphone-16-pro:26'],
['iphone-16:18', 'iphone-17:26'],
[],
);
expect(matrix).to.deep.equal([
{ iOSDevice: 'iphone-16', iOSVersion: '18' },
{ iOSDevice: 'iphone-16-pro', iOSVersion: '26' },
{ iOSDevice: 'iphone-17', iOSVersion: '26' },
]);
expect(matrixIsIos(matrix)).to.equal(true);
});
Expand Down
31 changes: 16 additions & 15 deletions test/unit/device-validation.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { CompatibilityData } from '../../src/utils/compatibility.js';
const compat: CompatibilityData = {
ios: {
'iphone-14': ['17', '18'],
'iphone-16': ['18', '26', '27'],
'iphone-17': ['26', '27'],
'iphone-19': ['28'],
// A rollout gate can leave a device with nothing bookable.
Expand Down Expand Up @@ -65,14 +66,14 @@ describe('DeviceValidationService', () => {

describe('unknown devices', () => {
it('rejects an unknown iOS device, listing what the API offers in its order', () => {
expect(() => ios('17', 'iphone-99')).to.throw(
'iOS device "iphone-99" is not supported. Supported iOS devices: iphone-14, iphone-17, iphone-19. See https://docs.devicecloud.dev/getting-started/devices-configuration',
expect(() => ios('26', 'iphone-99')).to.throw(
'iOS device "iphone-99" is not supported. Supported iOS devices: iphone-14, iphone-16, iphone-17, iphone-19. See https://docs.devicecloud.dev/getting-started/devices-configuration',
);
});

it('never offers a gated device, and treats it as unsupported', () => {
expect(() => ios('17', 'iphone-gated')).to.throw(
/iOS device "iphone-gated" is not supported\. Supported iOS devices: iphone-14, iphone-17, iphone-19\./,
expect(() => ios('26', 'iphone-gated')).to.throw(
/iOS device "iphone-gated" is not supported\. Supported iOS devices: iphone-14, iphone-16, iphone-17, iphone-19\./,
);
});

Expand All @@ -95,14 +96,14 @@ describe('DeviceValidationService', () => {
});

it('does not resolve a prototype member as a device', () => {
expect(() => ios('17', 'constructor')).to.throw(
expect(() => ios('26', 'constructor')).to.throw(
/iOS device "constructor" is not supported/,
);
});

it('still fails when the API sent no list at all, without an empty list', () => {
const noIos = { ...compat, ios: {} };
expect(() => ios('17', 'iphone-14', noIos)).to.throw(
expect(() => ios('18', 'iphone-14', noIos)).to.throw(
'iOS device "iphone-14" is not supported. See https://docs.devicecloud.dev/getting-started/devices-configuration',
);
});
Expand All @@ -129,13 +130,13 @@ describe('DeviceValidationService', () => {
});
});

// The API fills an omitted half from the same global defaults (iphone-14 /
// iOS 17, pixel-10 / API 36) and rejects an incompatible pair, so the CLI
// The API fills an omitted half from the same global defaults (iphone-16 /
// iOS 26, pixel-10 / API 36) and rejects an incompatible pair, so the CLI
// mirrors that — but must tell the user to pass the other half.
describe('requests that rely on a default', () => {
it('tells a device-only iOS request to pass the version explicitly', () => {
expect(() => ios(undefined, 'iphone-17')).to.throw(
'iphone-17 only supports these iOS versions: 26, 27. No iOS version was given, so the default (17) was checked: pass one of those explicitly with --ios-version',
expect(() => ios(undefined, 'iphone-14')).to.throw(
'iphone-14 only supports these iOS versions: 17, 18. No iOS version was given, so the default (26) was checked: pass one of those explicitly with --ios-version',
);
});

Expand All @@ -146,14 +147,14 @@ describe('DeviceValidationService', () => {
});

it('accepts a device-only request when the default version fits', () => {
expect(() => ios(undefined, 'iphone-14')).to.not.throw();
expect(() => ios(undefined, 'iphone-17')).to.not.throw();
expect(() => android(undefined, 'pixel-7')).to.not.throw();
expect(() => android(undefined, 'pixel-10')).to.not.throw();
});

it('names the MCP tool parameters when asked to', () => {
expect(() =>
service.validateiOSDevice(undefined, 'iphone-17', compat, {
service.validateiOSDevice(undefined, 'iphone-14', compat, {
argNames: MCP_ARG_NAMES,
}),
).to.throw(/pass one of those explicitly with iosVersion$/);
Expand All @@ -165,14 +166,14 @@ describe('DeviceValidationService', () => {
});

it('points a version-only request at the devices that offer that version', () => {
expect(() => ios('27')).to.throw(
'iphone-14 only supports these iOS versions: 17, 18. No iOS device was given, so the default (iphone-14) was checked: pass one that offers iOS 27 with --ios-device: iphone-17',
expect(() => ios('28')).to.throw(
'iphone-16 only supports these iOS versions: 18, 26, 27. No iOS device was given, so the default (iphone-16) was checked: pass one that offers iOS 28 with --ios-device: iphone-19',
);
});

it('says when no device offers the requested version at all', () => {
expect(() => ios('99')).to.throw(
/No iOS device was given, so the default \(iphone-14\) was checked: no iOS device offers iOS 99$/,
/No iOS device was given, so the default \(iphone-16\) was checked: no iOS device offers iOS 99$/,
);
// The default device (pixel-10) has Play images, as it does in the API.
const playDefault = {
Expand Down
Loading