Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 68 additions & 7 deletions scripts/ci/dev-up.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,19 @@ recordLivePid()

const stopWithServer = (server) => {
const stopDelayMs = Number(process.env.FAKE_STOP_DELAY_MS ?? 0)
const stop = () => server.close(() => {
if (Number.isFinite(stopDelayMs) && stopDelayMs > 0) {
setTimeout(() => process.exit(0), stopDelayMs)
return
}
process.exit(0)
})
const stop = () => {
// server.close waits for active connections. A browser or health probe can keep one open
// forever, so close the accepted HTTP connections after stopping new accepts; otherwise TERM
// leaves this fixture alive until the launcher kills it.
server.close(() => {
if (Number.isFinite(stopDelayMs) && stopDelayMs > 0) {
setTimeout(() => process.exit(0), stopDelayMs)
return
}
process.exit(0)
})
server.closeAllConnections()
}
process.on('SIGTERM', stop)
process.on('SIGINT', stop)
}
Expand Down Expand Up @@ -1027,8 +1033,63 @@ test('launchers encode the transactional lifecycle and custom-port environment b
assert.match(sh, /redirect: "manual"/)
assert.match(sh, /r\.headers\.get\("taskdeck-dev-run-id"\) === expectedRunId/)
assert.doesNotMatch(sh, /export (?:TASKDECK_API_BASE_URL|VITE_API_BASE_URL)/)

const stopWithServerStart = helperSource.indexOf('const stopWithServer = (server) => {')
const stopWithServerEnd = helperSource.indexOf('\n\nif (kind ===', stopWithServerStart)
const stopWithServerSource = helperSource.slice(stopWithServerStart, stopWithServerEnd)
assert.match(stopWithServerSource, /server\.close\(\(\) => \{/)
assert.match(stopWithServerSource, /server\.closeAllConnections\(\)/)
assert.ok(
stopWithServerSource.indexOf('server.close(() => {') < stopWithServerSource.indexOf('server.closeAllConnections()'),
'stop must stop new accepts before closing active connections',
)
})

if (process.platform !== 'win32') {
test(
'Node helper: TERM closes an active frontend connection',
{ concurrency: false, timeout: 10_000 },
async () => {
const platform = { name: 'Bash', launcher: 'dev-up.sh' }
const fixture = await createFixture(platform)
const frontendPort = await getFreePort()
const child = spawn(process.execPath, [fixture.helper, 'npm', 'run', 'dev'], {
cwd: fixture.root,
stdio: 'ignore',
env: fixtureEnvironment(platform, fixture, { FAKE_FRONTEND_PORT: String(frontendPort) }),
})
let socket
try {
await waitUntil(() => canBind(frontendPort, 'localhost').then((available) => !available), 'frontend helper did not bind')
socket = net.createConnection({ host: 'localhost', port: frontendPort })
await new Promise((resolve, reject) => {
socket.once('connect', resolve)
socket.once('error', reject)
})

const exitResult = new Promise((resolve) => {
const timer = setTimeout(() => resolve(null), 3000)
child.once('exit', (code, signal) => {
clearTimeout(timer)
resolve({ code, signal })
})
})
assert.equal(child.kill('SIGTERM'), true)
const result = await exitResult
assert.ok(result, 'frontend helper did not exit after TERM with an open connection')
assert.deepEqual(result, { code: 0, signal: null })
} finally {
socket?.destroy()
if (child.exitCode === null && child.signalCode === null) {
child.kill('SIGKILL')
await new Promise((resolve) => child.once('exit', resolve))
}
await removeFixture(fixture)
}
},
)
}

for (const platform of platforms) {
test(`${platform.name}: reset seed option is rejected before launcher side effects without seed`, { concurrency: false }, async () => {
const fixture = await createFixture(platform)
Expand Down
Loading