Skip to content

Commit d75911d

Browse files
committed
fix: tighten maestro runtime architecture
1 parent aca9637 commit d75911d

30 files changed

Lines changed: 479 additions & 328 deletions

scripts/maestro-conformance-model.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
33
import { parseMaestroProgram } from '../src/compat/maestro/program-ir-parser.ts';
4+
import { MAESTRO_COMPATIBILITY_PRESETS } from '../src/compat/maestro/compatibility-policy.ts';
45
import type {
56
MaestroCommand,
67
MaestroProgram,
@@ -20,7 +21,7 @@ import type {
2021
} from './maestro-conformance-types.ts';
2122
import { readOptionalString, readRequiredRecord } from './maestro-conformance-values.ts';
2223

23-
const DEFAULT_SWIPE_DURATION_MS = 400;
24+
const UPSTREAM_DEFAULT_SWIPE_DURATION_MS = 400;
2425

2526
export function normalizeUpstreamFixture(
2627
fixture: RawFixture,
@@ -87,7 +88,7 @@ function normalizeAgentCommand(
8788
{
8889
kind: command.kind,
8990
selector: normalizeTypedSelector(command.target),
90-
timeoutMs: 17_000,
91+
timeoutMs: MAESTRO_COMPATIBILITY_PRESETS.command.targetLookupTimeoutMs,
9192
source,
9293
},
9394
];
@@ -139,7 +140,7 @@ function normalizeAgentSwipe(
139140
gesture: Extract<MaestroCommand, { kind: 'swipe' }>['gesture'],
140141
source: NormalizedSource,
141142
): NormalizedAction {
142-
const durationMs = gesture.duration ?? DEFAULT_SWIPE_DURATION_MS;
143+
const durationMs = gesture.duration ?? MAESTRO_COMPATIBILITY_PRESETS.command.swipeDurationMs;
143144
if (gesture.kind === 'screen') {
144145
return { kind: 'swipe', mode: 'direction', direction: gesture.direction, durationMs, source };
145146
}
@@ -207,7 +208,7 @@ function normalizeUpstreamCommand(
207208
}
208209

209210
function normalizeUpstreamSwipe(command: RawCommand, source: NormalizedSource): NormalizedAction {
210-
const durationMs = integerOrDefault(command.duration, DEFAULT_SWIPE_DURATION_MS);
211+
const durationMs = integerOrDefault(command.duration, UPSTREAM_DEFAULT_SWIPE_DURATION_MS);
211212
const startRelative = readOptionalString(command, 'startRelative');
212213
const endRelative = readOptionalString(command, 'endRelative');
213214
if (startRelative !== undefined || endRelative !== undefined) {
Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
1-
import type { DaemonInvokeFn, DaemonRequest } from '../../../daemon/types.ts';
1+
import type { DaemonRequest } from '../../../daemon/types.ts';
22
import type { SnapshotNode, SnapshotState } from '../../../kernel/snapshot.ts';
33
import type { CreateDaemonMaestroRuntimeOperationsOptions } from '../daemon-runtime-port.ts';
44

5-
export type CapturedDaemonRequest = Pick<
6-
DaemonRequest,
7-
'command' | 'positionals' | 'input' | 'flags'
8-
>;
9-
105
export const IOS_FRAME = { x: 0, y: 0, width: 402, height: 874 } as const;
116

127
export function makeSnapshot(
@@ -39,26 +34,3 @@ export function makeDependencies(
3934
resolveGestureViewport: async () => ({ x: 0, y: 0, width: 402, height: 874 }),
4035
};
4136
}
42-
43-
export function makeInvoke(
44-
requests: CapturedDaemonRequest[],
45-
response: DaemonInvokeFn = async (request) => {
46-
requests.push({
47-
command: request.command,
48-
positionals: request.positionals,
49-
input: request.input,
50-
flags: request.flags,
51-
});
52-
return { ok: true, data: {} };
53-
},
54-
): DaemonInvokeFn {
55-
return async (request) => {
56-
requests.push({
57-
command: request.command,
58-
positionals: request.positionals,
59-
input: request.input,
60-
flags: request.flags,
61-
});
62-
return await response(request);
63-
};
64-
}

src/compat/maestro/__tests__/daemon-runtime-port-observation-input.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ test('commits Maestro input text before dispatching an immediate tap', async ()
149149
'snapshot',
150150
'snapshot',
151151
'snapshot',
152-
'snapshot',
153152
'click',
154153
]);
155154
});

src/compat/maestro/__tests__/daemon-runtime-port-observation-scroll.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ test('scrolls until the target is fully visible in the screen viewport', async (
9393
platform: 'ios',
9494
});
9595

96-
await port.execute({
96+
const result = await port.execute({
9797
command: {
9898
kind: 'scrollUntilVisible',
9999
source: { line: 2 },
@@ -108,6 +108,8 @@ test('scrolls until the target is fully visible in the screen viewport', async (
108108

109109
expect(requests.filter(({ command }) => command === 'scroll')).toHaveLength(1);
110110
expect(requests.filter(({ command }) => command === 'snapshot')).toHaveLength(3);
111+
expect(result.observation).toMatchObject({ generation: 1, matched: true });
112+
expect(result.observation?.identity).toMatch(/^maestro-observation-/);
111113
});
112114

113115
test('does not treat a target larger than the viewport as fully visible', async () => {

src/compat/maestro/__tests__/daemon-runtime-port-observation.test.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import type { DaemonRequest } from '../../../daemon/types.ts';
33
import { createDaemonMaestroRuntimePort } from '../daemon-runtime-port.ts';
44
import {
55
MAESTRO_OBSERVATION_POLL_MS,
6+
resolveTypedMaestroTarget,
67
waitForTypedSnapshotStability,
78
} from '../daemon-runtime-port-observation.ts';
89
import { makeBaseRequest, makeDependencies, makeSnapshot } from './daemon-runtime-port-fixtures.ts';
910

10-
test('does not pair an observation with later same-generation snapshots', async () => {
11+
test('replaces pre-mutation evidence with the stable post-mutation snapshot', async () => {
1112
const requests: DaemonRequest[] = [];
1213
let snapshots = 0;
1314
const port = createDaemonMaestroRuntimePort({
@@ -16,7 +17,7 @@ test('does not pair an observation with later same-generation snapshots', async
1617
requests.push(request);
1718
if (request.command !== 'snapshot') return { ok: true, data: {} };
1819
snapshots += 1;
19-
const targetX = snapshots >= 4 ? 40 : 200;
20+
const targetX = snapshots >= 2 ? 40 : 200;
2021
return {
2122
ok: true,
2223
data: {
@@ -49,7 +50,7 @@ test('does not pair an observation with later same-generation snapshots', async
4950
platform: 'ios',
5051
});
5152

52-
const observation = await port.observe({
53+
await port.observe({
5354
condition: { kind: 'visible', selector: { id: 'ready' } },
5455
timeoutMs: 0,
5556
generation: 0,
@@ -58,7 +59,6 @@ test('does not pair an observation with later same-generation snapshots', async
5859
await port.execute({
5960
command: { kind: 'inputText', source: { line: 2 }, text: 'hello' },
6061
generation: 0,
61-
cachedObservation: observation,
6262
env: {},
6363
invalidateObservation() {},
6464
});
@@ -68,8 +68,7 @@ test('does not pair an observation with later same-generation snapshots', async
6868
source: { line: 3 },
6969
target: { space: 'target', selector: { id: 'continue' } },
7070
},
71-
generation: 0,
72-
cachedObservation: observation,
71+
generation: 1,
7372
env: {},
7473
invalidateObservation() {},
7574
});
@@ -79,10 +78,49 @@ test('does not pair an observation with later same-generation snapshots', async
7978
'type',
8079
'snapshot',
8180
'snapshot',
82-
'snapshot',
8381
'click',
8482
]);
8583
expect(requests.at(-1)?.positionals).toEqual(['id="continue"']);
84+
expect(requests.at(-1)?.flags?.maestro?.expectedTapPoint).toEqual({ x: 100, y: 122 });
85+
});
86+
87+
test('computes expensive target evidence only for the command policies that consume it', () => {
88+
const snapshot = makeSnapshot([
89+
{ index: 0, type: 'Application', rect: { x: 0, y: 0, width: 402, height: 874 } },
90+
{
91+
index: 1,
92+
parentIndex: 0,
93+
type: 'Button',
94+
identifier: 'continue',
95+
hittable: true,
96+
rect: { x: 20, y: 40, width: 120, height: 44 },
97+
},
98+
]);
99+
const base = {
100+
context: { generation: 0, env: {} },
101+
snapshot,
102+
platform: 'ios' as const,
103+
};
104+
105+
const ordinary = resolveTypedMaestroTarget({
106+
...base,
107+
query: { selector: { id: 'continue' }, purpose: 'tap', timeoutMs: 0 },
108+
});
109+
const atomicRetry = resolveTypedMaestroTarget({
110+
...base,
111+
query: {
112+
selector: { id: 'continue' },
113+
purpose: 'tap',
114+
timeoutMs: 0,
115+
allowAtomicSelectorDispatch: true,
116+
includeSurfaceSignature: true,
117+
},
118+
});
119+
120+
expect(ordinary).not.toHaveProperty('surfaceSignature');
121+
expect(ordinary).not.toHaveProperty('dispatchSelector');
122+
expect(atomicRetry.surfaceSignature).toMatch(/^[a-f0-9]{64}$/);
123+
expect(atomicRetry.dispatchSelector).toEqual({ key: 'id', value: 'continue' });
86124
});
87125

88126
test('compares snapshots before sleeping and captures once beyond a zero settle budget', async () => {
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { expect, test, vi } from 'vitest';
2+
import { invokeMaestroPublicOperation } from '../daemon-runtime-port-support.ts';
3+
import { makeBaseRequest, makeDependencies } from './daemon-runtime-port-fixtures.ts';
4+
5+
test('composes operation-specific Maestro flags with the runtime envelope', async () => {
6+
const invoke = vi.fn(async () => ({ ok: true as const, data: {} }));
7+
8+
await invokeMaestroPublicOperation(
9+
{
10+
baseReq: makeBaseRequest({
11+
flags: {
12+
platform: 'ios',
13+
maestro: { prewarmRunnerBeforeOpen: true },
14+
},
15+
}),
16+
invoke,
17+
dependencies: makeDependencies(),
18+
platform: 'ios',
19+
},
20+
{
21+
kind: 'clickSelector',
22+
selector: { key: 'id', value: 'submit' },
23+
expectedPoint: { x: 10, y: 20 },
24+
options: {},
25+
},
26+
);
27+
28+
expect(invoke).toHaveBeenCalledWith(
29+
expect.objectContaining({
30+
flags: expect.objectContaining({
31+
maestro: {
32+
prewarmRunnerBeforeOpen: true,
33+
allowNonHittableCoordinateFallback: true,
34+
expectedTapPoint: { x: 10, y: 20 },
35+
},
36+
}),
37+
}),
38+
);
39+
});

src/compat/maestro/__tests__/runtime-port-gestures.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ test('uses the structured gesture contract without observing absolute swipes', a
6666
to: { x: 300, y: 200 },
6767
durationMs: 240,
6868
},
69-
expect.objectContaining({ generation: 0 }),
69+
expect.objectContaining({ generation: 1 }),
7070
);
7171
expect(gesture).toHaveBeenNthCalledWith(
7272
2,
@@ -76,7 +76,7 @@ test('uses the structured gesture contract without observing absolute swipes', a
7676
durationMs: 400,
7777
},
7878
expect.objectContaining({
79-
generation: 1,
79+
generation: 2,
8080
gestureViewport: { x: 10, y: 20, width: 400, height: 800 },
8181
}),
8282
);
@@ -88,7 +88,7 @@ test('uses the structured gesture contract without observing absolute swipes', a
8888
durationMs: 300,
8989
},
9090
expect.objectContaining({
91-
generation: 2,
91+
generation: 3,
9292
gestureViewport: { x: 10, y: 20, width: 400, height: 800 },
9393
}),
9494
);
@@ -100,7 +100,7 @@ test('uses the structured gesture contract without observing absolute swipes', a
100100
durationMs: 400,
101101
},
102102
expect.objectContaining({
103-
generation: 3,
103+
generation: 4,
104104
gestureViewport: { x: 10, y: 20, width: 400, height: 800 },
105105
}),
106106
);

src/compat/maestro/__tests__/runtime-port.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ describe('MaestroRuntimePort', () => {
9292
clearState: true,
9393
launchArguments: { kind: 'map', values: { seed: 7 } },
9494
},
95-
generation: 0,
95+
generation: 1,
9696
appId: 'com.example.checkout',
9797
});
9898
expect(calls[5]).toMatchObject({
@@ -104,12 +104,12 @@ describe('MaestroRuntimePort', () => {
104104
durationMs: 601,
105105
},
106106
});
107-
expect(calls[6]).toMatchObject({ kind: 'hideKeyboard', input: {}, generation: 6 });
108-
expect(calls[7]).toMatchObject({ kind: 'pressKey', input: { key: 'enter' }, generation: 7 });
107+
expect(calls[6]).toMatchObject({ kind: 'hideKeyboard', input: {}, generation: 7 });
108+
expect(calls[7]).toMatchObject({ kind: 'pressKey', input: { key: 'enter' }, generation: 8 });
109109
expect(calls[9]).toMatchObject({
110110
kind: 'waitForAnimationToEnd',
111111
input: { timeoutMs: 50 },
112-
generation: 9,
112+
generation: 10,
113113
});
114114
expect(calls[11]).toMatchObject({
115115
kind: 'runScript',
@@ -275,7 +275,7 @@ describe('MaestroRuntimePort', () => {
275275
}),
276276
}),
277277
}),
278-
expect.objectContaining({ generation: 0 }),
278+
expect.objectContaining({ generation: 1 }),
279279
);
280280
expect(gesture).toHaveBeenCalledWith(
281281
{
@@ -284,7 +284,7 @@ describe('MaestroRuntimePort', () => {
284284
durationMs: 400,
285285
},
286286
expect.objectContaining({
287-
generation: 1,
287+
generation: 2,
288288
gestureViewport: { x: 0, y: 0, width: 402, height: 874 },
289289
}),
290290
);

0 commit comments

Comments
 (0)