Repository navigation
Expand file tree
/
Copy pathhttp-e2e.sh
More file actions
executable file
·340 lines (323 loc) · 14.6 KB
/
Copy pathhttp-e2e.sh
File metadata and controls
executable file
·340 lines (323 loc) · 14.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
#!/usr/bin/env bash
# Runs one end-to-end suite group over real HTTP, each against its own app: `next dev`, except
# CI groups, which reuse the production build from `e2e-build`.
# The job extracts it into apps/sim/.next before running these suites.
#
# Usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|assistant-actions|organization-search|browser-extension|browser-extension-native|browser-extension-access|project-files|mobile|seo> (run from apps/sim)
#
# The job provides DATABASE_URL, BETTER_AUTH_SECRET and ENCRYPTION_KEY; each group sets the rest of
# its app's environment here. Reports and server logs land in $RUNNER_TEMP/e2e.
#
# Under `next dev` the first request cold-compiles the app under Turbopack, which takes 60-185s on
# a healthy CI runner and has run past 300s on a degraded one. The readiness deadline, about three
# times the healthy worst case, only has to catch a hung boot: an exited server fails immediately,
# and either way the server log tail lands in the job log.
#
# Each app starts from an empty Turbopack dev cache: a cache written under other NEXT_PUBLIC_*
# values, by a server that `next dev` SIGKILLs 100ms after SIGTERM, can panic Turbopack or wedge a
# route compile on restore. It runs in its own session under an E2E_APP tag, and stop-session.sh
# returns only once every process in that session or carrying that tag has exited (Next's
# telemetry flush runs detached and still writes .next/dev).
set -euo pipefail
group=${1:?usage: http-e2e.sh <scim|cli|stop-after|desktop-inbox|assistant-actions|organization-search|browser-extension|browser-extension-native|browser-extension-access|project-files|mobile|seo>}
report_dir="$RUNNER_TEMP/e2e"
ready_timeout_seconds=600
mkdir -p "$report_dir"
server_pid=''
app_tag=''
server_log=''
status_log=''
realtime_pid=''
realtime_tag=''
realtime_log=''
browsers_pid=''
local_app_url=''
# `dev` or `start`; a group sets it before start_app.
app_server=${E2E_APP_SERVER:-dev}
finish() {
local status=$?
if [ -n "$server_pid" ]; then
bash "$GITHUB_WORKSPACE/.github/scripts/stop-session.sh" "$server_pid" "$app_tag" || status=1
wait "$server_pid" 2>/dev/null || true
if [ -n "$status_log" ] && [ "$app_server" = start ]; then
# `next start` logs no requests; RouteHandler's closing line for each API request carries
# its status (an unhandled error's line is a 500 without one) and duration.
jq -Rr 'fromjson? | objects | select(.module == "RouteHandler" and .duration != null
and ((.path // "") | startswith("/api/")))
| "\(.method) \(.path) \(.status // 500) in \(.duration)ms"' \
"$server_log" > "$status_log"
elif [ -n "$status_log" ]; then
awk '/^ (GET|POST|PUT|PATCH|DELETE|HEAD) \/api\// { print }' "$server_log" > "$status_log"
fi
if [ "$status" -ne 0 ]; then
tail -n 200 "$server_log"
fi
fi
if [ -n "$realtime_pid" ]; then
bash "$GITHUB_WORKSPACE/.github/scripts/stop-session.sh" "$realtime_pid" "$realtime_tag" || status=1
wait "$realtime_pid" 2>/dev/null || true
if [ "$status" -ne 0 ]; then
tail -n 100 "$realtime_log"
fi
fi
if [ -n "$browsers_pid" ]; then
kill -- "-$browsers_pid" 2>/dev/null || true
wait "$browsers_pid" 2>/dev/null || true
fi
exit "$status"
}
trap finish EXIT
start_browser_install() {
local name=$1
shift
browsers_log="$report_dir/$name-playwright-install.log"
setsid --wait bunx --no-install playwright install --with-deps "$@" > "$browsers_log" 2>&1 &
browsers_pid=$!
}
wait_for_browsers() {
if ! wait "$browsers_pid"; then
browsers_pid=''
tail -n 100 "$browsers_log"
echo "::error::Playwright browser install failed."
exit 1
fi
browsers_pid=''
}
# start_app <name> <port> <label> [record-http-status]
start_app() {
local name=$1 port=$2 label=$3
server_log="$report_dir/$name-next.log"
if [ "${4:-}" = record-http-status ]; then
status_log="$report_dir/$name-http-status.log"
fi
app_tag="$name-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$$"
local_app_url="http://127.0.0.1:$port"
export NEXT_PUBLIC_APP_URL="${E2E_APP_PUBLIC_URL:-$local_app_url}"
export BETTER_AUTH_URL="$NEXT_PUBLIC_APP_URL"
export DISABLE_TELEMETRY=true NEXT_TELEMETRY_DISABLED=1
if [ "$app_server" = start ]; then
if [ ! -f .next/BUILD_ID ]; then
echo "::error::No production build in apps/sim/.next for next start."
exit 1
fi
# Production logs only errors by default; INFO writes the RouteHandler lines the status log
# reads.
E2E_APP="$app_tag" LOG_LEVEL=INFO setsid node ../../node_modules/next/dist/bin/next start \
--hostname 127.0.0.1 --port "$port" > "$server_log" 2>&1 &
else
rm -rf .next/dev
E2E_APP="$app_tag" setsid node ../../node_modules/next/dist/bin/next dev --hostname 127.0.0.1 \
--port "$port" > "$server_log" 2>&1 &
fi
server_pid=$!
wait_ready "$server_pid" "$local_app_url/api/health" "$label app"
}
# start_realtime <name> <port>: the Socket.IO server the app's browser clients connect to, so pages
# render connected rather than under a "Reconnecting..." notification. Start after start_app, which
# exports the app URL the server allows as an origin; NEXT_PUBLIC_SOCKET_URL must already be set
# when the app starts.
start_realtime() {
local name=$1 port=$2
realtime_tag="$name-realtime-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT-$$"
realtime_log="$report_dir/$name-realtime.log"
(cd ../realtime && PORT="$port" E2E_APP="$realtime_tag" exec setsid bun run start \
> "$realtime_log" 2>&1) &
realtime_pid=$!
wait_ready "$realtime_pid" "http://127.0.0.1:$port/health" "$name realtime server"
}
# wait_ready <pid> <health-url> <label>
wait_ready() {
local pid=$1 url=$2 label=$3
local started=$SECONDS
until curl --fail --silent --max-time 10 "$url" > /dev/null; do
if ! kill -0 "$pid" 2>/dev/null; then
echo "::error::Local $label exited during startup."
exit 1
fi
if [ $((SECONDS - started)) -ge "$ready_timeout_seconds" ]; then
echo "::error::Local $label did not become ready within $ready_timeout_seconds seconds."
exit 1
fi
sleep 2
done
echo "Local $label ready after $((SECONDS - started))s"
}
case "$group" in
scim)
export NEXT_PUBLIC_FORCE_HOSTED=true
export BILLING_ENABLED=true NEXT_PUBLIC_BILLING_ENABLED=true
export ENTERPRISE_ENABLED=true NEXT_PUBLIC_ENTERPRISE_ENABLED=true
export SCIM_ENABLED=true NEXT_PUBLIC_SCIM_ENABLED=true
export SSO_ENABLED=true NEXT_PUBLIC_SSO_ENABLED=true
export ORGANIZATIONS_ENABLED=true NEXT_PUBLIC_ORGANIZATIONS_ENABLED=true
export INTERNAL_API_SECRET=scim-http-ci-local-secret-at-least-32-characters
export DB_TX_TRIPWIRE=throw
export NEXT_PUBLIC_CHAT_DISABLED=true
export SIM_LINKS=true
export PEOPLE=true
start_app scim 3017 SCIM record-http-status
SCIM_E2E_BASE_URL="$local_app_url" \
SCIM_E2E_DATABASE_URL="$DATABASE_URL" \
SCIM_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
SCIM_E2E_REPORT_PATH="$report_dir/scim-e2e-report.json" \
bun run test:scim:e2e
VERSION_COMPARE_E2E_BASE_URL="$local_app_url" \
VERSION_COMPARE_E2E_DATABASE_URL="$DATABASE_URL" \
VERSION_COMPARE_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
VERSION_COMPARE_E2E_REPORT_PATH="$report_dir/version-compare-http-report.json" \
bun run test:workflow-version-compare:e2e
SIM_LINKS_E2E_BASE_URL="$local_app_url" \
SIM_LINKS_E2E_DATABASE_URL="$DATABASE_URL" \
SIM_LINKS_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
SIM_LINKS_E2E_REPORT_PATH="$report_dir/sim-links-e2e-report.json" \
bun run test:sim-links:e2e
PEOPLE_E2E_BASE_URL="$local_app_url" \
PEOPLE_E2E_DATABASE_URL="$DATABASE_URL" \
PEOPLE_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
PEOPLE_E2E_REPORT_PATH="$report_dir/people-e2e-report.json" \
bun run test:people:e2e
;;
# The search suites serve their own fixtures in-process and need no app. They share this group
# because they finish in seconds. Self-hosted without billing: hosted billing admits runs through
# Redis, which the CLI app is not given.
cli)
for search in google-content lucid zoom google-meet; do
report_var="SEARCH_$(echo "$search" | tr 'a-z-' 'A-Z_')_REPORT_PATH"
env NEXT_PUBLIC_APP_URL=http://127.0.0.1:3040 NEXT_PUBLIC_FORCE_HOSTED=false \
"$report_var=$report_dir/search-$search.json" \
bun "scripts/test-search-$search-e2e.ts"
done
export NEXT_PUBLIC_FORCE_HOSTED=false
export INTERNAL_API_SECRET=cli-http-ci-local-secret-at-least-32-characters
export SIM_MCP_URL=http://mcp.sim.test/mcp
start_app cli 3018 CLI
MCP_HOST_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
MCP_HOST_E2E_REPORT_PATH="$report_dir/mcp-host-e2e-report.json" \
bun --no-env-file scripts/test-mcp-host-e2e.ts
CLI_LATENCY_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
CLI_LATENCY_E2E_DATABASE_URL="$DATABASE_URL" \
CLI_LATENCY_E2E_RUNS=1 \
CLI_LATENCY_E2E_WARMUP=1 \
CLI_LATENCY_E2E_REPORT_PATH="$report_dir/cli-run-latency-report.json" \
bun run test:cli-run-latency:e2e
;;
# Self-hosted: hosted billing admits a run only through a Redis usage reservation.
stop-after)
export NEXT_PUBLIC_FORCE_HOSTED=false
export CRON_SECRET=stop-after-e2e-local-cron-secret
export INTERNAL_API_SECRET=stop-after-http-ci-local-secret-at-least-32-characters
export DB_TX_TRIPWIRE=throw
export NEXT_PUBLIC_CHAT_DISABLED=true
start_app stop-after 3018 workflow record-http-status
STOP_AFTER_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
STOP_AFTER_E2E_DATABASE_URL="$DATABASE_URL" \
STOP_AFTER_E2E_REPORT_PATH="$report_dir/stop-after-http-report.json" \
STOP_AFTER_E2E_CRON_SECRET="$CRON_SECRET" \
bun run test:workflow-stop-after:e2e
;;
desktop-inbox|assistant-actions|organization-search)
export REDIS_URL=redis://127.0.0.1:6379
export NEXT_PUBLIC_FORCE_HOSTED=false
export COPILOT_TOOL_PERMISSIONS_ENABLED=true
export KNOWLEDGE_MEMBER_ACCESS=true CREDENTIAL_GROUPS=true ACCESS_CONTROL_ENABLED=true
export NEXT_PUBLIC_ACCESS_CONTROL_ENABLED=true NEXT_PUBLIC_ORGANIZATIONS_ENABLED=true
if [ "$group" != desktop-inbox ]; then start_browser_install "$group" chromium; fi
export INTERNAL_API_SECRET=desktop-inbox-http-ci-local-secret-at-least-32-characters
export DB_TX_TRIPWIRE=throw
start_app "$group" 3019 "$group" record-http-status
if [ "$group" != desktop-inbox ]; then wait_for_browsers; fi
case "$group" in
desktop-inbox)
DESKTOP_INBOX_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
DESKTOP_INBOX_E2E_DATABASE_URL="$DATABASE_URL" \
DESKTOP_INBOX_E2E_REDIS_URL="$REDIS_URL" \
DESKTOP_INBOX_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
DESKTOP_INBOX_E2E_REPORT_PATH="$report_dir/desktop-inbox-http-report.json" \
bun run test:desktop-inbox:e2e
;;
assistant-actions)
ASSISTANT_ACTIONS_E2E_BASE_URL="$local_app_url" \
ASSISTANT_ACTIONS_E2E_DATABASE_URL="$DATABASE_URL" \
ASSISTANT_ACTIONS_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
ASSISTANT_ACTIONS_E2E_REPORT_PATH="$report_dir/assistant-actions-ui-report.json" \
bun --no-env-file scripts/test-assistant-actions-e2e.ts
;;
organization-search)
ORGANIZATION_SEARCH_E2E_BASE_URL="$local_app_url" \
ORGANIZATION_SEARCH_E2E_DATABASE_URL="$DATABASE_URL" \
ORGANIZATION_SEARCH_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
ORGANIZATION_SEARCH_E2E_REPORT_PATH="$report_dir/organization-search-ui-report.json" \
bun --no-env-file scripts/test-organization-search-e2e.ts
;;
esac
;;
browser-extension|browser-extension-native|browser-extension-access)
extension_group=all
case "$group" in
browser-extension-native) extension_group=native ;;
browser-extension-access) extension_group=access ;;
esac
export REDIS_URL=redis://127.0.0.1:6379
export NEXT_PUBLIC_FORCE_HOSTED=false
export KNOWLEDGE_MEMBER_ACCESS=true CREDENTIAL_GROUPS=true ACCESS_CONTROL_ENABLED=true
export NEXT_PUBLIC_ACCESS_CONTROL_ENABLED=true
export API_ENCRYPTION_KEY="$ENCRYPTION_KEY"
start_browser_install "$group" chromium
extension_identity=$(bun - <<'JS'
import { generateKeyPairSync, createHash } from 'node:crypto'
const { publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
const der = publicKey.export({ type: 'spki', format: 'der' })
const id = [...createHash('sha256').update(der).digest('hex').slice(0, 32)]
.map(c => String.fromCharCode(97 + parseInt(c, 16))).join('')
process.stdout.write(`${der.toString('base64')}\n${id}`)
JS
)
export SIM_EXTENSION_KEY="${extension_identity%%$'\n'*}"
export BROWSER_EXTENSION_IDS="${extension_identity#*$'\n'}"
start_app "$group" 3007 'browser extension' record-http-status
wait_for_browsers
EXTENSION_APP_E2E_BASE_URL="$local_app_url" \
EXTENSION_APP_E2E_DATABASE_URL="$DATABASE_URL" \
EXTENSION_APP_E2E_GROUP="$extension_group" \
EXTENSION_APP_E2E_REPORT_PATH="$report_dir/$group/report.json" \
bun run test:browser-extension:e2e
;;
project-files)
PROJECT_FILES_E2E_ADMIN_DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/postgres \
PROJECT_FILES_E2E_REDIS_URL=redis://127.0.0.1:6379 \
PROJECT_FILES_E2E_BASE_URL=http://127.0.0.1:3050 \
PROJECT_FILES_E2E_RELAY_URL=http://127.0.0.1:3052 \
PROJECT_FILES_E2E_REPORT_PATH="$report_dir/project-files/orchestration.json" \
bun --no-env-file scripts/test-project-files-e2e.ts
;;
# MOBILE_E2E_BROWSER (chromium or webkit) narrows the job to one browser; unset runs both.
mobile)
app_server=start
export NEXT_PUBLIC_FORCE_HOSTED=false
export INTERNAL_API_SECRET=mobile-http-ci-local-secret-at-least-32-characters
export NEXT_PUBLIC_SOCKET_URL=http://127.0.0.1:3025 SOCKET_SERVER_URL=http://127.0.0.1:3025
read -ra browsers <<< "${MOBILE_E2E_BROWSER:-chromium webkit}"
start_browser_install mobile "${browsers[@]}"
start_app mobile 3024 'mobile browser' record-http-status
start_realtime mobile 3025
wait_for_browsers
MOBILE_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
MOBILE_E2E_DATABASE_URL="$DATABASE_URL" \
MOBILE_E2E_AUTH_SECRET="$BETTER_AUTH_SECRET" \
MOBILE_E2E_REPORT_PATH="$report_dir/mobile-e2e-report.json" \
bun run test:mobile:e2e
;;
seo)
app_server=start
export NEXT_PUBLIC_FORCE_HOSTED=false
start_app seo 3024 'landing SEO' record-http-status
# Plain HTTP assertions have no browser-specific behavior.
LANDING_SEO_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
LANDING_SEO_E2E_REPORT_PATH="$report_dir/landing-seo-e2e-report.json" \
bun run test:landing-seo:e2e
;;
*)
echo "::error::Unknown end-to-end group: $group" >&2
exit 2
;;
esac