Skip to content

Commit e7cd9a8

Browse files
committed
fix: Express 5 wildcard routes, local build scripts, and E2E test coverage
- Fix path-to-regexp crash: Express 5 requires named wildcards (/*splat) - Fix local build scripts: yao-pkg → pkg (actual binary name from @yao-pkg/pkg) - Rewrite E2E workflow: proper Windows process management with PowerShell Start-Process and log capture, curl retry health checks, HTTP status code validation, and full parity between Windows and Unix test coverage
1 parent d84ab62 commit e7cd9a8

3 files changed

Lines changed: 148 additions & 46 deletions

File tree

.github/workflows/e2e-tests.yml

Lines changed: 140 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ jobs:
104104
- name: Verify binary size
105105
shell: bash
106106
run: |
107-
if [[ "${{ runner.os }}" == "macOS" ]] || [[ "${{ runner.os }}" == "macos-latest" ]] || [[ "${{ runner.os }}" == "macos-15-intel" ]]; then
107+
if [[ "${{ runner.os }}" == "macOS" ]]; then
108108
size=$(stat -f%z "dist/${{ matrix.output }}")
109109
elif [[ "${{ runner.os }}" == "Windows" ]]; then
110110
size=$(powershell -Command "(Get-Item 'dist/${{ matrix.output }}').length")
@@ -118,88 +118,179 @@ jobs:
118118
fi
119119
echo "✓ Binary size: $size bytes"
120120
121-
- name: Start binary in background
122-
if: matrix.can_execute == true
121+
# Windows: use PowerShell Start-Process for reliable background process with log capture
122+
- name: Start binary (Windows)
123+
if: matrix.can_execute == true && runner.os == 'Windows'
124+
shell: pwsh
125+
run: |
126+
$proc = Start-Process -FilePath ".\dist\${{ matrix.output }}" `
127+
-ArgumentList "--no-printers" `
128+
-RedirectStandardOutput "startup.log" `
129+
-RedirectStandardError "startup-err.log" `
130+
-NoNewWindow -PassThru
131+
$proc.Id | Out-File -FilePath server.pid -Encoding ascii
132+
Write-Host "Started server with PID: $($proc.Id)"
133+
134+
# Unix: use standard background process
135+
- name: Start binary (Unix)
136+
if: matrix.can_execute == true && runner.os != 'Windows'
123137
shell: bash
124138
run: |
125-
if [[ "${{ runner.os }}" == "Windows" ]]; then
126-
powershell -Command "Start-Process -FilePath '.\dist\${{ matrix.output }}' -ArgumentList '--no-printers' -WindowStyle Hidden"
127-
else
128-
chmod +x dist/${{ matrix.output }}
129-
./dist/${{ matrix.output }} --no-printers > startup.log 2>&1 &
130-
echo $! > binary.pid
131-
fi
139+
chmod +x dist/${{ matrix.output }}
140+
./dist/${{ matrix.output }} --no-printers > startup.log 2>&1 &
141+
echo $! > server.pid
132142
133-
- name: Wait for startup
143+
- name: Wait for server to be ready
134144
if: matrix.can_execute == true
135145
shell: bash
136-
run: sleep 15
146+
run: |
147+
max_attempts=30
148+
attempt=0
149+
until curl -sf http://localhost:3000/ > /dev/null 2>&1; do
150+
attempt=$((attempt + 1))
151+
if [ $attempt -ge $max_attempts ]; then
152+
echo "::error::Server failed to start after $max_attempts attempts (60s)"
153+
echo "--- startup.log ---"
154+
cat startup.log 2>/dev/null || echo "(no stdout log)"
155+
echo "--- startup-err.log ---"
156+
cat startup-err.log 2>/dev/null || echo "(no stderr log)"
157+
exit 1
158+
fi
159+
echo "Waiting for server... (attempt $attempt/$max_attempts)"
160+
sleep 2
161+
done
162+
echo "✓ Server is responding"
137163
138-
- name: Validate startup
164+
- name: Validate startup logs
139165
if: matrix.can_execute == true
140166
shell: bash
141167
run: |
142-
if [[ "${{ runner.os }}" != "Windows" ]]; then
143-
if grep -iE "\[Error\]|\[Fatal\]|exception" startup.log; then
144-
echo "::error::Errors detected during startup"
168+
if [ -f startup.log ]; then
169+
if grep -iE "\[Error\]|\[Fatal\]|exception|EADDRINUSE" startup.log; then
170+
echo "::error::Errors detected in startup log"
145171
cat startup.log
146172
exit 1
147173
fi
148174
149175
if ! grep -q "\[Ready\] FlashForgeWebUI is ready" startup.log; then
150176
echo "::error::Startup did not complete - missing ready marker"
151-
tail -n 50 startup.log
177+
cat startup.log
152178
exit 1
153179
fi
154-
echo "✓ Startup successful"
180+
echo "✓ Startup log looks clean"
181+
else
182+
echo "::warning::No startup.log found"
183+
fi
184+
185+
- name: Test static file serving
186+
if: matrix.can_execute == true
187+
shell: bash
188+
run: |
189+
# Test index.html is served at root
190+
status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/)
191+
if [ "$status" != "200" ]; then
192+
echo "::error::GET / returned HTTP $status (expected 200)"
193+
exit 1
194+
fi
195+
196+
# Test index.html contains expected content
197+
if ! curl -sf http://localhost:3000/ | grep -q "FlashForge Web UI"; then
198+
echo "::error::GET / did not contain 'FlashForge Web UI'"
199+
curl -s http://localhost:3000/ | head -20
200+
exit 1
155201
fi
202+
echo "✓ Static file serving works"
156203
157204
- name: Test API endpoints
158205
if: matrix.can_execute == true
159206
shell: bash
160207
run: |
161-
response=$(curl -s http://localhost:3000/api/auth/status)
162-
if ! echo "$response" | jq '.' > /dev/null 2>&1; then
163-
echo "::error::Auth status API returned invalid JSON: $response"
208+
# Auth status endpoint
209+
status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/auth/status)
210+
if [ "$status" != "200" ]; then
211+
echo "::error::GET /api/auth/status returned HTTP $status (expected 200)"
164212
exit 1
165213
fi
166214
167-
if ! curl -s http://localhost:3000/ | grep -q "FlashForge Web UI"; then
168-
echo "::error::Failed to serve index.html"
215+
response=$(curl -sf http://localhost:3000/api/auth/status)
216+
if ! echo "$response" | jq '.' > /dev/null 2>&1; then
217+
echo "::error::Auth status API returned invalid JSON: $response"
169218
exit 1
170219
fi
220+
echo "✓ GET /api/auth/status - valid JSON response"
171221
172-
login_response=$(curl -s -X POST http://localhost:3000/api/auth/login \
222+
# Login endpoint
223+
login_response=$(curl -s -w "\n%{http_code}" -X POST http://localhost:3000/api/auth/login \
173224
-H "Content-Type: application/json" \
174225
-d '{"password":"changeme"}')
175-
success=$(echo "$login_response" | jq -r '.success')
226+
login_body=$(echo "$login_response" | sed '$d')
227+
login_status=$(echo "$login_response" | tail -1)
228+
229+
if [ "$login_status" != "200" ]; then
230+
echo "::error::POST /api/auth/login returned HTTP $login_status (expected 200)"
231+
echo "Response: $login_body"
232+
exit 1
233+
fi
234+
235+
success=$(echo "$login_body" | jq -r '.success')
176236
if [ "$success" != "true" ]; then
177-
echo "::error::Login API failed: $login_response"
237+
echo "::error::Login API returned success=$success (expected true)"
238+
echo "Response: $login_body"
239+
exit 1
240+
fi
241+
echo "✓ POST /api/auth/login - login successful"
242+
243+
# 404 handler for unknown API routes
244+
unknown_status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/nonexistent)
245+
if [ "$unknown_status" != "404" ]; then
246+
echo "::error::GET /api/nonexistent returned HTTP $unknown_status (expected 404)"
178247
exit 1
179248
fi
249+
echo "✓ GET /api/nonexistent - returns 404"
180250
181251
echo "✓ All API tests passed"
182252
183-
- name: Stop binary
184-
if: matrix.can_execute == true
253+
# Windows cleanup
254+
- name: Stop binary (Windows)
255+
if: always() && matrix.can_execute == true && runner.os == 'Windows'
256+
shell: pwsh
257+
run: |
258+
if (Test-Path server.pid) {
259+
$pid = Get-Content server.pid
260+
$proc = Get-Process -Id $pid -ErrorAction SilentlyContinue
261+
if ($proc) {
262+
Stop-Process -Id $pid -Force
263+
Write-Host "Stopped server (PID: $pid)"
264+
} else {
265+
Write-Host "Process already exited"
266+
}
267+
}
268+
# Fallback: kill by image name
269+
Get-Process -Name "flashforge-webui*" -ErrorAction SilentlyContinue | Stop-Process -Force
270+
Start-Sleep -Seconds 3
271+
272+
# Unix cleanup
273+
- name: Stop binary (Unix)
274+
if: always() && matrix.can_execute == true && runner.os != 'Windows'
185275
shell: bash
186276
run: |
187-
if [[ "${{ runner.os }}" == "Windows" ]]; then
188-
taskkill /F /IM "${{ matrix.output }}" 2>/dev/null || true
189-
else
190-
if [ -f binary.pid ]; then
191-
kill -TERM $(cat binary.pid) || true
192-
rm binary.pid
193-
fi
194-
pkill -TERM -f "${{ matrix.output }}" || true
277+
if [ -f server.pid ]; then
278+
kill -TERM $(cat server.pid) 2>/dev/null || true
279+
rm server.pid
195280
fi
281+
pkill -TERM -f "${{ matrix.output }}" 2>/dev/null || true
196282
sleep 3
197283
198284
- name: Verify cleanup
199-
if: matrix.can_execute == true
285+
if: always() && matrix.can_execute == true
200286
shell: bash
201287
run: |
202-
if [[ "${{ runner.os }}" != "Windows" ]]; then
288+
if [[ "${{ runner.os }}" == "Windows" ]]; then
289+
if powershell -Command "Get-Process -Name 'flashforge-webui*' -ErrorAction SilentlyContinue"; then
290+
echo "::error::Binary left zombie processes"
291+
exit 1
292+
fi
293+
else
203294
if pgrep -f "${{ matrix.output }}"; then
204295
echo "::error::Binary left zombie processes"
205296
exit 1
@@ -216,6 +307,17 @@ jobs:
216307
retention-days: 7
217308
compression-level: 6
218309

310+
- name: Upload startup logs
311+
uses: actions/upload-artifact@v4
312+
if: always() && matrix.can_execute == true
313+
with:
314+
name: logs-${{ matrix.platform }}-${{ matrix.arch }}
315+
path: |
316+
startup.log
317+
startup-err.log
318+
if-no-files-found: ignore
319+
retention-days: 7
320+
219321
- name: Generate summary
220322
if: always()
221323
shell: bash

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
"build:webui": "tsc --project src/webui/static/tsconfig.json && npm run build:webui:copy",
1717
"build:webui:watch": "tsc --project src/webui/static/tsconfig.json --watch",
1818
"build:webui:copy": "node scripts/copy-webui-assets.js",
19-
"build:linux": "npm run build && yao-pkg . --targets node20-linux-x64 --output dist/flashforge-webui-linux-x64",
20-
"build:linux-arm": "npm run build && yao-pkg . --targets node20-linux-arm64 --output dist/flashforge-webui-linux-arm64",
21-
"build:linux-armv7": "npm run build && yao-pkg . --targets node20.19.5-linux-armv7 --output dist/flashforge-webui-linux-armv7",
22-
"build:win": "npm run build && yao-pkg . --targets node20-win-x64 --output dist/flashforge-webui-win-x64.exe",
23-
"build:mac": "npm run build && yao-pkg . --targets node20-macos-x64 --output dist/flashforge-webui-macos-x64",
24-
"build:mac-arm": "npm run build && yao-pkg . --targets node20-macos-arm64 --output dist/flashforge-webui-macos-arm64",
19+
"build:linux": "npm run build && pkg . --targets node20-linux-x64 --output dist/flashforge-webui-linux-x64",
20+
"build:linux-arm": "npm run build && pkg . --targets node20-linux-arm64 --output dist/flashforge-webui-linux-arm64",
21+
"build:linux-armv7": "npm run build && pkg . --targets node20.19.5-linux-armv7 --output dist/flashforge-webui-linux-armv7",
22+
"build:win": "npm run build && pkg . --targets node20-win-x64 --output dist/flashforge-webui-win-x64.exe",
23+
"build:mac": "npm run build && pkg . --targets node20-macos-x64 --output dist/flashforge-webui-macos-x64",
24+
"build:mac-arm": "npm run build && pkg . --targets node20-macos-arm64 --output dist/flashforge-webui-macos-arm64",
2525
"build:all": "npm run build && npm run build:linux && npm run build:linux-arm && npm run build:linux-armv7 && npm run build:win && npm run build:mac && npm run build:mac-arm",
2626
"build:wrapper": "tsx scripts/platform-build-wrapper.ts",
2727
"build:win:wrapped": "npm run build:wrapper -- --platform win",

src/webui/server/WebUIManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ export class WebUIManager extends EventEmitter {
221221
this.expressApp.use('/api', apiRoutes);
222222

223223
// 404 handler for API routes - must come before SPA fallback
224-
this.expressApp.use('/api/*', (req, res) => {
224+
this.expressApp.use('/api/*splat', (req, res) => {
225225
const response: StandardAPIResponse = {
226226
success: false,
227227
error: `API endpoint not found: ${req.method} ${req.path}`
@@ -231,7 +231,7 @@ export class WebUIManager extends EventEmitter {
231231

232232
// SPA fallback - serve index.html for non-API routes that don't match static files
233233
// This enables client-side routing in the WebUI
234-
this.expressApp.get('*', (req, res, next) => {
234+
this.expressApp.get('/*splat', (req, res, next) => {
235235
// Skip if this looks like a file request with extension (handled by static middleware)
236236
if (path.extname(req.path) && req.path !== '/') {
237237
// File request that wasn't found by static middleware - return 404

0 commit comments

Comments
 (0)