Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/contracts/src/client-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type { DeviceCommandBaseOptions } from './client-connection.ts';

export type PermissionTarget =
| 'all'
| 'camera'
| 'microphone'
| 'photos'
Expand Down
5 changes: 4 additions & 1 deletion packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AppError } from '@agent-device/kernel/errors';

export type PermissionAction = 'grant' | 'deny' | 'reset';
export type PermissionTarget =
| 'all'
| 'camera'
| 'microphone'
| 'photos'
Expand All @@ -22,6 +23,7 @@ export type SettingOptions = {
longitude?: number;
};
const PERMISSION_TARGETS: readonly PermissionTarget[] = [
'all',
'camera',
'microphone',
'photos',
Expand All @@ -47,7 +49,7 @@ const SETTINGS_FINGERPRINT_USAGE = 'fingerprint <match|nonmatch>';
const SETTINGS_CLEAR_APP_STATE_USAGE = 'clear-app-state [app-id]';
const SETTINGS_RESET_KEYCHAIN_USAGE = 'reset-keychain clear';
const SETTINGS_PERMISSION_USAGE =
'permission <grant|deny|reset> <camera|microphone|photos|contacts|contacts-limited|notifications|calendar|location|location-always|media-library|motion|reminders|siri> [full|limited]';
'permission <grant|deny|reset> <all|camera|microphone|photos|contacts|contacts-limited|notifications|calendar|location|location-always|media-library|motion|reminders|siri> [full|limited]';
const SETTINGS_MACOS_PERMISSION_USAGE =
'permission <grant|reset> <accessibility|screen-recording|input-monitoring>';
const SETTINGS_MACOS_SUPPORTED_MESSAGE = `macOS supports only settings ${SETTINGS_APPEARANCE_USAGE} and settings ${SETTINGS_MACOS_PERMISSION_USAGE}. wifi|airplane|location|animations remain unsupported on macOS.`;
Expand Down Expand Up @@ -88,6 +90,7 @@ export function parsePermissionAction(action: string): PermissionAction {
export function parsePermissionTarget(value: string | undefined): PermissionTarget {
const normalized = value?.trim().toLowerCase();
if (
normalized === 'all' ||
normalized === 'camera' ||
normalized === 'microphone' ||
normalized === 'photos' ||
Expand Down
2 changes: 2 additions & 0 deletions packages/maestro/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export {
MAESTRO_COMPAT_SUPPORTED_CAPABILITIES,
} from './internal/facade-support.ts';

export { MAESTRO_PERMISSION_VALUES } from './internal/program-ir-values.ts';

export {
createMaestroRuntimePort,
literalFromMaestroRegex,
Expand Down
104 changes: 104 additions & 0 deletions packages/maestro/src/internal/__tests__/program-ir-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,110 @@ describe('parseMaestroProgram', () => {
});
});

test('parses setPermissions maps, variables, and optional/label', () => {
const program = parseMaestroProgram(`appId: example.app
---
- setPermissions:
permissions:
all: deny
notifications: unset
- setPermissions:
appId: child.app
permissions:
camera: \${CAMERA_STATE}
location: always
optional: true
label: Prepare scan
`);

assert.deepEqual(program.commands[0], {
kind: 'setPermissions',
source: { line: 3 },
permissions: { all: 'deny', notifications: 'unset' },
});
assert.deepEqual(program.commands[1], {
kind: 'setPermissions',
source: { line: 7 },
appId: 'child.app',
permissions: { camera: '${CAMERA_STATE}', location: 'always' },
optional: true,
label: 'Prepare scan',
});
// Prototype names are not duplicates: the YAML layer already rejects real
// duplicate keys, so parsing accepts them and the backend verdict applies.
const prototype = parseMaestroProgram(`---
- setPermissions:
permissions:
constructor: allow
`);
assert.deepEqual(prototype.commands[0], {
kind: 'setPermissions',
source: { line: 2 },
permissions: { constructor: 'allow' },
});
assert.throws(
() =>
parseMaestroProgram(`---
- setPermissions:
appId: example.app
`),
/requires permissions.*line 2/i,
);
assert.throws(
() =>
parseMaestroProgram(`---
- setPermissions:
permissions:
camera: sometimes
`),
/allow\|deny\|unset.*line 4/i,
);
assert.throws(
() =>
parseMaestroProgram(`---
- setPermissions:
permissions:
camera: \${ALLOW + 1}
`),
/not supported.*line 4/i,
);
});

test('parses launchApp permissions maps', () => {
const program = parseMaestroProgram(`appId: example.app
---
- launchApp:
clearState: true
permissions:
all: deny
camera: \${CAMERA_STATE}
`);

assert.deepEqual(program.commands[0], {
kind: 'launchApp',
source: { line: 3 },
clearState: true,
permissions: { all: 'deny', camera: '${CAMERA_STATE}' },
});
assert.throws(
() =>
parseMaestroProgram(`---
- launchApp:
permissions: {}
`),
/launchApp\.permissions requires at least one permission.*line 3/i,
);
assert.throws(
() =>
parseMaestroProgram(`---
- launchApp:
permissions:
camera: sometimes
`),
/allow\|deny\|unset.*line 4/i,
);
});

test('reports source lines for unsupported and invalid command shapes', () => {
assert.throws(
() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function makeOperations(
resolveGestureViewport: async () => ({ x: 0, y: 0, width: 402, height: 874 }),
launchApp: noOp,
stopApp: noOp,
setPermissions: noOp,
openLink: noOp,
tapOn: noOp,
doubleTapOn: noOp,
Expand Down
34 changes: 34 additions & 0 deletions packages/maestro/src/internal/__tests__/runtime-port.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,40 @@ import {
} from './runtime-port-fixtures.ts';

describe('MaestroRuntimePort', () => {
test('dispatches setPermissions with the flow appId and resolved values', async () => {
const calls: RecordedCall[] = [];
const operations = makeOperations({
setPermissions: vi.fn(async (input, context) =>
record(calls, 'setPermissions', input, context),
),
});
const program = parseMaestroProgram(
[
'appId: com.example.checkout',
'env:',
' CAMERA_STATE: allow',
'---',
'- setPermissions:',
' permissions:',
' all: deny',
' camera: ${CAMERA_STATE}',
].join('\n'),
);

const result = await executeMaestroProgram(program, createMaestroRuntimePort(operations));

expect(result).toMatchObject({ executed: 1, skipped: 0 });
expect(calls).toHaveLength(1);
expect(calls[0]).toMatchObject({
kind: 'setPermissions',
input: {
appId: 'com.example.checkout',
permissions: { all: 'deny', camera: 'allow' },
},
appId: 'com.example.checkout',
});
});

test('delegates typed lifecycle, input, keyboard, screenshot, and script operations', async () => {
const calls: RecordedCall[] = [];
const operations = makeOperations({
Expand Down
Loading