experiments in a post-browser web
at main 658 lines 22 kB view raw
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."