Skip to content

Commit 31abc9c

Browse files
committed
test: stabilize smoke runtime startup
1 parent 53e0948 commit 31abc9c

1 file changed

Lines changed: 79 additions & 9 deletions

File tree

tests/smoke/cli-smoke.test.mjs

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import test from 'node:test';
22
import assert from 'node:assert/strict';
33
import fs from 'node:fs/promises';
4+
import net from 'node:net';
45
import os from 'node:os';
56
import path from 'node:path';
67
import { fileURLToPath } from 'node:url';
@@ -9,7 +10,6 @@ import { NPM_CMD, PNPM_CMD, run } from '../helpers/exec.mjs';
910
const ROOT = fileURLToPath(new URL('../..', import.meta.url));
1011
const ARTIFACTS_DIR = path.join(ROOT, '.artifacts');
1112
const CLI_BIN_NAME = process.platform === 'win32' ? 'coder-studio.cmd' : 'coder-studio';
12-
const DEFAULT_TEST_PORT = 41933;
1313

1414
async 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+
63137
test('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

Comments
 (0)