@@ -122,6 +122,118 @@ describe('build-container', () => {
122122 ) ;
123123 } ) ;
124124
125+ test ( 'removes stale running builders for the same key when the owner pid is gone' , ( ) => {
126+ const spawnSyncMock = jest
127+ . fn ( )
128+ . mockReturnValueOnce ( dockerResult ( ) )
129+ . mockReturnValueOnce ( dockerResult ( { stdout : 'stale-container\n' } ) )
130+ . mockReturnValueOnce ( dockerResult ( { stdout : '/builder-key-123\n123\n' } ) )
131+ . mockReturnValueOnce ( dockerResult ( ) )
132+ . mockReturnValueOnce ( dockerResult ( ) )
133+ . mockReturnValueOnce ( dockerResult ( { stdout : 'container-id\n' } ) )
134+ . mockReturnValueOnce ( dockerResult ( { stdout : 'running' } ) )
135+ . mockReturnValueOnce ( dockerResult ( { stdout : 'ready' } ) ) ;
136+ const processKillSpy = jest
137+ . spyOn ( process , 'kill' )
138+ . mockImplementation ( ( ( _pid : number , _signal ?: number | NodeJS . Signals ) => {
139+ const error = new Error ( 'missing process' ) as NodeJS . ErrnoException ;
140+ error . code = 'ESRCH' ;
141+ throw error ;
142+ } ) as typeof process . kill ) ;
143+
144+ const buildContainer = loadBuildContainerModule ( spawnSyncMock ) ;
145+
146+ buildContainer . ensureBuilderContainer ( {
147+ name : 'builder-key-999' ,
148+ builderKey : 'builder-key' ,
149+ args : [ 'run' , 'image' ] ,
150+ readyLog : 'ready' ,
151+ } ) ;
152+
153+ expect ( processKillSpy ) . toHaveBeenCalledWith ( 123 , 0 ) ;
154+ expect ( spawnSyncMock ) . toHaveBeenCalledWith (
155+ 'docker' ,
156+ [ 'rm' , '-f' , 'stale-container' ] ,
157+ { encoding : 'utf8' } ,
158+ ) ;
159+
160+ processKillSpy . mockRestore ( ) ;
161+ } ) ;
162+
163+ test ( 'keeps running builders for the same key when the owner pid is still alive' , ( ) => {
164+ const spawnSyncMock = jest
165+ . fn ( )
166+ . mockReturnValueOnce ( dockerResult ( ) )
167+ . mockReturnValueOnce ( dockerResult ( { stdout : 'live-container\n' } ) )
168+ . mockReturnValueOnce ( dockerResult ( { stdout : '/builder-key-456\n456\n' } ) )
169+ . mockReturnValueOnce ( dockerResult ( ) )
170+ . mockReturnValueOnce ( dockerResult ( { stdout : 'container-id\n' } ) )
171+ . mockReturnValueOnce ( dockerResult ( { stdout : 'running' } ) )
172+ . mockReturnValueOnce ( dockerResult ( { stdout : 'ready' } ) ) ;
173+ const processKillSpy = jest
174+ . spyOn ( process , 'kill' )
175+ . mockImplementation (
176+ ( ( _pid : number , _signal ?: number | NodeJS . Signals ) =>
177+ true ) as typeof process . kill ,
178+ ) ;
179+
180+ const buildContainer = loadBuildContainerModule ( spawnSyncMock ) ;
181+
182+ buildContainer . ensureBuilderContainer ( {
183+ name : 'builder-key-999' ,
184+ builderKey : 'builder-key' ,
185+ args : [ 'run' , 'image' ] ,
186+ readyLog : 'ready' ,
187+ } ) ;
188+
189+ expect ( processKillSpy ) . toHaveBeenCalledWith ( 456 , 0 ) ;
190+ expect ( spawnSyncMock ) . not . toHaveBeenCalledWith (
191+ 'docker' ,
192+ [ 'rm' , '-f' , 'live-container' ] ,
193+ { encoding : 'utf8' } ,
194+ ) ;
195+
196+ processKillSpy . mockRestore ( ) ;
197+ } ) ;
198+
199+ test ( 'falls back to the builder name pid when older containers lack an owner label' , ( ) => {
200+ const spawnSyncMock = jest
201+ . fn ( )
202+ . mockReturnValueOnce ( dockerResult ( ) )
203+ . mockReturnValueOnce ( dockerResult ( { stdout : 'stale-container\n' } ) )
204+ . mockReturnValueOnce ( dockerResult ( { stdout : '/builder-key-123\n\n' } ) )
205+ . mockReturnValueOnce ( dockerResult ( ) )
206+ . mockReturnValueOnce ( dockerResult ( ) )
207+ . mockReturnValueOnce ( dockerResult ( { stdout : 'container-id\n' } ) )
208+ . mockReturnValueOnce ( dockerResult ( { stdout : 'running' } ) )
209+ . mockReturnValueOnce ( dockerResult ( { stdout : 'ready' } ) ) ;
210+ const processKillSpy = jest
211+ . spyOn ( process , 'kill' )
212+ . mockImplementation ( ( ( _pid : number , _signal ?: number | NodeJS . Signals ) => {
213+ const error = new Error ( 'missing process' ) as NodeJS . ErrnoException ;
214+ error . code = 'ESRCH' ;
215+ throw error ;
216+ } ) as typeof process . kill ) ;
217+
218+ const buildContainer = loadBuildContainerModule ( spawnSyncMock ) ;
219+
220+ buildContainer . ensureBuilderContainer ( {
221+ name : 'builder-key-999' ,
222+ builderKey : 'builder-key' ,
223+ args : [ 'run' , 'image' ] ,
224+ readyLog : 'ready' ,
225+ } ) ;
226+
227+ expect ( processKillSpy ) . toHaveBeenCalledWith ( 123 , 0 ) ;
228+ expect ( spawnSyncMock ) . toHaveBeenCalledWith (
229+ 'docker' ,
230+ [ 'rm' , '-f' , 'stale-container' ] ,
231+ { encoding : 'utf8' } ,
232+ ) ;
233+
234+ processKillSpy . mockRestore ( ) ;
235+ } ) ;
236+
125237 test ( 'times out when the builder never becomes ready' , ( ) => {
126238 const spawnSyncMock = jest
127239 . fn ( )
@@ -190,6 +302,8 @@ describe('build-container', () => {
190302 readyLog : 'ready' ,
191303 } ) ;
192304
305+ expect ( listeners . has ( 'beforeExit' ) ) . toBe ( true ) ;
306+ expect ( listeners . has ( 'exit' ) ) . toBe ( true ) ;
193307 listeners . get ( 'SIGINT' ) ?.( ) ;
194308
195309 expect ( buildContainer . getManagedBuilderContainerNames ( ) ) . toHaveLength ( 0 ) ;
0 commit comments