Skip to content

Commit 553610f

Browse files
committed
test(apple-runner): let a real prepare deadline fail the artifact check
1 parent 8ba425a commit 553610f

2 files changed

Lines changed: 46 additions & 40 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,12 @@
1919
ran out and says "Runner command deadline exceeded" — neither of the two message checks on the
2020
recovery paths looked for that phrasing, so the session was never restarted and the command was
2121
never replayed. The rule reads the preflight marker and the recorded deadline now, so the restart
22-
happens whatever the message happens to say. The other half of this is what no longer happens: a
23-
prepare deadline, a slow boot, or a busy device no longer wipes a restored `xcodebuild` artifact on
24-
the way to a rebuild, because only a runner that refused a connection or never answered on any
25-
route says the artifact itself is at fault. A restored artifact whose runner hangs past its
26-
deadline on every attempt does not rebuild itself: the runner session is invalidated and the
27-
deadline is reported, and a rebuild needs either a failure that indicts the artifact or the runner
28-
cache cleared by hand.
22+
happens whatever the message happens to say. The other half is what no longer happens: when a slow
23+
boot spends the whole prepare budget, the health check reports "prepare ios-runner timed out", and
24+
that no longer wipes a restored `xcodebuild` artifact on the way to a rebuild — the runner session
25+
is invalidated and prepare retries with the artifact intact. Only a failure that indicts the
26+
artifact rebuilds it, so a runner that refuses the connection or never answers on any route still
27+
wipes it and rebuilds, which is what that rule is for.
2928

3029
- Changed (sessions): the implicit session is now keyed by workspace **and platform**, so one checkout
3130
can drive iOS and Android without inventing a `--session` name for every command (#2580). An

packages/platform-apple/src/runner/__tests__/runner-lifecycle-prepare-artifact.test.ts

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
22
import { beforeEach, test, vi } from 'vitest';
33
import { AppError } from '@agent-device/kernel/errors';
44
import { appleRunnerTestHost } from '../test-host.ts';
5+
import { Deadline } from '../host.ts';
56
import type { RunnerXctestrunArtifact } from '../runner-xctestrun.ts';
67
import { IOS_SIMULATOR } from './device-fixtures.ts';
78
import { createTestRequestCancellation, makeRunnerSession } from './runner-session-fixtures.ts';
@@ -76,41 +77,47 @@ beforeEach(() => {
7677
});
7778
});
7879

79-
// What a prepare deadline does to a restored artifact. The wipe that rebuilds a suspect artifact
80-
// comes from the rules that indict the artifact itself; a runner that never answers inside its
81-
// budget indicts the boot, not the derived data it was launched from, so the artifact stays and
82-
// the session goes. The error is the one `ensureRunnerAttemptCanStart` reports when the startup
83-
// attempt is already out of time: "Runner connection deadline exceeded" is what a real prepare
84-
// deadline looks like, and the word in it is exactly what the deleted message check matched.
80+
// What a spent prepare deadline does to a restored artifact. Prepare spends one `Deadline` across
81+
// boot and health check, so the failure this decision actually sees is the one
82+
// `readPreparePhaseTimeoutMs` raises when the boot ate the budget: "prepare ios-runner timed out"
83+
// with reason `prepare_deadline_expired`. That indicts the boot, not the derived data it was
84+
// launched from, so the artifact stays and prepare retries with a fresh session. The artifact is
85+
// only wiped by the rules that indict it, such as a runner that refused the connection.
8586

86-
test('a restored artifact whose runner outlives the prepare deadline is kept while the session goes', async () => {
87-
const restoredSession = makeRunnerSession({
88-
port: 8100,
89-
xctestrunPath: '/tmp/restored.xctestrun',
90-
xctestrunArtifact: makeRunnerArtifact({ xctestrunPath: '/tmp/restored.xctestrun' }),
91-
});
92-
93-
mockEnsureRunnerSession.mockResolvedValue(restoredSession);
94-
mockExecuteRunnerCommandWithSession.mockRejectedValue(
95-
new AppError('COMMAND_FAILED', 'Runner connection deadline exceeded', {
87+
test('a prepare deadline spent during boot keeps the restored artifact and retries', async () => {
88+
vi.useFakeTimers();
89+
try {
90+
vi.setSystemTime(1_000);
91+
const restoredSession = makeRunnerSession({
9692
port: 8100,
97-
timeoutMs: 45_000,
98-
}),
99-
);
93+
xctestrunPath: '/tmp/restored.xctestrun',
94+
xctestrunArtifact: makeRunnerArtifact({ xctestrunPath: '/tmp/restored.xctestrun' }),
95+
});
96+
const prepareDeadline = Deadline.fromTimeoutMs(45_000);
97+
98+
mockEnsureRunnerSession.mockImplementation(async () => {
99+
// The boot consumed the whole prepare budget, so no health phase time remains.
100+
vi.setSystemTime(46_000);
101+
return restoredSession;
102+
});
100103

101-
await assert.rejects(
102-
() => prepareIosRunner(IOS_SIMULATOR, { healthTimeoutMs: 90_000 }),
103-
(error: unknown) => {
104-
assert.ok(error instanceof AppError);
105-
assert.equal(error.message, 'Runner connection deadline exceeded');
106-
return true;
107-
},
108-
);
104+
await assert.rejects(
105+
() => prepareIosRunner(IOS_SIMULATOR, { healthTimeoutMs: 90_000, prepareDeadline }),
106+
(error: unknown) => {
107+
assert.ok(error instanceof AppError);
108+
assert.equal(error.message, 'prepare ios-runner timed out');
109+
assert.equal(error.details?.reason, 'prepare_deadline_expired');
110+
assert.equal(error.details?.phase, 'runner_session');
111+
return true;
112+
},
113+
);
109114

110-
assert.equal(mockMarkRunnerXctestrunArtifactBadForRun.mock.calls.length, 0);
111-
assert.equal(mockEnsureRunnerSession.mock.calls.length, 2);
112-
assert.deepEqual(mockInvalidateRunnerSession.mock.calls.at(-1), [
113-
restoredSession,
114-
'prepare_runner_health_failed',
115-
]);
115+
assert.equal(mockMarkRunnerXctestrunArtifactBadForRun.mock.calls.length, 0);
116+
assert.deepEqual(mockInvalidateRunnerSession.mock.calls.at(-1), [
117+
restoredSession,
118+
'prepare_runner_health_retry',
119+
]);
120+
} finally {
121+
vi.useRealTimers();
122+
}
116123
});

0 commit comments

Comments
 (0)