11import test from 'node:test' ;
22import assert from 'node:assert/strict' ;
33import fs from 'node:fs/promises' ;
4+ import net from 'node:net' ;
45import os from 'node:os' ;
56import path from 'node:path' ;
67import { fileURLToPath } from 'node:url' ;
@@ -9,7 +10,6 @@ import { NPM_CMD, PNPM_CMD, run } from '../helpers/exec.mjs';
910const ROOT = fileURLToPath ( new URL ( '../..' , import . meta. url ) ) ;
1011const ARTIFACTS_DIR = path . join ( ROOT , '.artifacts' ) ;
1112const CLI_BIN_NAME = process . platform === 'win32' ? 'coder-studio.cmd' : 'coder-studio' ;
12- const DEFAULT_TEST_PORT = 41933 ;
1313
1414async function ensureTarballs ( ) {
1515 let files = [ ] ;
@@ -60,6 +60,80 @@ async function runCli(cliPath, args, env, allowFailure = false) {
6060 }
6161}
6262
63+ async function readIfExists ( filePath ) {
64+ try {
65+ return await fs . readFile ( filePath , 'utf8' ) ;
66+ } catch {
67+ return '' ;
68+ }
69+ }
70+
71+ async function findAvailablePort ( ) {
72+ return await new Promise ( ( resolve , reject ) => {
73+ const server = net . createServer ( ) ;
74+ server . unref ( ) ;
75+ server . on ( 'error' , reject ) ;
76+ server . listen ( 0 , '127.0.0.1' , ( ) => {
77+ const address = server . address ( ) ;
78+ if ( ! address || typeof address === 'string' ) {
79+ server . close ( ( ) => reject ( new Error ( 'failed_to_resolve_ephemeral_port' ) ) ) ;
80+ return ;
81+ }
82+ const { port } = address ;
83+ server . close ( ( error ) => {
84+ if ( error ) {
85+ reject ( error ) ;
86+ return ;
87+ }
88+ resolve ( port ) ;
89+ } ) ;
90+ } ) ;
91+ } ) ;
92+ }
93+
94+ function formatCliFailure ( error , details = { } ) {
95+ const sections = [ ] ;
96+ if ( details . stdout ) sections . push ( `stdout:\n${ details . stdout . trimEnd ( ) } ` ) ;
97+ if ( details . stderr ) sections . push ( `stderr:\n${ details . stderr . trimEnd ( ) } ` ) ;
98+ if ( details . logOutput ) sections . push ( `log:\n${ details . logOutput . trimEnd ( ) } ` ) ;
99+ if ( sections . length === 0 ) {
100+ return error ;
101+ }
102+
103+ error . message = `${ error . message } \n\n${ sections . join ( '\n\n---\n\n' ) } ` ;
104+ return error ;
105+ }
106+
107+ async function startRuntimeWithRetry ( cliPath , env , stateDir , attempts = 5 ) {
108+ let lastError = null ;
109+
110+ for ( let index = 0 ; index < attempts ; index += 1 ) {
111+ const port = await findAvailablePort ( ) ;
112+ await runCli ( cliPath , [ 'config' , 'set' , 'server.port' , String ( port ) , '--json' ] , env ) ;
113+
114+ try {
115+ const start = await runCli ( cliPath , [ 'start' , '--json' ] , env ) ;
116+ return {
117+ port,
118+ result : JSON . parse ( start . stdout ) ,
119+ } ;
120+ } catch ( error ) {
121+ const stdout = String ( error . stdout ?? '' ) ;
122+ const stderr = String ( error . stderr ?? '' ) ;
123+ const logOutput = await readIfExists ( path . join ( stateDir , 'coder-studio.log' ) ) ;
124+ lastError = formatCliFailure ( error , { stdout, stderr, logOutput } ) ;
125+ const addressInUse = stdout . includes ( 'runtime_exited_early' )
126+ && logOutput . includes ( 'Address already in use' ) ;
127+ await runCli ( cliPath , [ 'stop' , '--json' ] , env , true ) ;
128+ if ( ! addressInUse ) {
129+ throw lastError ;
130+ }
131+ }
132+ }
133+
134+ throw lastError ?? new Error ( 'failed_to_start_runtime' ) ;
135+ }
136+
63137test ( 'installed package can manage runtime and auth config' , { timeout : 600000 } , async ( ) => {
64138 const tarballs = await ensureTarballs ( ) ;
65139 const tempRoot = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'coder-studio-smoke-' ) ) ;
@@ -79,9 +153,6 @@ test('installed package can manage runtime and auth config', { timeout: 600000 }
79153 const configShowResult = JSON . parse ( configShow . stdout ) ;
80154 assert . equal ( configShowResult . values [ 'server.port' ] , 41033 ) ;
81155
82- const setPort = await runCli ( cliPath , [ 'config' , 'set' , 'server.port' , String ( DEFAULT_TEST_PORT ) , '--json' ] , env ) ;
83- assert . deepEqual ( JSON . parse ( setPort . stdout ) . changedKeys , [ 'server.port' ] ) ;
84-
85156 const setRoot = await runCli ( cliPath , [ 'config' , 'root' , 'set' , accessibleRoot , '--json' ] , env ) ;
86157 assert . deepEqual ( JSON . parse ( setRoot . stdout ) . changedKeys , [ 'root.path' ] ) ;
87158
@@ -93,21 +164,20 @@ test('installed package can manage runtime and auth config', { timeout: 600000 }
93164 const validateResult = JSON . parse ( validate . stdout ) ;
94165 assert . equal ( validateResult . ok , true ) ;
95166
96- const start = await runCli ( cliPath , [ 'start' , '--json' ] , env ) ;
97- const startResult = JSON . parse ( start . stdout ) ;
167+ const { port : testPort , result : startResult } = await startRuntimeWithRetry ( cliPath , env , stateDir ) ;
98168 assert . equal ( startResult . status , 'running' ) ;
99- assert . equal ( startResult . endpoint , `http://127.0.0.1:${ DEFAULT_TEST_PORT } ` ) ;
169+ assert . equal ( startResult . endpoint , `http://127.0.0.1:${ testPort } ` ) ;
100170
101171 const status = await runCli ( cliPath , [ 'status' , '--json' ] , env ) ;
102172 const statusResult = JSON . parse ( status . stdout ) ;
103173 assert . equal ( statusResult . status , 'running' ) ;
104174 assert . equal ( statusResult . managed , true ) ;
105- assert . equal ( statusResult . endpoint , `http://127.0.0.1:${ DEFAULT_TEST_PORT } ` ) ;
175+ assert . equal ( statusResult . endpoint , `http://127.0.0.1:${ testPort } ` ) ;
106176
107177 const authStatus = await runCli ( cliPath , [ 'auth' , 'status' , '--json' ] , env ) ;
108178 const authStatusResult = JSON . parse ( authStatus . stdout ) ;
109179 assert . equal ( authStatusResult . runtimeRunning , true ) ;
110- assert . equal ( authStatusResult . server . port , DEFAULT_TEST_PORT ) ;
180+ assert . equal ( authStatusResult . server . port , testPort ) ;
111181 assert . equal ( authStatusResult . root . path , accessibleRoot ) ;
112182 assert . equal ( authStatusResult . auth . passwordConfigured , true ) ;
113183
0 commit comments