Skip to content

Commit ed5ad2b

Browse files
committed
refactor: split ios runner status recovery handling
1 parent e32746f commit ed5ad2b

1 file changed

Lines changed: 109 additions & 65 deletions

File tree

src/platforms/ios/runner-client.ts

Lines changed: 109 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -264,93 +264,137 @@ async function tryRecoverRunnerCommandAfterTransportError(
264264
lifecycleState,
265265
},
266266
});
267+
return handleRunnerCommandStatusRecovery(
268+
status,
269+
lifecycleState,
270+
command,
271+
transportError,
272+
options,
273+
);
274+
}
267275

276+
function handleRunnerCommandStatusRecovery(
277+
status: Record<string, unknown>,
278+
lifecycleState: string,
279+
command: RunnerCommand,
280+
transportError: AppError,
281+
options: AppleRunnerCommandOptions,
282+
): Record<string, unknown> | undefined {
268283
if (lifecycleState === 'completed') {
269-
const recovered = parseLifecycleResponseJson(status.lifecycleResponseJson);
270-
if (recovered) return recovered;
271-
if (isReadOnlyRunnerCommand(command.command)) {
272-
throw transportError;
273-
}
274-
throw new AppError(
275-
'COMMAND_FAILED',
276-
`Runner command "${command.command}" completed after the transport response was lost, but no recoverable response was retained.`,
277-
{
278-
command: command.command,
279-
commandId: command.commandId,
280-
lifecycleState,
281-
recovery: 'completed_without_retained_response',
282-
hint: completedWithoutRetainedResponseHint(command.command),
283-
logPath: options.logPath,
284-
transportError: transportError.message,
285-
},
286-
transportError,
287-
);
284+
return handleCompletedRunnerStatus(status, command, transportError, options);
288285
}
289286

290287
if (lifecycleState === 'failed') {
291-
const errorCode =
292-
typeof status.lifecycleErrorCode === 'string' ? status.lifecycleErrorCode : undefined;
293-
const errorMessage =
294-
typeof status.lifecycleErrorMessage === 'string'
295-
? status.lifecycleErrorMessage
296-
: 'Runner command failed';
297-
const hint =
298-
typeof status.lifecycleErrorHint === 'string' ? status.lifecycleErrorHint : undefined;
299-
throw new AppError(
300-
toAppErrorCode(errorCode),
301-
errorMessage,
302-
{
303-
command: command.command,
304-
commandId: command.commandId,
305-
lifecycleState,
306-
recovery: 'runner_reported_failure',
307-
hint: hint ?? runnerReportedFailureHint(command.command),
308-
logPath: options.logPath,
309-
transportError: transportError.message,
310-
},
311-
transportError,
312-
);
288+
throw runnerStatusFailureError(status, command, transportError, options);
313289
}
314290

315291
if (lifecycleState === 'accepted' || lifecycleState === 'started') {
316-
if (isReadOnlyRunnerCommand(command.command)) {
317-
throw transportError;
318-
}
319-
throw new AppError(
320-
'COMMAND_FAILED',
321-
`Runner command "${command.command}" is still ${lifecycleState} after the transport response was lost.`,
322-
{
323-
command: command.command,
324-
commandId: command.commandId,
325-
lifecycleState,
326-
recovery: 'command_still_in_flight',
327-
hint: inFlightAfterLostResponseHint(command.command),
328-
logPath: options.logPath,
329-
transportError: transportError.message,
330-
},
331-
transportError,
332-
);
292+
throw runnerStatusInFlightError(lifecycleState, command, transportError, options);
333293
}
334294

335295
return undefined;
336296
}
337297

298+
function handleCompletedRunnerStatus(
299+
status: Record<string, unknown>,
300+
command: RunnerCommand,
301+
transportError: AppError,
302+
options: AppleRunnerCommandOptions,
303+
): Record<string, unknown> | undefined {
304+
const recovered = parseLifecycleResponseJson(status.lifecycleResponseJson);
305+
if (recovered) return recovered;
306+
if (isReadOnlyRunnerCommand(command.command)) {
307+
throw transportError;
308+
}
309+
throw new AppError(
310+
'COMMAND_FAILED',
311+
`Runner command "${command.command}" completed after the transport response was lost, but no recoverable response was retained.`,
312+
{
313+
command: command.command,
314+
commandId: command.commandId,
315+
lifecycleState: 'completed',
316+
recovery: 'completed_without_retained_response',
317+
hint: completedWithoutRetainedResponseHint(command.command),
318+
logPath: options.logPath,
319+
transportError: transportError.message,
320+
},
321+
transportError,
322+
);
323+
}
324+
325+
function runnerStatusFailureError(
326+
status: Record<string, unknown>,
327+
command: RunnerCommand,
328+
transportError: AppError,
329+
options: AppleRunnerCommandOptions,
330+
): AppError {
331+
const errorCode =
332+
typeof status.lifecycleErrorCode === 'string' ? status.lifecycleErrorCode : undefined;
333+
const errorMessage =
334+
typeof status.lifecycleErrorMessage === 'string'
335+
? status.lifecycleErrorMessage
336+
: 'Runner command failed';
337+
const hint =
338+
typeof status.lifecycleErrorHint === 'string' ? status.lifecycleErrorHint : undefined;
339+
return new AppError(
340+
toAppErrorCode(errorCode),
341+
errorMessage,
342+
{
343+
command: command.command,
344+
commandId: command.commandId,
345+
lifecycleState: 'failed',
346+
recovery: 'runner_reported_failure',
347+
hint: hint ?? runnerReportedFailureHint(command.command),
348+
logPath: options.logPath,
349+
transportError: transportError.message,
350+
},
351+
transportError,
352+
);
353+
}
354+
355+
function runnerStatusInFlightError(
356+
lifecycleState: string,
357+
command: RunnerCommand,
358+
transportError: AppError,
359+
options: AppleRunnerCommandOptions,
360+
): AppError {
361+
if (isReadOnlyRunnerCommand(command.command)) {
362+
return transportError;
363+
}
364+
return new AppError(
365+
'COMMAND_FAILED',
366+
`Runner command "${command.command}" is still ${lifecycleState} after the transport response was lost.`,
367+
{
368+
command: command.command,
369+
commandId: command.commandId,
370+
lifecycleState,
371+
recovery: 'command_still_in_flight',
372+
hint: inFlightAfterLostResponseHint(command.command),
373+
logPath: options.logPath,
374+
transportError: transportError.message,
375+
},
376+
transportError,
377+
);
378+
}
379+
338380
function parseLifecycleResponseJson(value: unknown): Record<string, unknown> | undefined {
339381
if (typeof value !== 'string' || value.trim().length === 0) return undefined;
340-
let parsed: LifecycleResponsePayload;
341-
try {
342-
const raw: unknown = JSON.parse(value);
343-
parsed = raw && typeof raw === 'object' ? (raw as LifecycleResponsePayload) : {};
344-
} catch {
345-
return undefined;
346-
}
382+
const parsed = parseLifecycleResponsePayload(value);
347383
if (!parsed.ok) return undefined;
348384
if (parsed.data && typeof parsed.data === 'object' && !Array.isArray(parsed.data)) {
349385
return parsed.data as Record<string, unknown>;
350386
}
351387
return {};
352388
}
353389

390+
function parseLifecycleResponsePayload(value: string): LifecycleResponsePayload {
391+
try {
392+
const raw: unknown = JSON.parse(value);
393+
if (raw && typeof raw === 'object') return raw as LifecycleResponsePayload;
394+
} catch {}
395+
return {};
396+
}
397+
354398
function completedWithoutRetainedResponseHint(command: string): string {
355399
return `The runner reports "${command}" already completed, so agent-device will not replay it. Run snapshot -i to inspect the current UI, then continue from that observed state. If the session is stale, close and reopen the session before retrying.`;
356400
}

0 commit comments

Comments
 (0)