mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
3a02e514e1
* refactor(ios): consolidate series batching onto the sequence runner command Closes #767 Routes every Apple multi-press variant (plain, double-tap, hold, jitter) and swipe series through budget-chunked sequence requests, retiring the daemon-side tapSeries and dragSeries senders: - Add a doubleTap step kind to the sequence allowlist on both ends, mirroring the retired tapSeries doubleTapAt branch. - The single doubleTap interactor sends a one-step sequence and parses the result, surfacing step failures as errors. - Swipe series unroll ping-pong daemon-side into per-step endpoints; the runner's coordinate-drag path ignores durationMs exactly as the daemon-sent (non-synthesized) dragSeries did. - Extract runIosSequenceChunks so press and swipe share the chunking, aggregation, and global step-index rebasing. - Keep tapSeries/dragSeries runner handlers for wire compatibility with older daemons, annotated like interactionFrame; remove both from the preflight-skip allowlist (daemon never sends them) and update ADR 0005 / protocol-optimizations docs. This also closes the latent watchdog exposure where press --count N --interval-ms M routed to tapSeries and executed all pauses inside one 30s-watchdog main-thread block with no chunking. Behavior note: plain tap series now use the synthesized HID tap path on iOS non-tv (with runner-side tapAt fallback), matching the individual tap command instead of the retired tapSeries' XCUICoordinate taps. https://claude.ai/code/session_01VokBZWESTDgcnbYwS4DkJo * refactor(ios): drop dead series wire surface from the daemon - Remove chunkRunnerSequenceSteps: superseded by the budget-aware chunker; no production callers remained. - Remove tapSeries/dragSeries from the RunnerCommand union along with their orphaned fields (count, intervalMs, doubleTap, pauseMs, pattern) and protocol fixtures: this type is the send surface of the current daemon, which no longer sends either command. The Swift runner keeps serving both for wire compatibility with older daemons. - Retarget the ready-mutation preflight test from tapSeries to sequence. https://claude.ai/code/session_01VokBZWESTDgcnbYwS4DkJo * refactor(ios): remove retired series and frame wire commands entirely Drops the runner-side wire compatibility for tapSeries, dragSeries, and interactionFrame now that no daemon path sends them (series fuse into sequence since this branch; interactionFrame was fused into scroll in #760): - Swift: delete the three handler cases, performDragSeries, runSeries (no remaining callers), the CommandType enum cases, journal-retention and traits entries, and the Command fields (count, intervalMs, doubleTap, pauseMs, pattern) that existed only for them. The never-sent synthesized dragSeries branch goes with it. - TS: drop interactionFrame from the RunnerCommand union and isReadOnlyRunnerCommand, and its protocol fixture. - Update stale perf scenario labels referencing the retired commands. Verified dead before removal: no dynamic command construction anywhere (runner-command-recovery only echoes in-flight command ids), no raw-string references in Swift, no docs references. Helpers shared with live paths (synthesizedDragAt, doubleTapAt, keyboardAvoidingDragPoints, sleepFor) all retain callers. Compat: an old daemon paired with a runner built from these sources gets a CommandType decode rejection; the source-fingerprint check rebuilds a matching runner on the next session. https://claude.ai/code/session_01VokBZWESTDgcnbYwS4DkJo --------- Co-authored-by: Claude <noreply@anthropic.com>
172 lines
6.7 KiB
TypeScript
172 lines
6.7 KiB
TypeScript
import path from 'node:path';
|
|
import type { ResolvedProfile } from './platform-profiles.ts';
|
|
|
|
// A legacy-form batch step: maps through the exact documented CLI grammar.
|
|
// `flags` uses internal CliFlags field names (e.g. snapshotInteractiveOnly).
|
|
export type BatchStepSpec = {
|
|
command: string;
|
|
positionals?: string[];
|
|
flags?: Record<string, unknown>;
|
|
};
|
|
|
|
type ScenarioStepBase = {
|
|
label: string;
|
|
command: string;
|
|
// When set, the harness runs an untimed `open --relaunch` (reset to root, top of list)
|
|
// before timing this step. Used for steps whose precondition is a clean root, since
|
|
// earlier commands (find/is, search) leave the list scrolled or in a different surface.
|
|
freshRoot?: boolean;
|
|
};
|
|
|
|
// Discriminated on execMode so the invoker gets the right payload without `!`/`?? []`:
|
|
// standalone carries full CLI args; batch carries one legacy batch step.
|
|
export type ScenarioStep =
|
|
| (ScenarioStepBase & { execMode: 'standalone'; args: string[] })
|
|
| (ScenarioStepBase & { execMode: 'batch'; step: BatchStepSpec; isSnapshot?: boolean });
|
|
|
|
export type StepContext = { artifactsDir: string };
|
|
|
|
function std(label: string, command: string, args: string[]): ScenarioStep {
|
|
return { label, command, execMode: 'standalone', args };
|
|
}
|
|
|
|
function bat(
|
|
label: string,
|
|
command: string,
|
|
step: BatchStepSpec,
|
|
opts: { isSnapshot?: boolean; freshRoot?: boolean } = {},
|
|
): ScenarioStep {
|
|
return { label, command, execMode: 'batch' as const, step, ...opts };
|
|
}
|
|
|
|
// One ordered pass over Settings. The harness repeats this N (+warmup) times;
|
|
// the leading `open --relaunch` resets the app to its root each round, so every
|
|
// round starts from a known state while commands run in their natural order.
|
|
export function buildSettingsTour(p: ResolvedProfile, ctx: StepContext): ScenarioStep[] {
|
|
const s = p.selectors;
|
|
const shot = path.join(ctx.artifactsDir, 'shot.png');
|
|
const rec = path.join(ctx.artifactsDir, 'rec.mp4');
|
|
const trace = path.join(ctx.artifactsDir, 'trace.log');
|
|
|
|
// Text entry differs per platform: iOS fills the root search field directly (focusing it
|
|
// first can hang); Android must open the search screen before an editable field exists.
|
|
const textEntry: ScenarioStep[] = p.selectors.searchEditableAtRoot
|
|
? [
|
|
// iOS: editable search field exists at root; fill it directly (freshRoot resets scroll).
|
|
bat(
|
|
'fill search',
|
|
'fill',
|
|
{ command: 'fill', positionals: [s.searchFieldEditable, 'general'] },
|
|
{ freshRoot: true },
|
|
),
|
|
bat('type', 'type', { command: 'type', positionals: ['wifi'] }),
|
|
bat('get editable text', 'get', {
|
|
command: 'get',
|
|
positionals: ['text', s.searchFieldEditable],
|
|
}),
|
|
bat('keyboard return', 'keyboard', { command: 'keyboard', positionals: ['return'] }),
|
|
]
|
|
: [
|
|
// Android: tap the search entry first to reveal the editable, then type/fill it.
|
|
bat(
|
|
'press search field',
|
|
'press',
|
|
{ command: 'press', positionals: [s.searchField] },
|
|
{ freshRoot: true },
|
|
),
|
|
bat('type', 'type', { command: 'type', positionals: ['wifi'] }),
|
|
bat('fill search', 'fill', {
|
|
command: 'fill',
|
|
positionals: [s.searchFieldEditable, 'general'],
|
|
}),
|
|
bat('get editable text', 'get', {
|
|
command: 'get',
|
|
positionals: ['text', s.searchFieldEditable],
|
|
}),
|
|
];
|
|
|
|
// These iOS-only repeated gesture forms fuse into `sequence` runner requests:
|
|
// press --count > 1 and swipe --count > 1 both batch their steps into one command.
|
|
const iosRunnerSeries: ScenarioStep[] =
|
|
p.platform === 'ios'
|
|
? [
|
|
bat(
|
|
'press series (sequence)',
|
|
'press',
|
|
{ command: 'press', positionals: ['200', '95'], flags: { count: 2, intervalMs: 50 } },
|
|
{ freshRoot: true },
|
|
),
|
|
bat(
|
|
'swipe series (sequence)',
|
|
'swipe',
|
|
{
|
|
command: 'swipe',
|
|
positionals: ['200', '650', '200', '450', '120'],
|
|
flags: { count: 2, pauseMs: 50, pattern: 'ping-pong' },
|
|
},
|
|
{ freshRoot: true },
|
|
),
|
|
]
|
|
: [];
|
|
|
|
return [
|
|
// --- reset to root via relaunch ---
|
|
std('open (relaunch → root)', 'open', ['open', p.appTarget, '--relaunch']),
|
|
|
|
// --- reads on the root tree (snapshots first; anchor label is visible here) ---
|
|
bat(
|
|
'snapshot -i (root)',
|
|
'snapshot',
|
|
{ command: 'snapshot', flags: { snapshotInteractiveOnly: true } },
|
|
{ isSnapshot: true },
|
|
),
|
|
bat('snapshot (root)', 'snapshot', { command: 'snapshot' }, { isSnapshot: true }),
|
|
|
|
// --- navigate into a sub-screen from a fresh root (freshRoot resets scroll so the
|
|
// deep-screen row is in view), read it, then return ---
|
|
bat(
|
|
'press → deep screen',
|
|
'press',
|
|
{ command: 'press', positionals: [s.deepScreen] },
|
|
{ freshRoot: true },
|
|
),
|
|
bat('snapshot (deep)', 'snapshot', { command: 'snapshot' }, { isSnapshot: true }),
|
|
bat(
|
|
'snapshot -i (deep)',
|
|
'snapshot',
|
|
{ command: 'snapshot', flags: { snapshotInteractiveOnly: true } },
|
|
{ isSnapshot: true },
|
|
),
|
|
bat('back', 'back', { command: 'back' }),
|
|
|
|
// --- iOS runner series commands surfaced by PR #643 ---
|
|
...iosRunnerSeries,
|
|
|
|
// --- targeted reads against the visible anchor (freshRoot so the anchor is on screen) ---
|
|
bat(
|
|
'wait text',
|
|
'wait',
|
|
{ command: 'wait', positionals: ['text', s.anchorText, '3000'] },
|
|
{ freshRoot: true },
|
|
),
|
|
bat('find', 'find', { command: 'find', positionals: [s.anchorText] }),
|
|
bat('get text', 'get', { command: 'get', positionals: ['text', s.anchorLabel] }),
|
|
bat('is visible', 'is', { command: 'is', positionals: ['visible', s.anchorLabel] }),
|
|
|
|
// --- text entry (platform-specific order; see textEntry above) then scroll results ---
|
|
...textEntry,
|
|
bat('scroll down', 'scroll', { command: 'scroll', positionals: ['down'] }),
|
|
|
|
// --- artifact-producing commands; record brackets the rest so the clip has >1s of
|
|
// footage (an instant start→stop makes simctl recordVideo fail to finalize) ---
|
|
std('record start', 'record', ['record', 'start', rec, '--hide-touches']),
|
|
bat('screenshot', 'screenshot', { command: 'screenshot', positionals: [shot] }),
|
|
bat('logs mark', 'logs', { command: 'logs', positionals: ['mark', 'perf-mark'] }),
|
|
bat('logs clear', 'logs', { command: 'logs', positionals: ['clear'] }),
|
|
std('trace start', 'trace', ['trace', 'start', trace]),
|
|
std('trace stop', 'trace', ['trace', 'stop']),
|
|
bat('perf', 'perf', { command: 'perf' }),
|
|
std('record stop', 'record', ['record', 'stop']),
|
|
];
|
|
}
|