experiments in a post-browser web
1#!/usr/bin/env bash
2set -euo pipefail
3
4# ============================================================================
5# E2E Sync & Version Compatibility Test
6#
7# Tests sync between local server, headless desktop, and iOS simulator
8# across multiple accounts, multiple profiles, and all 9 version permutations.
9#
10# Usage:
11# ./scripts/e2e-version-test.sh # run all phases
12# ./scripts/e2e-version-test.sh --phase 4 # run only phase 4 (setup is automatic)
13# ./scripts/e2e-version-test.sh --phase 2,3 # run phases 2 and 3
14# ./scripts/e2e-version-test.sh --phase 4,5 # run mobile phases
15#
16# Phases:
17# 0 Prerequisites (always runs)
18# 1 Setup: server, users, profiles, seed data (always runs)
19# 2 Desktop sync tests (automated)
20# 3 Version permutation tests (automated)
21# 4 Mobile sync test (semi-automated)
22# 5 Mobile version mismatch test (semi-automated)
23# ============================================================================
24
25# --- Argument parsing -------------------------------------------------------
26
27RUN_PHASES="" # empty = all
28
29while [[ $# -gt 0 ]]; do
30 case "$1" in
31 --phase|--phases)
32 RUN_PHASES="$2"
33 shift 2
34 ;;
35 -h|--help)
36 head -22 "$0" | tail -16
37 exit 0
38 ;;
39 *)
40 echo "Unknown argument: $1"
41 exit 1
42 ;;
43 esac
44done
45
46should_run() {
47 local phase="$1"
48 # Empty = run all
49 [ -z "$RUN_PHASES" ] && return 0
50 # Check if phase is in comma-separated list
51 echo ",$RUN_PHASES," | grep -q ",$phase,"
52}
53
54SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
55PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
56VERSION_JS="$PROJECT_ROOT/backend/server/version.js"
57
58TS="$(date +%s)"
59LOG_FILE="/tmp/e2e-version-test-$(date +%Y%m%d-%H%M%S).log"
60exec > >(tee -a "$LOG_FILE") 2>&1
61
62# --- Logging helpers --------------------------------------------------------
63
64FAILURES=0
65PASSES=0
66PHASE="init"
67
68log() { echo "[$(date +%H:%M:%S)] [$1] $2"; }
69log_pass() { log "$1" "PASS: $2"; PASSES=$((PASSES + 1)); }
70log_fail() { log "$1" "FAIL: $2"; FAILURES=$((FAILURES + 1)); }
71log_skip() { log "$1" "SKIP: $2"; }
72
73assert_status() {
74 local TEST_NAME="$1" EXPECTED="$2" ACTUAL="$3"
75 local RESPONSE_BODY="${4:-}" RESPONSE_HEADERS="${5:-}"
76 if [ "$ACTUAL" -eq "$EXPECTED" ]; then
77 log_pass "$PHASE" "$TEST_NAME: $ACTUAL"
78 else
79 log_fail "$PHASE" "$TEST_NAME: got $ACTUAL, expected $EXPECTED"
80 [ -n "$RESPONSE_HEADERS" ] && log "$PHASE" " Response headers: $RESPONSE_HEADERS"
81 [ -n "$RESPONSE_BODY" ] && log "$PHASE" " Response body: $RESPONSE_BODY"
82 if [ -f "$SERVER_LOG" ]; then
83 log "$PHASE" " Server log (last 20 lines):"
84 tail -20 "$SERVER_LOG" | while IFS= read -r line; do
85 log "$PHASE" " $line"
86 done
87 fi
88 fi
89}
90
91# --- Configuration ----------------------------------------------------------
92
93PORT=$((20000 + RANDOM % 10000))
94SERVER_TEMP_DIR="$(mktemp -d /tmp/e2e-peek-server-XXXXXX)"
95SERVER_LOG="$SERVER_TEMP_DIR/server.log"
96SERVER_PID=""
97
98USER_A_KEY="e2e-a-key-$TS"
99USER_B_KEY="e2e-b-key-$TS"
100
101PROFILE_A_DEFAULT_ID=""
102PROFILE_A_WORK_ID=""
103PROFILE_B_DEFAULT_ID=""
104
105DESKTOP_PROFILE_A_DEFAULT="test-e2e-a-default-$TS"
106DESKTOP_PROFILE_A_WORK="test-e2e-a-work-$TS"
107DESKTOP_PROFILE_B_DEFAULT="test-e2e-b-default-$TS"
108
109USER_DATA_PATH="$HOME/Library/Application Support/Peek"
110
111ORIGINAL_VERSION_JS=""
112
113log "init" "E2E Sync & Version Compatibility Test"
114log "init" "Log file: $LOG_FILE"
115log "init" "Server temp dir: $SERVER_TEMP_DIR"
116log "init" "Port: $PORT"
117if [ -n "$RUN_PHASES" ]; then
118 log "init" "Running phases: $RUN_PHASES (setup always included)"
119else
120 log "init" "Running all phases"
121fi
122
123# --- Cleanup trap -----------------------------------------------------------
124
125cleanup() {
126 log "cleanup" "Starting cleanup..."
127
128 # Kill server
129 if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
130 kill "$SERVER_PID" 2>/dev/null || true
131 wait "$SERVER_PID" 2>/dev/null || true
132 log "cleanup" "Killed server (PID $SERVER_PID)"
133 fi
134
135 # Restore version.js
136 if [ -n "$ORIGINAL_VERSION_JS" ]; then
137 echo "$ORIGINAL_VERSION_JS" > "$VERSION_JS"
138 log "cleanup" "Restored version.js"
139 fi
140
141 # Remove server temp dir
142 rm -rf "$SERVER_TEMP_DIR"
143 log "cleanup" "Removed $SERVER_TEMP_DIR"
144
145 # Remove desktop test profiles
146 for p in "$DESKTOP_PROFILE_A_DEFAULT" "$DESKTOP_PROFILE_A_WORK" "$DESKTOP_PROFILE_B_DEFAULT"; do
147 local dir="$USER_DATA_PATH/$p"
148 if [ -d "$dir" ]; then
149 rm -rf "$dir"
150 log "cleanup" "Removed desktop profile dir: $p"
151 fi
152 done
153
154 # Clean test profiles from .dev-profiles.db
155 local profiles_db="$USER_DATA_PATH/.dev-profiles.db"
156 if [ -f "$profiles_db" ]; then
157 sqlite3 "$profiles_db" "DELETE FROM profiles WHERE name LIKE 'test-e2e-%';" 2>/dev/null || true
158 log "cleanup" "Cleaned test profiles from .dev-profiles.db"
159 fi
160
161 # Clear mobile sync config
162 clear_mobile_config
163
164 log "cleanup" "Cleanup complete"
165 print_results
166 log "cleanup" "Log file: $LOG_FILE"
167}
168
169trap cleanup EXIT
170
171# --- Helper: start/stop server ----------------------------------------------
172
173start_server() {
174 DATA_DIR="$SERVER_TEMP_DIR" PORT="$PORT" node "$PROJECT_ROOT/backend/server/index.js" > "$SERVER_LOG" 2>&1 &
175 SERVER_PID=$!
176 log "$PHASE" "Starting server on port $PORT (PID: $SERVER_PID)"
177
178 # Wait for server ready
179 local attempts=0
180 while ! curl -sf "http://localhost:$PORT/" > /dev/null 2>&1; do
181 attempts=$((attempts + 1))
182 if [ $attempts -gt 30 ]; then
183 log_fail "$PHASE" "Server failed to start after 30 attempts"
184 log "$PHASE" "Server log:"
185 cat "$SERVER_LOG"
186 exit 1
187 fi
188 sleep 0.5
189 done
190 log "$PHASE" "Server ready (health check OK)"
191}
192
193stop_server() {
194 if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
195 kill "$SERVER_PID" 2>/dev/null || true
196 wait "$SERVER_PID" 2>/dev/null || true
197 log "$PHASE" "Stopped server (PID $SERVER_PID)"
198 SERVER_PID=""
199 fi
200}
201
202write_version_js() {
203 local DS="$1" PROTO="$2"
204 cat > "$VERSION_JS" <<VEOF
205const DATASTORE_VERSION = $DS;
206const PROTOCOL_VERSION = $PROTO;
207module.exports = { DATASTORE_VERSION, PROTOCOL_VERSION };
208VEOF
209 log "$PHASE" "Wrote version.js: DS=$DS, PROTO=$PROTO"
210}
211
212# --- Helper: mobile config --------------------------------------------------
213
214clear_mobile_config() {
215 local container
216 container="$(xcrun simctl get_app_container booted com.dietrich.peek-mobile groups 2>/dev/null \
217 | grep 'group.com.dietrich.peek-mobile' | awk '{print $2}')" || true
218 if [ -n "$container" ]; then
219 local pjson="$container/profiles.json"
220 if [ -f "$pjson" ]; then
221 python3 -c "
222import json, sys
223p = '$pjson'
224try:
225 d = json.load(open(p))
226 for prof in d.get('profiles', []):
227 prof['server_url'] = ''
228 prof['api_key'] = ''
229 prof.pop('server_profile_id', None)
230 d.pop('sync', None)
231 json.dump(d, open(p, 'w'), indent=2)
232except Exception:
233 pass
234"
235 log "cleanup" "Cleared mobile sync config in profiles.json"
236 fi
237 fi
238}
239
240# --- Results ----------------------------------------------------------------
241
242print_results() {
243 local TOTAL=$((PASSES + FAILURES))
244 echo ""
245 echo "=========================================="
246 echo " E2E Sync & Version Test Results"
247 echo "=========================================="
248 echo ""
249 echo "Total: $PASSES/$TOTAL passed"
250 if [ "$FAILURES" -gt 0 ]; then
251 echo " *** $FAILURES FAILURE(S) ***"
252 fi
253 echo ""
254 echo "Log: $LOG_FILE"
255 echo "=========================================="
256}
257
258# ============================================================================
259# Phase 0: Prerequisites (always runs)
260# ============================================================================
261PHASE="prereqs"
262log "$PHASE" "Checking prerequisites..."
263
264for tool in node electron curl sqlite3 python3 xcrun; do
265 if command -v "$tool" &>/dev/null; then
266 log "$PHASE" " $tool: $(command -v "$tool")"
267 else
268 if [ "$tool" = "xcrun" ]; then
269 log "$PHASE" " $tool: not found (mobile tests will be skipped)"
270 else
271 log_fail "$PHASE" "$tool not found"
272 exit 1
273 fi
274 fi
275done
276
277# Save original version.js
278ORIGINAL_VERSION_JS="$(cat "$VERSION_JS")"
279log "$PHASE" "Saved original version.js ($(wc -c < "$VERSION_JS") bytes)"
280
281# Build desktop
282log "$PHASE" "Building desktop app (yarn build)..."
283(cd "$PROJECT_ROOT" && yarn build) 2>&1 | tail -5
284log "$PHASE" "Desktop build complete"
285
286# ============================================================================
287# Phase 1: Setup (always runs)
288# ============================================================================
289PHASE="setup"
290log "$PHASE" "=== Phase 1: Setup ==="
291
292# Create test users
293log "$PHASE" "Creating test users..."
294USER_JSON="$(DATA_DIR="$SERVER_TEMP_DIR" USER_A_KEY="$USER_A_KEY" USER_B_KEY="$USER_B_KEY" \
295 node "$SCRIPT_DIR/e2e-setup-users.js")"
296log "$PHASE" "Users created: $USER_JSON"
297
298# Start server
299start_server
300
301# Create profiles for account-a
302log "$PHASE" "Creating profiles for account-a..."
303RESP="$(curl -sf -X POST "http://localhost:$PORT/profiles" \
304 -H "Authorization: Bearer $USER_A_KEY" \
305 -H "Content-Type: application/json" \
306 -d '{"name":"Default"}')"
307PROFILE_A_DEFAULT_ID="$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['profile']['id'])")"
308log "$PHASE" "Created profile Default for account-a → id=$PROFILE_A_DEFAULT_ID"
309
310RESP="$(curl -sf -X POST "http://localhost:$PORT/profiles" \
311 -H "Authorization: Bearer $USER_A_KEY" \
312 -H "Content-Type: application/json" \
313 -d '{"name":"Work"}')"
314PROFILE_A_WORK_ID="$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['profile']['id'])")"
315log "$PHASE" "Created profile Work for account-a → id=$PROFILE_A_WORK_ID"
316
317# Create profile for account-b
318log "$PHASE" "Creating profile for account-b..."
319RESP="$(curl -sf -X POST "http://localhost:$PORT/profiles" \
320 -H "Authorization: Bearer $USER_B_KEY" \
321 -H "Content-Type: application/json" \
322 -d '{"name":"Default"}')"
323PROFILE_B_DEFAULT_ID="$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['profile']['id'])")"
324log "$PHASE" "Created profile Default for account-b → id=$PROFILE_B_DEFAULT_ID"
325
326# Seed test data
327log "$PHASE" "Seeding test data..."
328
329seed_item() {
330 local KEY="$1" PROFILE_ID="$2" TYPE="$3" CONTENT="$4" LABEL="$5"
331 local STATUS BODY
332 BODY="$(curl -s -o /dev/null -w '%{http_code}' -X POST \
333 "http://localhost:$PORT/items?profile=$PROFILE_ID" \
334 -H "Authorization: Bearer $KEY" \
335 -H "Content-Type: application/json" \
336 -H "X-Peek-Datastore-Version: 1" \
337 -H "X-Peek-Protocol-Version: 1" \
338 -d "{\"type\":\"$TYPE\",\"content\":\"$CONTENT\"}")"
339 log "$PHASE" "Seeded $LABEL → HTTP $BODY"
340}
341
342# Account A / default (2 items)
343seed_item "$USER_A_KEY" "$PROFILE_A_DEFAULT_ID" "url" "https://acct-a-default-1.example.com" "account-a/default item 1 (url)"
344seed_item "$USER_A_KEY" "$PROFILE_A_DEFAULT_ID" "text" "acct-a-default-note-$TS" "account-a/default item 2 (text)"
345
346# Account A / work (2 items)
347seed_item "$USER_A_KEY" "$PROFILE_A_WORK_ID" "url" "https://acct-a-work-1.example.com" "account-a/work item 1 (url)"
348seed_item "$USER_A_KEY" "$PROFILE_A_WORK_ID" "text" "acct-a-work-note-$TS" "account-a/work item 2 (text)"
349
350# Account B / default (2 items)
351seed_item "$USER_B_KEY" "$PROFILE_B_DEFAULT_ID" "url" "https://acct-b-default-1.example.com" "account-b/default item 1 (url)"
352seed_item "$USER_B_KEY" "$PROFILE_B_DEFAULT_ID" "text" "acct-b-default-note-$TS" "account-b/default item 2 (text)"
353
354log "$PHASE" "Seeded 6 items total"
355
356# ============================================================================
357# Phase 2: Desktop Sync Tests
358# ============================================================================
359if should_run 2; then
360PHASE="desktop"
361log "$PHASE" "=== Phase 2: Desktop Sync Tests ==="
362
363run_desktop_sync() {
364 local PROFILE_NAME="$1" API_KEY="$2" SERVER_SLUG="$3" EXPECTED_COUNT="$4" LABEL="$5"
365
366 log "$PHASE" "Syncing profile $PROFILE_NAME (serverProfileId=$SERVER_SLUG)..."
367
368 PROFILE="$PROFILE_NAME" \
369 SERVER_URL="http://localhost:$PORT" \
370 API_KEY="$API_KEY" \
371 SERVER_PROFILE_ID="$SERVER_SLUG" \
372 electron "$SCRIPT_DIR/preconfigure-sync.mjs" 2>&1 | while IFS= read -r line; do
373 log "$PHASE" " [electron] $line"
374 done
375
376 # Verify via sqlite3
377 local DB_PATH="$USER_DATA_PATH/$PROFILE_NAME/datastore.sqlite"
378 if [ ! -f "$DB_PATH" ]; then
379 log_fail "$PHASE" "$LABEL — database not found at $DB_PATH"
380 return
381 fi
382
383 local ITEM_COUNT
384 ITEM_COUNT="$(sqlite3 "$DB_PATH" "SELECT COUNT(*) FROM items WHERE deletedAt = 0;")"
385 log "$PHASE" " Item count: $ITEM_COUNT (expected $EXPECTED_COUNT)"
386
387 if [ "$ITEM_COUNT" -eq "$EXPECTED_COUNT" ]; then
388 log_pass "$PHASE" "$LABEL: $ITEM_COUNT/$EXPECTED_COUNT items, isolation OK"
389 else
390 log_fail "$PHASE" "$LABEL: got $ITEM_COUNT items, expected $EXPECTED_COUNT"
391 log "$PHASE" " Items in DB:"
392 sqlite3 "$DB_PATH" "SELECT id, type, content, syncSource FROM items WHERE deletedAt = 0;" | while IFS= read -r row; do
393 log "$PHASE" " $row"
394 done
395 fi
396}
397
398run_desktop_sync "$DESKTOP_PROFILE_A_DEFAULT" "$USER_A_KEY" "$PROFILE_A_DEFAULT_ID" 2 "Account A default"
399run_desktop_sync "$DESKTOP_PROFILE_A_WORK" "$USER_A_KEY" "$PROFILE_A_WORK_ID" 2 "Account A work"
400run_desktop_sync "$DESKTOP_PROFILE_B_DEFAULT" "$USER_B_KEY" "$PROFILE_B_DEFAULT_ID" 2 "Account B default"
401
402else
403 log "desktop" "=== Phase 2: SKIPPED ==="
404fi
405
406# ============================================================================
407# Phase 3: Version Permutation Tests
408# ============================================================================
409if should_run 3; then
410PHASE="version"
411log "$PHASE" "=== Phase 3: Version Permutation Tests ==="
412
413# Test matrix: server_ds, server_proto, expected_status, label
414VERSION_TESTS=(
415 "1 1 200 match"
416 "2 1 409 DS-server-higher"
417 "0 1 409 DS-server-lower"
418 "1 2 409 PROTO-server-higher"
419 "1 0 409 PROTO-server-lower"
420 "2 2 409 both-higher"
421 "0 0 409 both-lower"
422 "2 0 409 DS-high-PROTO-low"
423 "0 2 409 DS-low-PROTO-high"
424)
425
426TEST_NUM=0
427for test_line in "${VERSION_TESTS[@]}"; do
428 read -r SRV_DS SRV_PROTO EXPECTED LABEL <<< "$test_line"
429 TEST_NUM=$((TEST_NUM + 1))
430
431 log "$PHASE" "Test #$TEST_NUM: Server DS=$SRV_DS, PROTO=$SRV_PROTO (expect $EXPECTED — $LABEL)"
432
433 stop_server
434 write_version_js "$SRV_DS" "$SRV_PROTO"
435 start_server
436
437 # Test GET /items
438 GET_HEADERS="$(mktemp)"
439 GET_BODY="$(curl -s -D "$GET_HEADERS" -o - -w '\n%{http_code}' \
440 "http://localhost:$PORT/items?profile=$PROFILE_A_DEFAULT_ID" \
441 -H "Authorization: Bearer $USER_A_KEY" \
442 -H "X-Peek-Datastore-Version: 1" \
443 -H "X-Peek-Protocol-Version: 1")"
444 GET_STATUS="$(echo "$GET_BODY" | tail -1)"
445 GET_RESPONSE="$(echo "$GET_BODY" | sed '$d')"
446 GET_HDRS="$(cat "$GET_HEADERS")"
447 rm -f "$GET_HEADERS"
448
449 assert_status "#$TEST_NUM GET ($LABEL)" "$EXPECTED" "$GET_STATUS" "$GET_RESPONSE" "$GET_HDRS"
450
451 # Verify response headers contain server version values
452 DS_HDR="$(echo "$GET_HDRS" | grep -i 'x-peek-datastore-version' | tr -d '\r' | awk '{print $2}')"
453 PROTO_HDR="$(echo "$GET_HDRS" | grep -i 'x-peek-protocol-version' | tr -d '\r' | awk '{print $2}')"
454 if [ "$DS_HDR" = "$SRV_DS" ] && [ "$PROTO_HDR" = "$SRV_PROTO" ]; then
455 log_pass "$PHASE" "#$TEST_NUM headers: DS=$DS_HDR, PROTO=$PROTO_HDR"
456 else
457 log_fail "$PHASE" "#$TEST_NUM headers: DS=$DS_HDR (expected $SRV_DS), PROTO=$PROTO_HDR (expected $SRV_PROTO)"
458 fi
459
460 # For 409 responses, verify JSON body fields
461 if [ "$EXPECTED" -eq 409 ]; then
462 ERROR_FIELD="$(echo "$GET_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('error',''))" 2>/dev/null || echo "")"
463 TYPE_FIELD="$(echo "$GET_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('type',''))" 2>/dev/null || echo "")"
464 if [ "$ERROR_FIELD" = "Version mismatch" ] && [ -n "$TYPE_FIELD" ]; then
465 log_pass "$PHASE" "#$TEST_NUM body: error='$ERROR_FIELD', type='$TYPE_FIELD'"
466 else
467 log_fail "$PHASE" "#$TEST_NUM body: error='$ERROR_FIELD', type='$TYPE_FIELD'"
468 fi
469 fi
470
471 # Test POST /items
472 POST_HEADERS="$(mktemp)"
473 POST_BODY="$(curl -s -D "$POST_HEADERS" -o - -w '\n%{http_code}' \
474 -X POST "http://localhost:$PORT/items?profile=$PROFILE_A_DEFAULT_ID" \
475 -H "Authorization: Bearer $USER_A_KEY" \
476 -H "Content-Type: application/json" \
477 -H "X-Peek-Datastore-Version: 1" \
478 -H "X-Peek-Protocol-Version: 1" \
479 -d "{\"type\":\"text\",\"content\":\"version-test-$TEST_NUM-$TS\"}")"
480 POST_STATUS="$(echo "$POST_BODY" | tail -1)"
481 POST_RESPONSE="$(echo "$POST_BODY" | sed '$d')"
482 POST_HDRS="$(cat "$POST_HEADERS")"
483 rm -f "$POST_HEADERS"
484
485 assert_status "#$TEST_NUM POST ($LABEL)" "$EXPECTED" "$POST_STATUS" "$POST_RESPONSE" "$POST_HDRS"
486done
487
488# Restore version.js and restart with correct versions
489log "$PHASE" "Restoring version.js to original (DS=1, PROTO=1)..."
490echo "$ORIGINAL_VERSION_JS" > "$VERSION_JS"
491stop_server
492start_server
493
494else
495 log "version" "=== Phase 3: SKIPPED ==="
496fi
497
498# ============================================================================
499# Phase 4: Mobile Sync Test (Semi-Automated)
500# ============================================================================
501if should_run 4; then
502PHASE="mobile"
503log "$PHASE" "=== Phase 4: Mobile Sync Test ==="
504
505# Ensure server is running with correct versions
506if [ -z "$SERVER_PID" ] || ! kill -0 "$SERVER_PID" 2>/dev/null; then
507 echo "$ORIGINAL_VERSION_JS" > "$VERSION_JS"
508 start_server
509fi
510
511MOBILE_CONTAINER=""
512
513if command -v xcrun &>/dev/null; then
514 MOBILE_CONTAINER="$(xcrun simctl get_app_container booted com.dietrich.peek-mobile groups 2>/dev/null \
515 | grep 'group.com.dietrich.peek-mobile' | awk '{print $2}')" || true
516fi
517
518if [ -z "$MOBILE_CONTAINER" ]; then
519 log_skip "$PHASE" "No booted simulator or app not installed — skipping mobile tests"
520else
521 LOCAL_IP="$(ipconfig getifaddr en0 2>/dev/null || echo "")"
522 if [ -z "$LOCAL_IP" ]; then
523 log_skip "$PHASE" "Could not determine local IP (en0) — skipping mobile tests"
524 else
525 log "$PHASE" "App Group container: $MOBILE_CONTAINER"
526 log "$PHASE" "Local IP: $LOCAL_IP"
527
528 # Write sync config to profiles.json
529 PROFILES_JSON="$MOBILE_CONTAINER/profiles.json"
530 python3 -c "
531import json, os
532path = '$PROFILES_JSON'
533data = {'profiles': []}
534if os.path.exists(path):
535 try:
536 data = json.load(open(path))
537 except Exception:
538 pass
539
540# Set sync config on ALL profile entries + top-level sync section
541profiles = data.get('profiles', [])
542for p in profiles:
543 p['server_url'] = 'http://$LOCAL_IP:$PORT'
544 p['api_key'] = '$USER_A_KEY'
545 p['server_profile_id'] = '$PROFILE_A_DEFAULT_ID'
546
547# Ensure at least one profile exists
548if not profiles:
549 profiles.append({
550 'id': 'e2e-default-profile',
551 'name': 'Default',
552 'createdAt': '2026-01-01T00:00:00+00:00',
553 'lastUsed': '2026-01-01T00:00:00+00:00',
554 'server_url': 'http://$LOCAL_IP:$PORT',
555 'api_key': '$USER_A_KEY',
556 'server_profile_id': '$PROFILE_A_DEFAULT_ID'
557 })
558 data['currentProfileId'] = 'e2e-default-profile'
559
560data['profiles'] = profiles
561data['sync'] = {
562 'server_url': 'http://$LOCAL_IP:$PORT',
563 'api_key': '$USER_A_KEY',
564 'auto_sync': False
565}
566json.dump(data, open(path, 'w'), indent=2)
567print('Wrote profiles.json')
568"
569 log "$PHASE" "Wrote sync config to $PROFILES_JSON"
570
571 # Clear stale last_sync from per-profile databases
572 for dbfile in "$MOBILE_CONTAINER"/peek-*.db; do
573 if [ -f "$dbfile" ]; then
574 sqlite3 "$dbfile" "DELETE FROM settings WHERE key = 'last_sync';" 2>/dev/null || true
575 log "$PHASE" "Cleared last_sync in $(basename "$dbfile")"
576 fi
577 done
578
579 echo ""
580 echo "============================================"
581 echo " MOBILE SYNC TEST"
582 echo "============================================"
583 echo " Server: http://$LOCAL_IP:$PORT"
584 echo " Account: account-a (default profile)"
585 echo " Server profile: $PROFILE_A_DEFAULT_ID"
586 echo ""
587 echo " 1. Force-quit and relaunch the app in simulator"
588 echo " 2. Go to Settings → verify server URL and API key"
589 echo " 3. Tap 'Sync All'"
590 echo " 4. Verify 2 items pulled (Account A default profile data)"
591 echo ""
592 echo -n " Press ENTER when done (or 's' to skip): "
593 read -r MOBILE_RESPONSE
594
595 if [ "$MOBILE_RESPONSE" = "s" ]; then
596 log_skip "$PHASE" "Mobile sync test skipped by user"
597 else
598 log_pass "$PHASE" "Mobile sync pull: user confirmed"
599 fi
600 fi
601fi
602
603else
604 log "mobile" "=== Phase 4: SKIPPED ==="
605fi
606
607# ============================================================================
608# Phase 5: Mobile Version Mismatch Test (Semi-Automated)
609# ============================================================================
610if should_run 5; then
611PHASE="mobile-version"
612log "$PHASE" "=== Phase 5: Mobile Version Mismatch Test ==="
613
614MOBILE_CONTAINER=""
615if command -v xcrun &>/dev/null; then
616 MOBILE_CONTAINER="$(xcrun simctl get_app_container booted com.dietrich.peek-mobile groups 2>/dev/null \
617 | grep 'group.com.dietrich.peek-mobile' | awk '{print $2}')" || true
618fi
619
620if [ -z "$MOBILE_CONTAINER" ]; then
621 log_skip "$PHASE" "No booted simulator or app not installed — skipping"
622else
623 stop_server
624 write_version_js 2 1
625 start_server
626
627 echo ""
628 echo "============================================"
629 echo " MOBILE VERSION MISMATCH TEST"
630 echo "============================================"
631 echo " Server now has DS=2 (mismatch with mobile's DS=1)."
632 echo ""
633 echo " 1. Trigger sync in the app"
634 echo " 2. Expect: Error message about version mismatch"
635 echo ""
636 echo -n " Press ENTER when done (or 's' to skip): "
637 read -r MISMATCH_RESPONSE
638
639 if [ "$MISMATCH_RESPONSE" = "s" ]; then
640 log_skip "$PHASE" "Mobile version mismatch test skipped by user"
641 else
642 log_pass "$PHASE" "Mobile version mismatch: user confirmed error shown"
643 fi
644
645 # Restore for cleanup
646 echo "$ORIGINAL_VERSION_JS" > "$VERSION_JS"
647 stop_server
648fi
649
650else
651 log "mobile-version" "=== Phase 5: SKIPPED ==="
652fi
653
654# ============================================================================
655# Done (cleanup handled by trap)
656# ============================================================================
657PHASE="done"
658log "$PHASE" "All requested phases complete."