Skip to content

Commit a1558c9

Browse files
committed
feat: add web platform vocabulary
1 parent f9a9662 commit a1558c9

12 files changed

Lines changed: 147 additions & 20 deletions

File tree

src/__tests__/test-utils/device-fixtures.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ export const LINUX_DEVICE: DeviceInfo = {
4141
target: 'desktop',
4242
};
4343

44+
export const WEB_DESKTOP_DEVICE: DeviceInfo = {
45+
platform: 'web',
46+
id: 'agent-browser-chrome',
47+
name: 'Agent Browser Chrome',
48+
kind: 'device',
49+
target: 'desktop',
50+
booted: true,
51+
};
52+
4453
export const ANDROID_TV_DEVICE: DeviceInfo = {
4554
platform: 'android',
4655
id: 'and-tv-1',

src/__tests__/test-utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export {
66
LINUX_DEVICE,
77
MACOS_DEVICE,
88
TVOS_SIMULATOR,
9+
WEB_DESKTOP_DEVICE,
910
} from './device-fixtures.ts';
1011

1112
export {

src/client-normalizers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,8 @@ export function normalizeOpenDevice(
183183
(platform !== 'ios' &&
184184
platform !== 'macos' &&
185185
platform !== 'android' &&
186-
platform !== 'linux') ||
186+
platform !== 'linux' &&
187+
platform !== 'web') ||
187188
!id ||
188189
!name
189190
) {

src/commands/__tests__/command-surface-metadata.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import assert from 'node:assert/strict';
22
import { test } from 'vitest';
33
import { listMcpExposedCommandNames } from '../../command-catalog.ts';
4-
import { listCommandMetadataNames, listMcpCommandMetadata } from '../command-metadata.ts';
4+
import {
5+
listCommandMetadata,
6+
listCommandMetadataNames,
7+
listMcpCommandMetadata,
8+
} from '../command-metadata.ts';
59
import { listExecutableCommandNames } from '../command-surface.ts';
610

711
test('MCP exposed command names have metadata and executable command definitions', () => {
@@ -23,3 +27,13 @@ test('MCP exposed command names have metadata and executable command definitions
2327
test('CI-only prepare command stays out of MCP tool surface', () => {
2428
assert.equal(listMcpExposedCommandNames().includes('prepare'), false);
2529
});
30+
31+
test('common command input accepts web platform selector', () => {
32+
const snapshotMetadata = listCommandMetadata().find((metadata) => metadata.name === 'snapshot');
33+
if (!snapshotMetadata) throw new Error('Expected snapshot command metadata');
34+
35+
const platformSchema = snapshotMetadata.inputSchema.properties?.platform;
36+
const input = snapshotMetadata.readInput({ platform: 'web' }) as { platform?: unknown };
37+
assert.deepEqual(platformSchema?.enum, ['ios', 'macos', 'android', 'linux', 'web', 'apple']);
38+
assert.equal(input.platform, 'web');
39+
});

src/core/__tests__/capabilities.test.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { test } from 'vitest';
22
import assert from 'node:assert/strict';
33
import { isCommandSupportedOnDevice, unsupportedHintForDevice } from '../capabilities.ts';
4-
import type { DeviceInfo } from '../../utils/device.ts';
4+
import { matchesPlatformSelector, type DeviceInfo } from '../../utils/device.ts';
5+
import { WEB_DESKTOP_DEVICE } from '../../__tests__/test-utils/index.ts';
56

67
const iosSimulator: DeviceInfo = {
78
platform: 'ios',
@@ -47,6 +48,8 @@ const linuxDevice: DeviceInfo = {
4748
target: 'desktop',
4849
};
4950

51+
const webDevice = WEB_DESKTOP_DEVICE;
52+
5053
const tvOsSimulator: DeviceInfo = {
5154
platform: 'ios',
5255
id: 'tv-sim-1',
@@ -384,6 +387,64 @@ test('Linux supports desktop interaction commands and blocks mobile/unsupported
384387
);
385388
});
386389

390+
test('web supports only the initial browser interaction slice', () => {
391+
assertCommandSupport(
392+
[
393+
'click',
394+
'close',
395+
'fill',
396+
'find',
397+
'get',
398+
'is',
399+
'open',
400+
'press',
401+
'screenshot',
402+
'scroll',
403+
'snapshot',
404+
'type',
405+
'wait',
406+
],
407+
[{ device: webDevice, expected: true, label: 'on web' }],
408+
);
409+
assertCommandSupport(
410+
[
411+
'alert',
412+
'app-switcher',
413+
'apps',
414+
'back',
415+
'boot',
416+
'clipboard',
417+
'diff',
418+
'fling',
419+
'focus',
420+
'home',
421+
'install',
422+
'install-from-source',
423+
'keyboard',
424+
'logs',
425+
'longpress',
426+
'network',
427+
'pan',
428+
'perf',
429+
'pinch',
430+
'push',
431+
'record',
432+
'reinstall',
433+
'rotate',
434+
'settings',
435+
'shutdown',
436+
'swipe',
437+
'trigger-app-event',
438+
],
439+
[{ device: webDevice, expected: false, label: 'on web' }],
440+
);
441+
});
442+
443+
test('apple selector does not match web platform', () => {
444+
assert.equal(matchesPlatformSelector(webDevice.platform, 'apple'), false);
445+
assert.equal(matchesPlatformSelector(webDevice.platform, 'web'), true);
446+
});
447+
387448
test('unknown commands default to supported', () => {
388449
assert.equal(isCommandSupportedOnDevice('some-future-cmd', iosSimulator), true);
389450
assert.equal(isCommandSupportedOnDevice('some-future-cmd', androidDevice), true);

src/core/capabilities.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type CommandCapability = {
1111
apple?: KindMatrix;
1212
android?: KindMatrix;
1313
linux?: KindMatrix;
14+
web?: KindMatrix;
1415
supports?: (device: DeviceInfo) => boolean;
1516
/** Optional actionable hint surfaced when this command is rejected at admission for `device`. */
1617
unsupportedHint?: (device: DeviceInfo) => string | undefined;
@@ -39,12 +40,17 @@ const synthesisGestureUnsupportedHint = (device: DeviceInfo): string | undefined
3940
// Linux device kind is always 'device' (local desktop).
4041
const LINUX_DEVICE: KindMatrix = { device: true };
4142
const LINUX_NONE: KindMatrix = {};
43+
const WEB_DEVICE: KindMatrix = { device: true };
4244
const ALL_DEVICE_COMMAND_CAPABILITY = {
4345
apple: { simulator: true, device: true },
4446
android: { emulator: true, device: true, unknown: true },
4547
linux: LINUX_DEVICE,
4648
} as const satisfies CommandCapability;
47-
const APP_RUNTIME_CAPABILITY = ALL_DEVICE_COMMAND_CAPABILITY;
49+
const WEB_COMMAND_CAPABILITY = {
50+
...ALL_DEVICE_COMMAND_CAPABILITY,
51+
web: WEB_DEVICE,
52+
} as const satisfies CommandCapability;
53+
const APP_RUNTIME_CAPABILITY = WEB_COMMAND_CAPABILITY;
4854
const APP_INVENTORY_CAPABILITY = {
4955
apple: { simulator: true, device: true },
5056
android: { emulator: true, device: true, unknown: true },
@@ -124,6 +130,7 @@ const COMMAND_CAPABILITY_MATRIX: Record<string, CommandCapability> = {
124130
apple: { simulator: true, device: true },
125131
android: { emulator: true, device: true, unknown: true },
126132
linux: LINUX_DEVICE,
133+
web: WEB_DEVICE,
127134
},
128135
clipboard: {
129136
apple: { simulator: true, device: true },
@@ -147,19 +154,20 @@ const COMMAND_CAPABILITY_MATRIX: Record<string, CommandCapability> = {
147154
apple: { simulator: true, device: true },
148155
android: { emulator: true, device: true, unknown: true },
149156
linux: LINUX_DEVICE,
157+
web: WEB_DEVICE,
150158
},
151159
fling: {
152160
apple: { simulator: true, device: true },
153161
android: { emulator: true, device: true, unknown: true },
154162
linux: LINUX_NONE,
155163
},
156-
snapshot: ALL_DEVICE_COMMAND_CAPABILITY,
164+
snapshot: WEB_COMMAND_CAPABILITY,
157165
diff: ALL_DEVICE_COMMAND_CAPABILITY,
158-
screenshot: ALL_DEVICE_COMMAND_CAPABILITY,
159-
wait: ALL_DEVICE_COMMAND_CAPABILITY,
160-
get: ALL_DEVICE_COMMAND_CAPABILITY,
161-
find: ALL_DEVICE_COMMAND_CAPABILITY,
162-
is: ALL_DEVICE_COMMAND_CAPABILITY,
166+
screenshot: WEB_COMMAND_CAPABILITY,
167+
wait: WEB_COMMAND_CAPABILITY,
168+
get: WEB_COMMAND_CAPABILITY,
169+
find: WEB_COMMAND_CAPABILITY,
170+
is: WEB_COMMAND_CAPABILITY,
163171
focus: {
164172
apple: { simulator: true, device: true },
165173
android: { emulator: true, device: true, unknown: true },
@@ -200,6 +208,7 @@ const COMMAND_CAPABILITY_MATRIX: Record<string, CommandCapability> = {
200208
apple: { simulator: true, device: true },
201209
android: { emulator: true, device: true, unknown: true },
202210
linux: LINUX_DEVICE,
211+
web: WEB_DEVICE,
203212
},
204213
push: {
205214
apple: { simulator: true },
@@ -228,6 +237,7 @@ const COMMAND_CAPABILITY_MATRIX: Record<string, CommandCapability> = {
228237
apple: { simulator: true, device: true },
229238
android: { emulator: true, device: true, unknown: true },
230239
linux: LINUX_DEVICE,
240+
web: WEB_DEVICE,
231241
},
232242
swipe: {
233243
apple: { simulator: true, device: true },
@@ -246,17 +256,19 @@ const COMMAND_CAPABILITY_MATRIX: Record<string, CommandCapability> = {
246256
android: { emulator: true, device: true, unknown: true },
247257
linux: LINUX_NONE,
248258
},
249-
type: ALL_DEVICE_COMMAND_CAPABILITY,
259+
type: WEB_COMMAND_CAPABILITY,
250260
};
251261

252262
export function isCommandSupportedOnDevice(command: string, device: DeviceInfo): boolean {
253263
const capability = COMMAND_CAPABILITY_MATRIX[command];
254264
if (!capability) return true;
255265
const byPlatform = isApplePlatform(device.platform)
256266
? capability.apple
257-
: device.platform === 'linux'
258-
? capability.linux
259-
: capability.android;
267+
: device.platform === 'android'
268+
? capability.android
269+
: device.platform === 'linux'
270+
? capability.linux
271+
: capability.web;
260272
if (!byPlatform) return false;
261273
if (capability.supports && !capability.supports(device)) return false;
262274
const kind = (device.kind ?? 'unknown') as keyof KindMatrix;

src/core/platform-inventory.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,22 @@ export type DeviceInventoryRequest = {
1010
androidSerialAllowlist?: string[];
1111
};
1212

13+
const WEB_DESKTOP_DEVICE: DeviceInfo = {
14+
platform: 'web',
15+
id: 'agent-browser-chrome',
16+
name: 'Agent Browser Chrome',
17+
kind: 'device',
18+
target: 'desktop',
19+
booted: true,
20+
};
21+
1322
export async function listLocalDeviceInventory(
1423
request: DeviceInventoryRequest,
1524
): Promise<DeviceInfo[]> {
25+
if (request.platform === 'web') {
26+
return [WEB_DESKTOP_DEVICE];
27+
}
28+
1629
if (shouldUseHostMacFastPath(request)) {
1730
const { listMacosDevices } = await import('../platforms/macos/devices.ts');
1831
return await listMacosDevices();
@@ -66,6 +79,7 @@ export async function listLocalDeviceInventory(
6679
const { listLinuxDevices } = await import('../platforms/linux/devices.ts');
6780
devices.push(...(await listLinuxDevices()));
6881
} catch {}
82+
devices.push(WEB_DESKTOP_DEVICE);
6983
return devices;
7084
}
7185

src/daemon/request-lock-policy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ function targetSelectorsConflict(
190190
return target === 'desktop';
191191
case 'macos':
192192
case 'linux':
193+
case 'web':
193194
return target !== 'desktop';
194195
case 'apple':
195196
return false;
@@ -225,8 +226,8 @@ function freshSessionSelectorKeysForPlatform(
225226
? ['udid', 'serial', 'androidDeviceAllowlist', 'iosSimulatorDeviceSet']
226227
: ['serial', 'androidDeviceAllowlist'];
227228
case 'macos':
228-
return ['udid', 'serial', 'iosSimulatorDeviceSet', 'androidDeviceAllowlist'];
229229
case 'linux':
230+
case 'web':
230231
return ['udid', 'serial', 'iosSimulatorDeviceSet', 'androidDeviceAllowlist'];
231232
default:
232233
return assertNever(lockPlatform);

src/utils/__tests__/args.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,16 @@ test('parseArgs recognizes command-specific flag combinations', async () => {
102102
assert.equal(parsed.flags.target, 'tv');
103103
},
104104
},
105+
{
106+
label: 'open --platform web',
107+
argv: ['open', 'https://example.com', '--platform', 'web', '--target', 'desktop'],
108+
strictFlags: true,
109+
assertParsed: (parsed) => {
110+
assert.equal(parsed.command, 'open');
111+
assert.equal(parsed.flags.platform, 'web');
112+
assert.equal(parsed.flags.target, 'desktop');
113+
},
114+
},
105115
{
106116
label: 'open --surface frontmost-app',
107117
argv: ['open', '--platform', 'macos', '--surface', 'frontmost-app'],
@@ -1636,7 +1646,7 @@ test('command usage shows command and global flags separately', () => {
16361646
assert.match(help, /Command flags:/);
16371647
assert.match(help, /--pattern one-way\|ping-pong/);
16381648
assert.match(help, /Global flags:/);
1639-
assert.match(help, /--platform ios\|macos\|android\|linux\|apple/);
1649+
assert.match(help, /--platform ios\|macos\|android\|linux\|web\|apple/);
16401650
});
16411651

16421652
test('back command usage documents explicit mode flags', () => {

src/utils/cli-flags.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,8 +322,8 @@ const FLAG_DEFINITIONS: readonly FlagDefinition[] = [
322322
key: 'platform',
323323
names: ['--platform'],
324324
type: 'enum',
325-
enumValues: ['ios', 'macos', 'android', 'linux', 'apple'],
326-
usageLabel: '--platform ios|macos|android|linux|apple',
325+
enumValues: ['ios', 'macos', 'android', 'linux', 'web', 'apple'],
326+
usageLabel: '--platform ios|macos|android|linux|web|apple',
327327
usageDescription: 'Platform to target (`apple` aliases the Apple automation backend)',
328328
},
329329
{

0 commit comments

Comments
 (0)