Skip to content

Commit b98894b

Browse files
committed
test(apple-runner): let a real prepare deadline fail the artifact check
1 parent bcda1a4 commit b98894b

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
@@ -7,13 +7,12 @@
77
ran out and says "Runner command deadline exceeded" — neither of the two message checks on the
88
recovery paths looked for that phrasing, so the session was never restarted and the command was
99
never replayed. The rule reads the preflight marker and the recorded deadline now, so the restart
10-
happens whatever the message happens to say. The other half of this is what no longer happens: a
11-
prepare deadline, a slow boot, or a busy device no longer wipes a restored `xcodebuild` artifact on
12-
the way to a rebuild, because only a runner that refused a connection or never answered on any
13-
route says the artifact itself is at fault. A restored artifact whose runner hangs past its
14-
deadline on every attempt does not rebuild itself: the runner session is invalidated and the
15-
deadline is reported, and a rebuild needs either a failure that indicts the artifact or the runner
16-
cache cleared by hand.
10+
happens whatever the message happens to say. The other half is what no longer happens: when a slow
11+
boot spends the whole prepare budget, the health check reports "prepare ios-runner timed out", and
12+
that no longer wipes a restored `xcodebuild` artifact on the way to a rebuild — the runner session
13+
is invalidated and prepare retries with the artifact intact. Only a failure that indicts the
14+
artifact rebuilds it, so a runner that refuses the connection or never answers on any route still
15+
wipes it and rebuilds, which is what that rule is for.
1716

1817
- Changed (sessions): the implicit session is now keyed by workspace **and platform**, so one checkout
1918
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)