Skip to content
Open
Show file tree
Hide file tree
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
62 changes: 62 additions & 0 deletions tests/test_litellm/ui/test_ui_build_pinning_lit_2723.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""
Regression tests for LIT-2723 - pin the UI build output layout and Node version
so committed `litellm/proxy/_experimental/out/` diffs stop flipping between
`<route>.html` and `<route>/index.html`, and so the build does not silently
fall back to a non-v20 Node.

These are file-level assertions on the build config + script. They do not run
npm or restructure the export; they only guard the locking lines.
"""

from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parents[3]
NEXT_CONFIG = REPO_ROOT / "ui" / "litellm-dashboard" / "next.config.mjs"
BUILD_UI_SH = REPO_ROOT / "ui" / "litellm-dashboard" / "build_ui.sh"


def test_next_config_pins_trailing_slash_true():
"""next.config.mjs must declare `trailingSlash: true` so every build emits
the `<route>/index.html` directory-index layout. Flipping back to the
`<route>.html` form rewrites every file in `_experimental/out` and breaks
deployments that rely on directory-index routing (e.g. the MCP OAuth
callback)."""
assert NEXT_CONFIG.is_file(), f"next.config.mjs missing at {NEXT_CONFIG}"
text = NEXT_CONFIG.read_text()
assert "trailingSlash: true" in text or "trailingSlash:true" in text, (
"next.config.mjs must set `trailingSlash: true` (LIT-2723). "
"Removing it makes the static export flip back to the bare "
"<route>.html form on the next local build."
)


def test_build_ui_sh_installs_node_v20_and_verifies():
"""build_ui.sh must install v20 (not just `nvm use`), error-check both
steps, and verify `node -v` actually reports v20 before running the
build. Without these guards the script silently succeeds against any
Node version on PATH in non-interactive shells, and the committed
artifacts pick up the wrong Node."""
assert BUILD_UI_SH.is_file(), f"build_ui.sh missing at {BUILD_UI_SH}"
text = BUILD_UI_SH.read_text()

assert "nvm install v20" in text, (
"build_ui.sh must `nvm install v20` before `nvm use v20` - "
"`use` alone silently no-ops when v20 is not already installed."
)
assert "nvm install v20 failed" in text, (
"build_ui.sh must surface `nvm install v20` failure with an "
"error message and non-zero exit (LIT-2723)."
)
assert "Failed to switch to Node.js v20" in text, (
"build_ui.sh must surface `nvm use v20` failure with an error "
"message and non-zero exit."
)
assert "node -v" in text, (
"build_ui.sh must read `node -v` to verify the active Node "
"version after `nvm use v20`."
)
assert "v20." in text, (
"build_ui.sh must check that `node -v` output starts with "
"`v20.` and abort otherwise (LIT-2723)."
)
24 changes: 20 additions & 4 deletions ui/litellm-dashboard/build_ui.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,29 @@ if ! command -v nvm &> /dev/null; then
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
fi

# Use nvm to set the required Node.js version
nvm use v20
# Install + use the pinned Node.js version. `nvm use` alone silently no-ops
# when v20 isn't installed in the current shell (e.g. non-interactive CI),
# and the build then runs against whatever node is on PATH. Install first, use
# second, and verify `node -v` actually reports v20 before continuing.
if ! nvm install v20; then
echo "Error: nvm install v20 failed. Deployment aborted."
exit 1
fi

# Check if nvm use was successful
if [ $? -ne 0 ]; then
if ! nvm use v20; then
echo "Error: Failed to switch to Node.js v20. Deployment aborted."
exit 1
fi

NODE_VER="$(node -v 2>/dev/null || true)"
case "$NODE_VER" in
v20.*) echo "Confirmed Node.js $NODE_VER" ;;
*)
echo "Error: expected Node.js v20.* but got '$NODE_VER'. Deployment aborted."
exit 1
;;
esac

# print contents of ui_colors.json
echo "Contents of ui_colors.json:"
cat ui_colors.json
Expand Down Expand Up @@ -60,4 +74,6 @@ if [ $? -eq 0 ]; then
echo "Deployment completed."
else
echo "Build failed. Deployment aborted."
exit 1
fi

6 changes: 6 additions & 0 deletions ui/litellm-dashboard/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ const __dirname = path.dirname(__filename);

const nextConfig = {
output: "export",
// Pin the directory-index layout (<route>/index.html). Otherwise different
// Next.js patch versions or stale .next caches flip the export between
// foo.html and foo/index.html, producing massive rename-only diffs in
// litellm/proxy/_experimental/out and breaking deployments that rely on
// directory-index routing for nested paths (e.g. /ui/mcp/oauth/callback).
trailingSlash: true,
// Required with output: "export" — default image optimizer runs only in server mode.
// See https://nextjs.org/docs/messages/export-image-api
images: {
Expand Down
Loading