@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
22import { beforeEach , test , vi } from 'vitest' ;
33import { AppError } from '@agent-device/kernel/errors' ;
44import { appleRunnerTestHost } from '../test-host.ts' ;
5+ import { Deadline } from '../host.ts' ;
56import type { RunnerXctestrunArtifact } from '../runner-xctestrun.ts' ;
67import { IOS_SIMULATOR } from './device-fixtures.ts' ;
78import { 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