@@ -8,7 +8,7 @@ import { EventEmitter } from 'node:events';
88import type { PhaseContext } from '../../services/phases/types.js' ;
99import type { BuildSession } from '../../models/session.js' ;
1010
11- // Mock child_process to prevent spawning real servers and opening browser tabs
11+ // Mock child_process to prevent spawning real processes
1212vi . mock ( 'node:child_process' , async ( importOriginal ) => {
1313 const original = await importOriginal < typeof import ( 'node:child_process' ) > ( ) ;
1414 return {
@@ -20,15 +20,19 @@ vi.mock('node:child_process', async (importOriginal) => {
2020 proc . stderr = new EventEmitter ( ) ;
2121 proc . pid = 99999 ;
2222 proc . kill = vi . fn ( ) ;
23- // Simulate serve printing its listen URL on stderr (serve v14 uses stderr for info)
24- setTimeout ( ( ) => {
25- proc . stderr . emit ( 'data' , Buffer . from ( ' INFO Accepting connections at http://localhost:4567\n' ) ) ;
26- } , 50 ) ;
23+ setTimeout ( ( ) => proc . emit ( 'close' , 0 ) , 50 ) ;
2724 return proc ;
2825 } ) ,
2926 } ;
3027} ) ;
3128
29+ // Mock staticServer to prevent binding real ports
30+ vi . mock ( '../../utils/staticServer.js' , ( ) => ( {
31+ startStaticServer : vi . fn ( ( _dir : string , port : number ) =>
32+ Promise . resolve ( { url : `http://localhost:${ port } ` , close : vi . fn ( ) } ) ,
33+ ) ,
34+ } ) ) ;
35+
3236// Inline mock factories for services
3337function makeMockHardwareService ( ) {
3438 return {
@@ -196,10 +200,9 @@ describe('DeployPhase - deployWeb', () => {
196200 if ( result . process ) result . process . kill ( ) ;
197201 } ) ;
198202
199- it ( 'spawns serve with cwd and -p flag, not positional dir arg (serve v14 compat)' , async ( ) => {
200- const { spawn } = await import ( 'node:child_process' ) ;
201- // Clear mock calls from prior tests so we only see this test's spawn
202- ( spawn as any ) . mockClear ( ) ;
203+ it ( 'uses built-in static server with correct serve directory' , async ( ) => {
204+ const { startStaticServer } = await import ( '../../utils/staticServer.js' ) ;
205+ ( startStaticServer as any ) . mockClear ( ) ;
203206 const { ctx } = makeCtx ( { spec : { deployment : { target : 'web' } } } ) ;
204207 ctx . nuggetDir = tmpDir ;
205208 // Create index.html in src/ subdir so serveDir resolves to src/
@@ -209,53 +212,25 @@ describe('DeployPhase - deployWeb', () => {
209212
210213 const result = await phase . deployWeb ( ctx ) ;
211214
212- // Find the spawn call for 'npx serve'
213- const calls = ( spawn as any ) . mock . calls as any [ ] [ ] ;
214- const serveCall = calls . find (
215- ( c : any [ ] ) => c [ 0 ] === 'npx' && c [ 1 ] ?. includes ( 'serve' ) ,
216- ) ;
217- expect ( serveCall ) . toBeDefined ( ) ;
218-
219- const [ , args , opts ] = serveCall ! ;
220- // Must use -p flag for port, NOT -l with raw number
221- expect ( args ) . toContain ( '-p' ) ;
222- expect ( args ) . not . toContain ( '-l' ) ;
223- // Must NOT pass directory as positional arg (broken in serve v14)
224- expect ( args ) . not . toContain ( srcDir ) ;
225- expect ( args ) . not . toContain ( tmpDir ) ;
226- // Must use cwd to set serve directory
227- expect ( opts . cwd ) . toBe ( srcDir ) ;
228- // Must not use removed --no-clipboard flag
229- expect ( args ) . not . toContain ( '--no-clipboard' ) ;
215+ expect ( startStaticServer ) . toHaveBeenCalledOnce ( ) ;
216+ const [ dir ] = ( startStaticServer as any ) . mock . calls [ 0 ] ;
217+ expect ( dir ) . toBe ( srcDir ) ;
218+ expect ( result . url ) . toMatch ( / ^ h t t p : \/ \/ l o c a l h o s t : \d + $ / ) ;
230219
231- if ( result . process ) result . process . kill ( ) ;
220+ if ( result . staticServer ) result . staticServer . close ( ) ;
232221 } ) ;
233222
234- it ( 'uses URL from serve stdout instead of assumed port ' , async ( ) => {
223+ it ( 'emits deploy_complete with URL from static server ' , async ( ) => {
235224 const { ctx, events } = makeCtx ( { spec : { deployment : { target : 'web' } } } ) ;
236225 ctx . nuggetDir = tmpDir ;
237226
238227 const result = await phase . deployWeb ( ctx ) ;
239228
240- // The mock emits "Accepting connections at http://localhost:4567"
241- // Deploy should parse this and use port 4567, not the findFreePort result
242- const complete = events . find ( e => e . type === 'deploy_complete' ) ;
243- expect ( complete . url ) . toBe ( 'http://localhost:4567' ) ;
244- expect ( result . url ) . toBe ( 'http://localhost:4567' ) ;
245-
246- if ( result . process ) result . process . kill ( ) ;
247- } ) ;
248-
249- it ( 'does not auto-open browser after starting server' , async ( ) => {
250- const { execFile } = await import ( 'node:child_process' ) ;
251- const { ctx } = makeCtx ( { spec : { deployment : { target : 'web' } } } ) ;
252- ctx . nuggetDir = tmpDir ;
253-
254- const result = await phase . deployWeb ( ctx ) ;
255-
256- expect ( execFile ) . not . toHaveBeenCalled ( ) ;
229+ const complete = events . find ( ( e : any ) => e . type === 'deploy_complete' ) ;
230+ expect ( complete . url ) . toBeDefined ( ) ;
231+ expect ( result . url ) . toBe ( complete . url ) ;
257232
258- if ( result . process ) result . process . kill ( ) ;
233+ if ( result . staticServer ) result . staticServer . close ( ) ;
259234 } ) ;
260235
261236 it ( 'does not send deploy_checklist when no before_deploy rules exist' , async ( ) => {
0 commit comments