@@ -118,6 +118,11 @@ const resolveWebHostSource = extractFunctionBySignature(
118118 'function resolveWebHost(options = {}) {' ,
119119 'resolveWebHost'
120120) ;
121+ const releaseRunPortIfNeededSource = extractFunctionBySignature (
122+ cliContent ,
123+ 'function releaseRunPortIfNeeded(port, deps = {}) {' ,
124+ 'releaseRunPortIfNeeded'
125+ ) ;
121126const resolveWebHost = instantiateFunction ( resolveWebHostSource , 'resolveWebHost' , {
122127 DEFAULT_WEB_HOST : defaultHostMatch [ 1 ] ,
123128 process : { env : { } }
@@ -153,6 +158,149 @@ test('web auto-open uses IPv6 loopback when binding to IPv6 any address', () =>
153158 ) ;
154159} ) ;
155160
161+ test ( 'releaseRunPortIfNeeded skips non-default ports' , ( ) => {
162+ const calls = [ ] ;
163+ const releaseRunPortIfNeeded = instantiateFunction ( releaseRunPortIfNeededSource , 'releaseRunPortIfNeeded' , {
164+ DEFAULT_WEB_PORT : 3737 ,
165+ spawnSync ( command , args ) {
166+ calls . push ( [ command , args ] ) ;
167+ return { status : 0 , stdout : '' , stderr : '' } ;
168+ } ,
169+ process : { platform : 'linux' } ,
170+ console : { log ( ) { } }
171+ } ) ;
172+
173+ const result = releaseRunPortIfNeeded ( 3999 ) ;
174+ assert . deepStrictEqual ( result , {
175+ attempted : false ,
176+ released : false ,
177+ pids : [ ] ,
178+ reason : 'non-default-port'
179+ } ) ;
180+ assert . deepStrictEqual ( calls , [ ] ) ;
181+ } ) ;
182+
183+ test ( 'releaseRunPortIfNeeded clears default port via fuser on linux' , ( ) => {
184+ const calls = [ ] ;
185+ const logs = [ ] ;
186+ const releaseRunPortIfNeeded = instantiateFunction ( releaseRunPortIfNeededSource , 'releaseRunPortIfNeeded' , {
187+ DEFAULT_WEB_PORT : 3737 ,
188+ spawnSync ( command , args ) {
189+ calls . push ( [ command , args ] ) ;
190+ if ( command === 'fuser' ) {
191+ return { status : 0 , stdout : '1234\n' , stderr : '' } ;
192+ }
193+ throw new Error ( `unexpected command: ${ command } ` ) ;
194+ } ,
195+ process : { platform : 'linux' } ,
196+ console : { log ( message ) { logs . push ( message ) ; } }
197+ } ) ;
198+
199+ const result = releaseRunPortIfNeeded ( 3737 ) ;
200+ assert . deepStrictEqual ( calls , [ [ 'fuser' , [ '-k' , '3737/tcp' ] ] ] ) ;
201+ assert . deepStrictEqual ( result , {
202+ attempted : true ,
203+ released : true ,
204+ pids : [ 1234 ]
205+ } ) ;
206+ assert . deepStrictEqual ( logs , [ '~ 已释放端口 3737 占用' ] ) ;
207+ } ) ;
208+
209+ test ( 'releaseRunPortIfNeeded falls back to lsof pids when fuser is unavailable' , ( ) => {
210+ const calls = [ ] ;
211+ const killed = [ ] ;
212+ const releaseRunPortIfNeeded = instantiateFunction ( releaseRunPortIfNeededSource , 'releaseRunPortIfNeeded' , {
213+ DEFAULT_WEB_PORT : 3737 ,
214+ spawnSync ( command , args ) {
215+ calls . push ( [ command , args ] ) ;
216+ if ( command === 'fuser' ) {
217+ return { error : { code : 'ENOENT' } , status : null , stdout : '' , stderr : '' } ;
218+ }
219+ if ( command === 'lsof' ) {
220+ return { status : 0 , stdout : '2222\n3333\n' , stderr : '' } ;
221+ }
222+ throw new Error ( `unexpected command: ${ command } ` ) ;
223+ } ,
224+ process : {
225+ platform : 'linux' ,
226+ kill ( pid , signal ) {
227+ killed . push ( [ pid , signal ] ) ;
228+ }
229+ } ,
230+ console : { log ( ) { } }
231+ } ) ;
232+
233+ const result = releaseRunPortIfNeeded ( 3737 ) ;
234+ assert . deepStrictEqual ( calls , [
235+ [ 'fuser' , [ '-k' , '3737/tcp' ] ] ,
236+ [ 'lsof' , [ '-ti' , 'tcp:3737' ] ]
237+ ] ) ;
238+ assert . deepStrictEqual ( killed , [
239+ [ 2222 , 'SIGKILL' ] ,
240+ [ 3333 , 'SIGKILL' ]
241+ ] ) ;
242+ assert . deepStrictEqual ( result , {
243+ attempted : true ,
244+ released : true ,
245+ pids : [ 2222 , 3333 ]
246+ } ) ;
247+ } ) ;
248+
249+ test ( 'releaseRunPortIfNeeded falls back to ps scan for managed run processes' , ( ) => {
250+ const calls = [ ] ;
251+ const killed = [ ] ;
252+ const releaseRunPortIfNeeded = instantiateFunction ( releaseRunPortIfNeededSource , 'releaseRunPortIfNeeded' , {
253+ DEFAULT_WEB_PORT : 3737 ,
254+ spawnSync ( command , args ) {
255+ calls . push ( [ command , args ] ) ;
256+ if ( command === 'fuser' ) {
257+ return { error : { code : 'EACCES' } , status : 1 , stdout : '' , stderr : 'Permission denied' } ;
258+ }
259+ if ( command === 'lsof' ) {
260+ return { error : { code : 'ENOENT' } , status : null , stdout : '' , stderr : '' } ;
261+ }
262+ if ( command === 'ps' ) {
263+ return {
264+ status : 0 ,
265+ stdout : [
266+ 'UID PID PPID C STIME TTY TIME CMD' ,
267+ 'u0_a876 9001 1000 0 1970 ? 00:00:00 node /repo/cli.js run --no-browser' ,
268+ 'u0_a876 9002 1000 0 1970 ? 00:00:00 /usr/bin/codexmate run' ,
269+ 'u0_a876 9100 1000 0 1970 ? 00:00:00 node /repo/cli.js config'
270+ ] . join ( '\n' ) ,
271+ stderr : ''
272+ } ;
273+ }
274+ throw new Error ( `unexpected command: ${ command } ` ) ;
275+ } ,
276+ process : {
277+ platform : 'linux' ,
278+ pid : 9002 ,
279+ kill ( pid , signal ) {
280+ killed . push ( [ pid , signal ] ) ;
281+ }
282+ } ,
283+ console : { log ( ) { } }
284+ } ) ;
285+
286+ const result = releaseRunPortIfNeeded ( 3737 ) ;
287+ assert . deepStrictEqual ( calls , [
288+ [ 'fuser' , [ '-k' , '3737/tcp' ] ] ,
289+ [ 'lsof' , [ '-ti' , 'tcp:3737' ] ] ,
290+ [ 'ps' , [ '-ef' ] ]
291+ ] ) ;
292+ assert . deepStrictEqual ( killed , [ [ 9001 , 'SIGKILL' ] ] ) ;
293+ assert . deepStrictEqual ( result , {
294+ attempted : true ,
295+ released : true ,
296+ pids : [ 9001 ]
297+ } ) ;
298+ } ) ;
299+
300+ test ( 'cmdStart releases the resolved port before creating the web server' , ( ) => {
301+ assert . match ( cliContent , / c o n s t p o r t = r e s o l v e W e b P o r t \( \) ; \s * c o n s t h o s t = r e s o l v e W e b H o s t \( o p t i o n s \) ; \s * r e l e a s e R u n P o r t I f N e e d e d \( p o r t \) ; \s * l e t s e r v e r H a n d l e = c r e a t e W e b S e r v e r \( / s) ;
302+ } ) ;
303+
156304const getCodexSkillsDirSource = extractFunctionBySignature (
157305 cliContent ,
158306 'function getCodexSkillsDir() {' ,
0 commit comments