Skip to content

Commit 948a718

Browse files
committed
fix: tighten Vega platform boundaries
1 parent 670570d commit 948a718

27 files changed

Lines changed: 378 additions & 365 deletions

‎src/__tests__/client-normalizers.test.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ test('normalizeSession carries the additive appleOs discriminant on the session
9696
assert.equal(session.device.platform, 'ios');
9797
});
9898

99-
test('normalizeOpenDevice preserves per-platform identifier shaping', () => {
99+
test('normalizeOpenDevice preserves iOS identifier shaping', () => {
100100
const ios = normalizeOpenDevice({
101101
platform: 'ios',
102102
id: 'udid-1',
@@ -105,7 +105,11 @@ test('normalizeOpenDevice preserves per-platform identifier shaping', () => {
105105
});
106106
assert.deepEqual(ios?.ios, { udid: 'udid-1', simulatorSetPath: '/tmp/set' });
107107
assert.equal(ios?.android, undefined);
108+
assert.equal('android' in (ios ?? {}), false);
109+
assert.equal('vega' in (ios ?? {}), false);
110+
});
108111

112+
test('normalizeOpenDevice preserves Android identifier shaping', () => {
109113
const android = normalizeOpenDevice({
110114
platform: 'android',
111115
id: 'serial-1',
@@ -114,7 +118,12 @@ test('normalizeOpenDevice preserves per-platform identifier shaping', () => {
114118
});
115119
assert.deepEqual(android?.android, { serial: 'explicit-serial' });
116120
assert.equal(android?.ios, undefined);
121+
assert.equal(android?.identifiers.serial, 'explicit-serial');
122+
assert.equal('ios' in (android ?? {}), false);
123+
assert.equal('vega' in (android ?? {}), false);
124+
});
117125

126+
test('normalizeOpenDevice preserves Vega identifier shaping', () => {
118127
const vega = normalizeOpenDevice({
119128
platform: 'vega',
120129
id: 'VirtualDevice',
@@ -124,4 +133,7 @@ test('normalizeOpenDevice preserves per-platform identifier shaping', () => {
124133
assert.deepEqual(vega?.vega, { serial: 'explicit-vega-serial' });
125134
assert.equal(vega?.ios, undefined);
126135
assert.equal(vega?.android, undefined);
136+
assert.equal(vega?.identifiers.serial, 'explicit-vega-serial');
137+
assert.equal('ios' in (vega ?? {}), false);
138+
assert.equal('android' in (vega ?? {}), false);
127139
});

‎src/cli/parser/__tests__/cli-help-topics.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,9 @@ test('usageForCommand resolves tv help topic', async () => {
338338
assert.match(help, /agent-device devices --platform vega --target tv/);
339339
assert.match(help, /Vega OS uses the exact hold duration through inputd-cli/);
340340
assert.match(help, /Use --platform vega --target tv/);
341-
assert.match(help, /Initial Vega OS support is VVD-only/);
342-
assert.match(help, /Physical Fire TV.*report unsupported/);
341+
assert.match(help, /Initial support is VVD-only/);
342+
assert.match(help, /Physical Fire TV devices remain unsupported/);
343+
assert.equal(help.match(/\nVega OS:/g)?.length, 1);
343344
});
344345

345346
test('usageForCommand resolves web help topic', async () => {

‎src/cli/parser/cli-help.ts‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -451,13 +451,20 @@ Core loop:
451451
agent-device screenshot ./tv-focus.png --overlay-refs --platform android --target tv --session tv
452452
453453
Vega OS:
454+
Vega OS is driven through the SDK-matched Vega CLI and VDA, not ADB.
455+
Initial support is VVD-only. Physical Fire TV devices remain unsupported until their discovery, lifecycle, and remote controls have durable hardware evidence.
456+
Use --platform vega --target tv for the running Vega Virtual Device.
454457
vega virtual-device start
455458
agent-device devices --platform vega --target tv
456459
agent-device open <component-id> --platform vega --target tv --session vega-tv
457460
agent-device tv-remote press down --platform vega --target tv --session vega-tv
458461
agent-device tv-remote press select --platform vega --target tv --session vega-tv
459462
agent-device close <component-id> --session vega-tv
460463
vega virtual-device stop
464+
Use a component ID from the app package or Vega SDK tooling; agent-device app inventory is not yet supported.
465+
Use --serial VirtualDevice for explicit VVD selection.
466+
agent-device does not boot the VVD implicitly.
467+
Snapshot, screenshot, selectors, install, touch/text/gesture, logs, and performance commands remain unsupported until their Vega backends are implemented.
461468
462469
Buttons:
463470
tv-remote press up|down|left|right|select|menu|home|back
@@ -477,14 +484,6 @@ tvOS:
477484
back maps to the Menu remote button; home maps to the Home remote button.
478485
Use --platform ios --target tv for Apple TV simulators and devices.
479486
480-
Vega OS:
481-
Vega OS is driven through the SDK-matched Vega CLI and VDA, not ADB.
482-
Initial Vega OS support is VVD-only. Use --platform vega --target tv for a running Vega Virtual Device.
483-
Use a component ID from the app package or Vega SDK tooling; agent-device app inventory is not yet supported.
484-
Use --serial VirtualDevice for explicit VVD selection.
485-
Start and stop the local emulator with vega virtual-device start|stop; agent-device does not boot the VVD implicitly.
486-
Initial support covers VVD discovery, app open/close, back, home, and tv-remote. Physical Fire TV, app inventory, snapshot, screenshot, selectors, install, touch/text/gesture, logs, and performance commands report unsupported until their Vega backends are validated and implemented.
487-
488487
Focus and visual truth:
489488
On Android TV and tvOS, if snapshot -i exposes a focused node, verify it with is focused <selector>.
490489
Use wait focused=true only when repeated snapshots preserve focus metadata for the app.

‎src/client/client-normalizers.ts‎

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import type { DaemonRequest, SessionRuntimeHints } from '../daemon/types.ts';
22
import { AppError, type NormalizedError } from '../kernel/errors.ts';
33
import type { SnapshotNode } from '../kernel/snapshot.ts';
44
import { buildAppIdentifiers, buildDeviceIdentifiers } from '../contracts/result-serialization.ts';
5-
import { isAppleOs, isApplePlatform, isPublicPlatform, type AppleOS } from '../kernel/device.ts';
5+
import {
6+
isAppleOs,
7+
isApplePlatform,
8+
isPublicPlatform,
9+
isSerialAddressablePlatform,
10+
type AppleOS,
11+
} from '../kernel/device.ts';
612
import { leaseScopeFromOptions, leaseScopeToRequestMeta } from '../core/lease-scope.ts';
713
import type {
814
AgentDeviceDevice,
@@ -125,11 +131,9 @@ export function normalizeSession(value: unknown): AgentDeviceSession {
125131
// Additive Apple-OS discriminant; present only when the daemon emits it (Apple devices).
126132
...(appleOs ? { appleOs } : {}),
127133
identifiers,
128-
...buildClientDevicePlatformFields(
129-
platform,
130-
id,
131-
readNullableString(record, 'ios_simulator_device_set'),
132-
),
134+
...buildClientDevicePlatformFields(platform, id, {
135+
simulatorSetPath: readNullableString(record, 'ios_simulator_device_set'),
136+
}),
133137
},
134138
identifiers,
135139
};
@@ -154,19 +158,21 @@ function readClientDeviceIdentity(value: unknown, nameField: string) {
154158
function buildClientDevicePlatformFields(
155159
platform: AgentDeviceDevice['platform'],
156160
id: string,
157-
simulatorSetPath?: string | null,
161+
options: { simulatorSetPath?: string | null; serial?: string } = {},
158162
): Pick<AgentDeviceSessionDevice, 'ios' | 'android' | 'vega'> {
159-
return {
160-
ios:
161-
platform === 'ios'
162-
? {
163-
udid: id,
164-
...(simulatorSetPath !== undefined ? { simulatorSetPath } : {}),
165-
}
166-
: undefined,
167-
android: platform === 'android' ? { serial: id } : undefined,
168-
vega: platform === 'vega' ? { serial: id } : undefined,
169-
};
163+
if (platform === 'ios') {
164+
return {
165+
ios: {
166+
udid: id,
167+
...(options.simulatorSetPath !== undefined
168+
? { simulatorSetPath: options.simulatorSetPath }
169+
: {}),
170+
},
171+
};
172+
}
173+
if (!isSerialAddressablePlatform(platform)) return {};
174+
const serial = options.serial ?? id;
175+
return platform === 'android' ? { android: { serial } } : { vega: { serial } };
170176
}
171177

172178
export function normalizeRuntimeHints(value: unknown): SessionRuntimeHints | undefined {
@@ -195,23 +201,27 @@ export function normalizeOpenDevice(
195201
return undefined;
196202
}
197203
const target = readDeviceTarget(value, 'target');
198-
const identifiers = buildDeviceIdentifiers(platform, id, name);
204+
const serial = isSerialAddressablePlatform(platform)
205+
? (readOptionalString(value, 'serial') ?? id)
206+
: undefined;
207+
const identifiers = {
208+
...buildDeviceIdentifiers(platform, id, name),
209+
...(serial ? { serial } : {}),
210+
};
199211
return {
200212
platform,
201213
target,
202214
id,
203215
name,
204216
identifiers,
205-
ios:
206-
platform === 'ios'
207-
? {
208-
udid: readOptionalString(value, 'device_udid') ?? id,
209-
simulatorSetPath: readNullableString(value, 'ios_simulator_device_set'),
210-
}
211-
: undefined,
212-
android:
213-
platform === 'android' ? { serial: readOptionalString(value, 'serial') ?? id } : undefined,
214-
vega: platform === 'vega' ? { serial: readOptionalString(value, 'serial') ?? id } : undefined,
217+
...buildClientDevicePlatformFields(
218+
platform,
219+
platform === 'ios' ? (readOptionalString(value, 'device_udid') ?? id) : id,
220+
{
221+
simulatorSetPath: readNullableString(value, 'ios_simulator_device_set'),
222+
serial,
223+
},
224+
),
215225
};
216226
}
217227

‎src/contracts/result-serialization.ts‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
publicSnapshotCaptureAnnotations,
1515
type SnapshotCaptureAnnotations,
1616
} from '../snapshot-capture-annotations.ts';
17-
import type { PublicPlatform } from '../kernel/device.ts';
17+
import { isSerialAddressablePlatform, type PublicPlatform } from '../kernel/device.ts';
1818
import { successText, withSuccessText } from '../utils/success-text.ts';
1919

2020
export function buildAppIdentifiers(params: {
@@ -40,7 +40,7 @@ export function buildDeviceIdentifiers(
4040
return {
4141
deviceId: id,
4242
deviceName: name,
43-
...(platform === 'android' || platform === 'vega'
43+
...(isSerialAddressablePlatform(platform)
4444
? { serial: id }
4545
: platform === 'ios'
4646
? { udid: id }
@@ -70,9 +70,9 @@ function serializeSessionDevice(
7070
}
7171

7272
function sessionDeviceSerial(device: AgentDeviceSessionDevice): string | undefined {
73-
if (device.platform === 'android') return device.android?.serial ?? device.id;
74-
if (device.platform === 'vega') return device.vega?.serial ?? device.id;
75-
return undefined;
73+
return isSerialAddressablePlatform(device.platform)
74+
? (device.identifiers.serial ?? device.id)
75+
: undefined;
7676
}
7777

7878
export function serializeSessionListEntry(session: AgentDeviceSession): Record<string, unknown> {

‎src/core/__tests__/capability-plugin-routing-parity.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ function isSupportedReference(command: string, device: DeviceInfo): boolean {
179179
if (!byPlatform) return false;
180180
const supports =
181181
device.platform === 'vega' && VEGA_VVD_ONLY_COMMANDS_REF.has(command)
182-
? (candidate: DeviceInfo) => candidate.kind === 'emulator' && candidate.target === 'tv'
182+
? (candidate: DeviceInfo) => candidate.target === 'tv'
183183
: SUPPORTS_REF[command];
184184
if (supports && !supports(device)) return false;
185185
const kind = (device.kind ?? 'unknown') as keyof NonNullable<CommandCapability['apple']>;

‎src/core/interactors/web.ts‎

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import type { Interactor } from '../interactor-types.ts';
22
import { AppError } from '../../kernel/errors.ts';
33
import { withDiagnosticTimer } from '../../utils/diagnostics.ts';
44
import { resolveWebProvider } from '../../platforms/web/provider.ts';
5+
import { createUnsupportedInteractor } from '../../platforms/unsupported-interactor.ts';
56

67
export function createWebInteractor(): Interactor {
78
const provider = () => resolveWebProvider();
89
return {
10+
...createUnsupportedInteractor('web'),
911
open: (target, options) => provider().open(options?.url ?? target, { url: options?.url }),
1012
openDevice: () => provider().open('about:blank'),
1113
close: (target) => provider().close(target),
1214
tap: (x, y) => provider().click(x, y),
13-
doubleTap: () => unsupportedWebOperation('doubleTap'),
14-
longPress: () => unsupportedWebOperation('longPress'),
1515
focus: (x, y) => provider().click(x, y),
1616
type: (text, delayMs) => provider().typeText(text, { delayMs }),
1717
fill: (x, y, text, delayMs) => provider().fill(x, y, text, { delayMs }),
@@ -30,17 +30,8 @@ export function createWebInteractor(): Interactor {
3030
backend: 'web',
3131
};
3232
},
33-
back: () => unsupportedWebOperation('back'),
34-
home: () => unsupportedWebOperation('home'),
35-
setOrientation: () => unsupportedWebOperation('orientation'),
36-
appSwitcher: () => unsupportedWebOperation('appSwitcher'),
37-
tvRemote: () => unsupportedWebOperation('tvRemote'),
38-
readClipboard: () => unsupportedWebOperation('readClipboard'),
39-
writeClipboard: () => unsupportedWebOperation('writeClipboard'),
40-
setSetting: () => unsupportedWebOperation('setSetting'),
33+
setOrientation: async () => {
34+
throw new AppError('UNSUPPORTED_OPERATION', 'orientation is not supported on web');
35+
},
4136
};
4237
}
43-
44-
async function unsupportedWebOperation(operation: string): Promise<never> {
45-
throw new AppError('UNSUPPORTED_OPERATION', `${operation} is not supported on web`);
46-
}

‎src/core/platform-inventory.test.ts‎

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,12 @@ import assert from 'node:assert/strict';
22
import { test, vi } from 'vitest';
33
import type { DeviceInfo } from '../kernel/device.ts';
44

5-
const { discoverVegaDevices } = vi.hoisted(() => ({
6-
discoverVegaDevices: vi.fn(),
5+
const { listVegaDevices } = vi.hoisted(() => ({
6+
listVegaDevices: vi.fn(),
77
}));
88

9-
vi.mock('../platforms/vega/plugin.ts', () => ({
10-
vegaPlugin: {
11-
discoverDevices: discoverVegaDevices,
12-
},
9+
vi.mock('../platforms/vega/devices.ts', () => ({
10+
listVegaDevices,
1311
}));
1412

1513
import { listLocalDeviceInventory } from './platform-inventory.ts';
@@ -23,8 +21,8 @@ const VEGA_EMULATOR: DeviceInfo = {
2321
booted: true,
2422
};
2523

26-
test('explicit Vega inventory delegates to the Vega plugin discovery seam', async () => {
27-
discoverVegaDevices.mockResolvedValueOnce([VEGA_EMULATOR]);
24+
test('explicit Vega inventory delegates to the Vega device module', async () => {
25+
listVegaDevices.mockResolvedValueOnce([VEGA_EMULATOR]);
2826

2927
const result = await listLocalDeviceInventory({
3028
platform: 'vega',
@@ -33,11 +31,5 @@ test('explicit Vega inventory delegates to the Vega plugin discovery seam', asyn
3331
});
3432

3533
assert.deepEqual(result, [VEGA_EMULATOR]);
36-
assert.deepEqual(discoverVegaDevices.mock.calls[0], [
37-
{
38-
platform: 'vega',
39-
target: 'tv',
40-
serial: VEGA_EMULATOR.id,
41-
},
42-
]);
34+
assert.deepEqual(listVegaDevices.mock.calls[0], []);
4335
});

‎src/core/platform-inventory.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ export async function listLocalDeviceInventory(
3131
}
3232

3333
if (request.platform === 'vega') {
34-
const { vegaPlugin } = await import('../platforms/vega/plugin.ts');
35-
return await vegaPlugin.discoverDevices(request);
34+
const { listVegaDevices } = await import('../platforms/vega/devices.ts');
35+
return await listVegaDevices();
3636
}
3737

3838
if (request.platform) {

‎src/daemon/handlers/session-doctor-toolchain.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export async function appendToolchainChecks(
3535
}
3636

3737
async function vegaToolchainCheck(): Promise<DoctorCheck> {
38+
const { parseVegaDeviceList } = await import('../../platforms/vega/devices.ts');
3839
const { resolveVegaToolProvider } = await import('../../platforms/vega/tool-provider.ts');
3940
const provider = resolveVegaToolProvider();
4041
if (!(await provider.isAvailable())) {
@@ -57,8 +58,7 @@ async function vegaToolchainCheck(): Promise<DoctorCheck> {
5758
});
5859
const versionLine = firstOutputLine(version.stdout);
5960
const hasRunningVvd =
60-
inventory.exitCode === 0 &&
61-
inventory.stdout.split(/\r?\n/).some((line) => /^\s*VirtualDevice\s*:/i.test(line));
61+
inventory.exitCode === 0 && parseVegaDeviceList(inventory.stdout).length > 0;
6262

6363
return {
6464
id: 'toolchain',

0 commit comments

Comments
 (0)