Skip to content

Commit 7e5ed4d

Browse files
thymikeeclaude
andauthored
test(ios): pin a read-only cancel in the connect retry delay (#3413)
A read-only command sent to a starting runner retries refused connects. A cancellation that lands in the retry delay after a refused attempt now has a test at the exchange: the command is reported `dispatched: no`, its charge is withdrawn, and no status poll is sent. The rule that an attempt failure counts as possibly written unless it proves otherwise gets one local helper in waitForRunner instead of two copies. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ed516ac commit 7e5ed4d

2 files changed

Lines changed: 39 additions & 4 deletions

File tree

‎packages/platform-apple/src/runner/__tests__/runner-exchange.test.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
let server: FakeRunnerServer | undefined;
2727

2828
afterEach(async () => {
29+
vi.unstubAllGlobals();
2930
await server?.close();
3031
server = undefined;
3132
});
@@ -435,6 +436,39 @@ test('a drain whose status read fails stops at once and leaves the charge abando
435436
assert.equal(session.commandCharges.hasAbandonedCharges, true);
436437
});
437438

439+
test('a read-only command canceled in the connect retry delay withdraws its charge', async () => {
440+
const session = sessionFor(await closedLocalPort());
441+
session.state = 'starting';
442+
const controller = new AbortController();
443+
const refusedAttempts: string[] = [];
444+
vi.stubGlobal(
445+
'fetch',
446+
vi.fn(async (url: string) => {
447+
refusedAttempts.push(url);
448+
setTimeout(() => controller.abort(createRequestCanceledError()), 0);
449+
throw Object.assign(new Error('connect ECONNREFUSED'), { code: 'ECONNREFUSED' });
450+
}),
451+
);
452+
453+
await assert.rejects(
454+
executeRunnerExchange(
455+
IOS_SIMULATOR,
456+
session,
457+
{ command: 'snapshot', appBundleId: 'com.example.app' },
458+
undefined,
459+
10_000,
460+
async () => {},
461+
controller.signal,
462+
),
463+
(error: unknown) =>
464+
isRequestCanceledError(error) &&
465+
(error as { details?: { dispatched?: string } }).details?.dispatched === 'no',
466+
);
467+
468+
assert.equal(refusedAttempts.length, 1);
469+
assert.equal(session.commandCharges.hasOutstandingCharges, false);
470+
});
471+
438472
test('a refused connection withdraws its charge although nothing canceled it', async () => {
439473
const port = await closedLocalPort();
440474
const session = sessionFor(port);

‎packages/platform-apple/src/runner/runner-startup-transport.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ export async function waitForRunner(
7373
// Every attempt posts the command itself, so the loop gives up "before send" only when no
7474
// attempt could have written it.
7575
let commandMayHaveBeenWritten = false;
76+
const recordAttemptWriteEvidence = (error: unknown): void => {
77+
if (!isRunnerCommandProvablyUnwritten(error)) commandMayHaveBeenWritten = true;
78+
};
7679
const maxAttempts = Math.max(1, Math.ceil(timeoutMs / RUNNER_CONNECT_ATTEMPT_INTERVAL_MS));
7780
let answered: Response | undefined;
7881
try {
@@ -95,12 +98,10 @@ export async function waitForRunner(
9598
},
9699
setLastError: (err) => {
97100
lastError = err;
98-
if (!isRunnerCommandProvablyUnwritten(err)) commandMayHaveBeenWritten = true;
101+
recordAttemptWriteEvidence(err);
99102
},
100103
}).catch((error: unknown) => {
101-
if (isRequestCanceledError(error) && !isRunnerCommandProvablyUnwritten(error)) {
102-
commandMayHaveBeenWritten = true;
103-
}
104+
if (isRequestCanceledError(error)) recordAttemptWriteEvidence(error);
104105
throw error;
105106
});
106107
if (response) return response;

0 commit comments

Comments
 (0)