From 75b8383b6aace810a8988ec7002969a1da95cc83 Mon Sep 17 00:00:00 2001 From: Rada Kamysheva Date: Thu, 6 Aug 2026 14:13:20 +0000 Subject: [PATCH] acc: run cmd/workspace/apps/run-local-node locally The test has been disabled since #4875, which took NPM registry access away from the runners. It needed the registry only for the app fixture: express, and the --prepare-environment run that installs it. The fixture is now a standard-library HTTP server started directly with node, so nothing is installed at test time. /shutdown answers with Connection: close so server.close() can complete and the app exits on its own, which lets wait $PID assert that the CLI exited zero. The app throws when PORT is unset rather than letting listen() pick a random port and failing fifteen seconds later as a proxy timeout; PORT is read in preference to DATABRICKS_APP_PORT because it is what Node apps read and what GetBaseEnvVars sets for that reason, so it covers an alias the python fixture cannot. Ports come from free_port.py, and the Timeout overrides go away because the run takes a few seconds rather than 2m. --prepare-environment comes back with it. The flag had no test left anywhere: the python sibling dropped it for needing PyPI, and neither PrepareEnvironment has a unit test. npm install only reaches the registry when there is something to install, so the fixture keeps an empty dependency list and gains a build script, and a single run covers both halves of NodeApp.PrepareEnvironment: the install has to succeed for the build script to run at all. The build script is a file rather than an inline command because npm's shell quotes inline arguments differently on Windows, and the assertion greps for the exact output. With nothing to install the one request npm still makes on its own is the check for a newer npm, which the rejecting proxy blocks, so app/.npmrc turns that check off. --debug stays too, unlike in the python sibling where it needs debugpy from PyPI: node debugging is only NODE_OPTIONS=--inspect. The test asserts both the port the CLI reports and that node listens on it, so --debug-port keeps its coverage. The CLI prints its line only after the proxy accepts connections, so the curl that reaches the app can beat it and that grep is retried. A failed run reaps the app, proxy and debug ports. node binds the inspector port before it runs app.js, so the debug port is the only handle on an app that died before it listened. --- .../workspace/apps/run-local-node/app/.npmrc | 3 + .../workspace/apps/run-local-node/app/app.js | 41 +++++----- .../workspace/apps/run-local-node/app/app.yml | 5 +- .../apps/run-local-node/app/build.js | 3 + .../apps/run-local-node/app/package.json | 7 +- .../apps/run-local-node/out.test.toml | 2 +- .../workspace/apps/run-local-node/output.txt | 32 ++++++-- .../cmd/workspace/apps/run-local-node/script | 75 +++++++++---------- .../workspace/apps/run-local-node/test.toml | 23 ++---- 9 files changed, 93 insertions(+), 98 deletions(-) create mode 100644 acceptance/cmd/workspace/apps/run-local-node/app/.npmrc create mode 100644 acceptance/cmd/workspace/apps/run-local-node/app/build.js diff --git a/acceptance/cmd/workspace/apps/run-local-node/app/.npmrc b/acceptance/cmd/workspace/apps/run-local-node/app/.npmrc new file mode 100644 index 00000000000..c43162960a8 --- /dev/null +++ b/acceptance/cmd/workspace/apps/run-local-node/app/.npmrc @@ -0,0 +1,3 @@ +; With nothing to install, the one thing npm still reaches the registry for is its check for +; a newer npm, and the acceptance proxy rejects that. The install and the audit stay local. +update-notifier=false diff --git a/acceptance/cmd/workspace/apps/run-local-node/app/app.js b/acceptance/cmd/workspace/apps/run-local-node/app/app.js index 74686d09a82..4e47e5c9cda 100644 --- a/acceptance/cmd/workspace/apps/run-local-node/app/app.js +++ b/acceptance/cmd/workspace/apps/run-local-node/app/app.js @@ -1,27 +1,26 @@ -const express = require('express'); -const app = express(); -const port = process.env.PORT || 8000; +const http = require('node:http'); -// Root route -app.get('/', (req, res) => { - res.json({ - message: 'Hello From App', - timestamp: new Date().toISOString(), - status: 'running' - }); -}); +// Standard library only: acceptance tests run with the network disabled, so the +// app cannot install anything from the NPM registry. + +// The CLI sets PORT alongside DATABRICKS_APP_PORT. Unset, listen() would pick a random port +// and the run would fail as a proxy timeout instead. +const port = process.env.PORT; +if (!port) throw new Error('PORT is not set'); -app.get('/shutdown', (req, res) => { - console.log('Server closed') - // Add a small delay to ensure response is sent before exit - setTimeout(() => { - process.exit(0); - }, 1000); +const server = http.createServer((req, res) => { + if (req.url === '/shutdown') { + // close() waits for open connections, so the response must end this one or the proxy holds it. + res.setHeader('Connection', 'close'); + res.end(); + server.close(); + return; + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ message: 'Hello From App' }) + '\n'); }); -// Start the server -app.listen(port, () => { +server.listen(port, '127.0.0.1', () => { console.log(`Server is running on port ${port}`); }); - -module.exports = app; diff --git a/acceptance/cmd/workspace/apps/run-local-node/app/app.yml b/acceptance/cmd/workspace/apps/run-local-node/app/app.yml index bc02e198203..8acf26d0927 100644 --- a/acceptance/cmd/workspace/apps/run-local-node/app/app.yml +++ b/acceptance/cmd/workspace/apps/run-local-node/app/app.yml @@ -1,4 +1,3 @@ command: - - npm - - run - - run-app + - node + - app.js diff --git a/acceptance/cmd/workspace/apps/run-local-node/app/build.js b/acceptance/cmd/workspace/apps/run-local-node/app/build.js new file mode 100644 index 00000000000..58421926421 --- /dev/null +++ b/acceptance/cmd/workspace/apps/run-local-node/app/build.js @@ -0,0 +1,3 @@ +// A file rather than an inline command in package.json, so the asserted output does +// not depend on how npm's shell quotes it, which differs on Windows. +console.log('Build script ran'); diff --git a/acceptance/cmd/workspace/apps/run-local-node/app/package.json b/acceptance/cmd/workspace/apps/run-local-node/app/package.json index 15e06e17ffb..e3b250f18dc 100644 --- a/acceptance/cmd/workspace/apps/run-local-node/app/package.json +++ b/acceptance/cmd/workspace/apps/run-local-node/app/package.json @@ -2,12 +2,7 @@ "name": "app", "version": "1.0.0", "description": "A simple Node.js app", - "main": "app.js", "scripts": { - "run-app": "node app.js", - "build": "echo 'Building app...'" - }, - "dependencies": { - "express": "^5.1.0" + "build": "node build.js" } } diff --git a/acceptance/cmd/workspace/apps/run-local-node/out.test.toml b/acceptance/cmd/workspace/apps/run-local-node/out.test.toml index 3ef9121aba6..e90b6d5d1ba 100644 --- a/acceptance/cmd/workspace/apps/run-local-node/out.test.toml +++ b/acceptance/cmd/workspace/apps/run-local-node/out.test.toml @@ -1,3 +1,3 @@ -Local = false +Local = true Cloud = false EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/cmd/workspace/apps/run-local-node/output.txt b/acceptance/cmd/workspace/apps/run-local-node/output.txt index 0185dbe5234..e6b0bd0a642 100644 --- a/acceptance/cmd/workspace/apps/run-local-node/output.txt +++ b/acceptance/cmd/workspace/apps/run-local-node/output.txt @@ -1,12 +1,28 @@ + +=== Prepare environment runs the build script, and the entry point overrides app.yml + +>>> grep -x Build script ran ../LOG.prepare +Build script ran + +>>> grep -x Running command: node -e console.log('Hello, world') ../LOG.prepare Running command: node -e console.log('Hello, world') + +>>> grep -x Hello, world ../LOG.prepare Hello, world -=== Starting the app in background... -=== Waiting -=== Checking app is running... ->>> curl -s -o - http://127.0.0.1:$(port) -{"message":"Hello From App","timestamp":"[TIMESTAMP]","status":"running"} +=== Proxy forwards to the app + +>>> retry --until Hello From App curl -sS http://127.0.0.1:[PROXY_PORT] +{"message":"Hello From App"} + +=== Debug mode reports the debugger port, and node listens on it + +>>> retry grep -x To debug your app, attach a debugger to port [DEBUG_PORT] LOG.run +To debug your app, attach a debugger to port [DEBUG_PORT] + +>>> grep Debugger listening on ws://127.0.0.1:[DEBUG_PORT]/ LOG.run +Debugger listening on ws://127.0.0.1:[DEBUG_PORT]/[UUID] + +=== Shutting the app down -=== Sending shutdown request... ->>> curl -s -o /dev/null http://127.0.0.1:$(port)/shutdown -Process terminated +>>> curl -sS -o /dev/null http://127.0.0.1:[PROXY_PORT]/shutdown diff --git a/acceptance/cmd/workspace/apps/run-local-node/script b/acceptance/cmd/workspace/apps/run-local-node/script index 3701e6fd697..58a139975b5 100755 --- a/acceptance/cmd/workspace/apps/run-local-node/script +++ b/acceptance/cmd/workspace/apps/run-local-node/script @@ -1,52 +1,45 @@ cd app -# We first run the command with different entry point which starts unblocking script -# so we don't need to start it in background. It will install the dependencies as part of the command -trace $CLI apps run-local --prepare-environment --entry-point test.yml 2>&1 | grep -w "Hello, world" - -read -r PORT DEBUG_PORT PROXY_PORT <<< "$(free_port.py 3)" +# Ports are allocated per run so that tests in parallel worktrees do not collide. +read -r APP_PORT PROXY_PORT DEBUG_PORT PREPARE_PORT <<< "$(free_port.py 4)" +add_repl.py "$PROXY_PORT" PROXY_PORT +add_repl.py "$DEBUG_PORT" DEBUG_PORT + +# The app and the proxy start asynchronously; allow up to 15s for them to come up. +export RETRY_MAX_ATTEMPTS=30 + +title "Prepare environment runs the build script, and the entry point overrides app.yml\n" +# npm reaches no registry here: nothing to install, and app/.npmrc turns off its update check. +# The build script runs only if the install succeeded, so the first grep covers both halves. +trace $CLI apps run-local --prepare-environment --entry-point test.yml --port "$PREPARE_PORT" &> ../LOG.prepare +trace grep -x "Build script ran" ../LOG.prepare +trace grep -x "Running command: node -e console.log('Hello, world')" ../LOG.prepare +trace grep -x "Hello, world" ../LOG.prepare cleanup() { - # Kill any still running processes on these ports when the script exits - kill_port.py $PORT $DEBUG_PORT $PROXY_PORT + local status=$? + # Reap by port rather than by $PID, and only on failure, for the reasons in + # ../run-local/script. The debug port is here too because node binds the inspector before + # it runs app.js, so it is the only handle on an app that died before it listened. + [ "$status" -eq 0 ] || kill_port.py "$APP_PORT" "$PROXY_PORT" "$DEBUG_PORT" } -trap cleanup EXIT - -title "Starting the app in background..." -trace $CLI apps run-local --prepare-environment --debug --port "$PROXY_PORT" --debug-port "$DEBUG_PORT" --app-port "$PORT" > ../out.run.txt 2>&1 & +# app/package.json selects the CLI's Node code path (runlocal.NewApp); without it the app is +# treated as Python and --debug no longer sets NODE_OPTIONS. +trace $CLI apps run-local --debug --port "$PROXY_PORT" --debug-port "$DEBUG_PORT" --app-port "$APP_PORT" &> ../LOG.run & PID=$! -# Ensure background process is killed on script exit -trap '(kill $PID >/dev/null 2>&1) 2>/dev/null || true' EXIT +trap cleanup EXIT cd .. -title Waiting for the app to start... -# Use a loop to check for the startup message instead of tail/sed which can be unreliable on Windows -# due to file locking, buffering issues, and different text processing behavior across Windows versions. -# A simple grep loop is more robust across platforms. -while [ -z "$(grep -o "Server is running on port " out.run.txt 2>/dev/null)" ]; do - sleep 1 -done - -# Make sure the proxy is ready to serve requests -while [ -z "$(grep -o "To access your app go to " out.run.txt 2>/dev/null)" ]; do - sleep 1 -done - -title "Checking app is running..." -trace curl -s -o - http://127.0.0.1:$PROXY_PORT | grep -w "Hello From App" - -title "Sending shutdown request..." -trace curl -s -o /dev/null http://127.0.0.1:$PROXY_PORT/shutdown || true - -# We need to wait for the app to shutdown before we can exit the test meaning wait until the -# server is closed. We need to poll because the server is closed asynchronously. -while [ -z "$(grep -o "Server closed" out.run.txt 2>/dev/null)" ]; do - sleep 1 -done +title "Proxy forwards to the app\n" +trace retry --until "Hello From App" curl -sS "http://127.0.0.1:$PROXY_PORT" -# Wait for the background process to actually terminate -wait $PID 2>/dev/null || true -echo "Process terminated" +title "Debug mode reports the debugger port, and node listens on it\n" +# The CLI prints this only after the proxy accepts connections, so the curl above can beat it. +# node's line below needs no retry: it is written before the app listens, so the curl proves it. +trace retry grep -x "To debug your app, attach a debugger to port $DEBUG_PORT" LOG.run +trace grep "Debugger listening on ws://127.0.0.1:$DEBUG_PORT/" LOG.run -rm out.run.txt +title "Shutting the app down\n" +trace curl -sS -o /dev/null "http://127.0.0.1:$PROXY_PORT/shutdown" +wait $PID diff --git a/acceptance/cmd/workspace/apps/run-local-node/test.toml b/acceptance/cmd/workspace/apps/run-local-node/test.toml index 2b6bb4bdda4..023a62e4f8c 100644 --- a/acceptance/cmd/workspace/apps/run-local-node/test.toml +++ b/acceptance/cmd/workspace/apps/run-local-node/test.toml @@ -1,26 +1,13 @@ -Badness = "need to enable NPM registry access" Cloud = false -Local = false +Local = true RecordRequests = false -Timeout = '2m' -TimeoutWindows = '10m' +# npm install writes package-lock.json even with nothing to install, and older npm versions +# also create node_modules for their own copy of the lockfile. Ignore = [ 'node_modules', 'package-lock.json' ] -[[Repls]] -Old='curl/[0-9]+\.[0-9]+\.[0-9]+' -New='curl/(version)' - -[[Repls]] -Old='127.0.0.1:[0-9]+' -New='127.0.0.1:$(port)' - -[[Repls]] -Old='To debug your app, attach a debugger to port [0-9]+' -New='To debug your app, attach a debugger to port $(debug_port)' - -[EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["direct"] +# The command is unrelated to bundle deployment, so run it once rather than per engine. +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]