diff --git a/apple/Package.resolved b/apple/Package.resolved index fa9d568..ac3fe54 100644 --- a/apple/Package.resolved +++ b/apple/Package.resolved @@ -15,7 +15,7 @@ "kind" : "remoteSourceControl", "location" : "ssh://git@github.com/logseq/lui.git", "state" : { - "revision" : "dae68945bfd9a95d1bbc83e222e2e31fc9d26f1b" + "revision" : "62e2e69d0c01281947afbb0f39ffa4c550b7ca3b" } }, { diff --git a/apple/Package.swift b/apple/Package.swift index c6d7b7e..ff7bca3 100644 --- a/apple/Package.swift +++ b/apple/Package.swift @@ -34,7 +34,7 @@ let package = Package( .library(name: "LogseqChatModel", type: .dynamic, targets: ["LogseqChatModel"]), ], dependencies: [ - .package(url: "ssh://git@github.com/logseq/lui.git", revision: "88f9b4a4c8fd9f0227656a986655479c495c0232"), + .package(url: "ssh://git@github.com/logseq/lui.git", revision: "62e2e69d0c01281947afbb0f39ffa4c550b7ca3b"), .package(url: "https://github.com/gonzalezreal/swiftui-math", from: "0.1.0"), .package(url: "https://github.com/appstefan/highlightswift.git", from: "1.1.0") ], diff --git a/apple/Sources/LogseqChat/LGChatRuntime.swift b/apple/Sources/LogseqChat/LGChatRuntime.swift index e47bcfe..e40ba4a 100644 --- a/apple/Sources/LogseqChat/LGChatRuntime.swift +++ b/apple/Sources/LogseqChat/LGChatRuntime.swift @@ -2,6 +2,7 @@ import Foundation import LUIAppleBackend import Observation import LogseqChatModel +import os private struct LGChatPatchMetadata: Decodable { let generation: Int @@ -113,6 +114,8 @@ public final class LGChatRuntime { @ObservationIgnored private let outlinerAutosaveDelayNanoseconds: UInt64 + private static let log = Logger(subsystem: "com.logseq.chat", category: "runtime") + @ObservationIgnored private var patchTail: Task? @@ -191,6 +194,7 @@ public final class LGChatRuntime { } let envelope = decodeCoreResponse(response) guard shouldApplyCoreResponse(response, envelope: envelope) else { + Self.log.notice("skipped core response (deduped) pendingSync=\(envelope?.result?.isPendingSyncPatch ?? false, privacy: .public) outliner=\(envelope?.result?.isOutlinerPatch ?? false, privacy: .public)") deliverPlatformCommands(envelope) return } @@ -390,15 +394,12 @@ public final class LGChatRuntime { } cancelOutlinerAutosaveBeforeExecuting(effect) + Self.log.notice("effect start id=\(effect.id, privacy: .public) kind=\(effect.kind, privacy: .public)") let resolution = await effectExecutor.execute(effect) - #if DEBUG + Self.log.notice("effect resolved id=\(effect.id, privacy: .public) kind=\(effect.kind, privacy: .public) succeeded=\(resolution.succeeded, privacy: .public)") if !resolution.succeeded { - print( - "LOGSEQ_LG_EFFECT failed id=\(effect.id)" - + " kind=\(effect.kind) message=\(resolution.message)" - ) + Self.log.error("effect failed id=\(effect.id, privacy: .public) kind=\(effect.kind, privacy: .public) message=\(resolution.message, privacy: .public)") } - #endif if resolution.succeeded, case .coreResponse = resolution.output { enqueueApply(native.applySnapshot(resolution.message)) @@ -420,6 +421,7 @@ public final class LGChatRuntime { let syncResolution = await startSyncIfNeeded( envelope: envelope ) + Self.log.notice("startSync resolved id=\(effect.id, privacy: .public) succeeded=\(syncResolution.succeeded, privacy: .public)") if !syncResolution.succeeded { lastError = syncResolution.message return @@ -563,21 +565,17 @@ public final class LGChatRuntime { guard let self else { return } do { let decoded = try await decodeTask.value - guard epoch == self.patchApplyEpoch else { return } - #if DEBUG - print( - "LOGSEQ_LG_PATCH apply generation=" - + String(Self.patchGeneration(patch) ?? -1) - ) - #endif + guard epoch == self.patchApplyEpoch else { + Self.log.warning("dropped stale patch generation=\(Self.patchGeneration(patch) ?? -1, privacy: .public) epoch=\(epoch, privacy: .public) current=\(self.patchApplyEpoch, privacy: .public)") + return + } + Self.log.notice("apply patch generation=\(Self.patchGeneration(patch) ?? -1, privacy: .public)") try self.renderer.apply(decoded: decoded) self.lastError = nil } catch { guard epoch == self.patchApplyEpoch else { return } self.lastError = String(describing: error) - #if DEBUG - print("LogseqChat renderer apply failed: \(self.lastError ?? "unknown")") - #endif + Self.log.error("renderer apply failed: \(self.lastError ?? "unknown", privacy: .public)") } } } diff --git a/apple/Sources/LogseqChatModel/ViewModel.swift b/apple/Sources/LogseqChatModel/ViewModel.swift index 4c8e596..649b1a6 100644 --- a/apple/Sources/LogseqChatModel/ViewModel.swift +++ b/apple/Sources/LogseqChatModel/ViewModel.swift @@ -15,7 +15,9 @@ private struct LogseqModelLogger { #if os(Android) print(message) #else - logger.info("\(message, privacy: .public)") + // notice so the line survives the sim's unified-log filtering (info + // is dropped from device-simulator.log captures). + logger.notice("\(message, privacy: .public)") #endif } diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index bf5ef6d..c901071 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -159,8 +159,8 @@ packages: dependency: "direct main" description: path: "platform/flutter" - ref: 2a2cc62dd4583734b7b7f6c226c587e950c5985e - resolved-ref: 2a2cc62dd4583734b7b7f6c226c587e950c5985e + ref: 11668eb7e5e26af8974901c5d158c1c0cafa40b3 + resolved-ref: 11668eb7e5e26af8974901c5d158c1c0cafa40b3 url: "ssh://git@github.com/logseq/lui.git" source: git version: "0.1.0" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index ec22bbb..a1667c8 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: lui_flutter_backend: git: url: ssh://git@github.com/logseq/lui.git - ref: 88f9b4a4c8fd9f0227656a986655479c495c0232 + ref: 11668eb7e5e26af8974901c5d158c1c0cafa40b3 path: platform/flutter webview_flutter: ^4.14.1 diff --git a/logseq_chat.opam b/logseq_chat.opam index a67f124..f0eddbc 100644 --- a/logseq_chat.opam +++ b/logseq_chat.opam @@ -31,9 +31,9 @@ pin-depends: [ ["melange-transit-core.0.1.2" "git+https://github.com/logseq/melange-transit.git#main"] ["melange-transit-native.0.1.2" "git+https://github.com/logseq/melange-transit.git#main"] ["melange-transit-melange.0.1.2" "git+https://github.com/logseq/melange-transit.git#main"] - ["lui.0.1.0" "git+ssh://git@github.com/logseq/lui.git#88f9b4a4c8fd9f0227656a986655479c495c0232"] + ["lui.0.1.0" "git+ssh://git@github.com/logseq/lui.git#62e2e69d0c01281947afbb0f39ffa4c550b7ca3b"] ["drive.dev" "git+https://github.com/logseq/drive.git#7f407b07f18e68596cb0dcb3ba1781cb0572a2bb"] - ["ocaml-signal.0.1.0" "git+https://github.com/logseq/ocaml-signal.git#976b40f1770a65b3464df1ef38d1550f1d8a43dd"] + ["ocaml-signal.0.1.0" "git+https://github.com/logseq/ocaml-signal.git#b9cb81fc910519fc579a90960267acb851483bca"] ["mldoc.dev" "git+https://github.com/logseq/mldoc.git#553dea6ed8694352527a189747f787365469c9cb"] ["angstrom.dev" "git+https://github.com/logseq/angstrom.git#3be9b966dc2bc9ccf9948d17a7b0df1cb526de15"] ["xmlm.dev" "git+https://github.com/logseq/xmlm.git#eb469d536e98c98f2754c0ff8813c92b3a17fe9f"] diff --git a/logseq_chat.opam.locked b/logseq_chat.opam.locked index 75b8163..8086e53 100644 --- a/logseq_chat.opam.locked +++ b/logseq_chat.opam.locked @@ -476,7 +476,7 @@ pin-depends: [ ] [ "ocaml-signal.0.1.0" - "git+https://github.com/logseq/ocaml-signal.git#976b40f1770a65b3464df1ef38d1550f1d8a43dd" + "git+https://github.com/logseq/ocaml-signal.git#b9cb81fc910519fc579a90960267acb851483bca" ] [ "ocaml-syntax-shims.1.0.0" @@ -1490,7 +1490,7 @@ x-opam-monorepo-duniverse-dirs: [ "ocaml-fsrs" ] [ - "git+https://github.com/logseq/ocaml-signal.git#976b40f1770a65b3464df1ef38d1550f1d8a43dd" + "git+https://github.com/logseq/ocaml-signal.git#b9cb81fc910519fc579a90960267acb851483bca" "ocaml-signal" ] [ diff --git a/scripts/test-android-e2e-runner.sh b/scripts/test-android-e2e-runner.sh index 01416d0..39e2bb4 100755 --- a/scripts/test-android-e2e-runner.sh +++ b/scripts/test-android-e2e-runner.sh @@ -189,8 +189,8 @@ PATH="$mock_bin:$PATH" \ LOGSEQ_CHAT_ANDROID_E2E_SKIP_INSTALL=1 \ LOGSEQ_CHAT_ANDROID_E2E_SKIP_VISUAL_GATES=1 \ "$runner" signed-out >/dev/null -[[ $(<"$flutter_args") == "$repo_root/flutter|build apk --debug" ]] \ - || die "Android E2E runner did not build the Flutter debug APK" +[[ $(<"$flutter_args") == "$repo_root/flutter|build apk --profile" ]] \ + || die "Android E2E runner did not build the Flutter profile APK" : >"$adb_args" PATH="$mock_bin:$PATH" \ @@ -400,6 +400,32 @@ expected_node_tag_args=$(printf '%s\n' \ [[ $(<"$maestro_args") == "$expected_node_tag_args" ]] \ || die "Android E2E runner did not preserve the node and tag parity flow" +: >"$adb_args" +cat >"$mock_bin/adb" <<'EOF' +#!/usr/bin/env bash +if [[ -n ${LOGSEQ_CHAT_ADB_ARGS:-} ]]; then + printf '%s\n' "$*" >>"$LOGSEQ_CHAT_ADB_ARGS" +fi +case "$*" in + *"exec-out uiautomator dump"*) + printf '%s\n' \ + '' + ;; +esac +EOF +printf '#!/usr/bin/env bash\nsleep 1\nprintf "%%s\\n" "$@" >"$LOGSEQ_CHAT_MAESTRO_ARGS"\n' >"$mock_bin/maestro" +chmod +x "$mock_bin/adb" "$mock_bin/maestro" +PATH="$mock_bin:$PATH" \ + ANDROID_SERIAL=test-device \ + LOGSEQ_CHAT_ADB_ARGS="$adb_args" \ + LOGSEQ_CHAT_MAESTRO_ARGS="$maestro_args" \ + LOGSEQ_CHAT_ANDROID_E2E_SKIP_BUILD=1 \ + LOGSEQ_CHAT_ANDROID_E2E_SKIP_INSTALL=1 \ + LOGSEQ_CHAT_ANDROID_E2E_SKIP_SEED=1 \ + "$runner" settings >/dev/null +grep -Fq -- '-s test-device shell input tap 225 912' "$adb_args" \ + || die "Android E2E runner's ANR watchdog did not tap the dialog's Close app button" + PATH="$mock_bin:$PATH" \ ANDROID_SERIAL=test-device \ LOGSEQ_CHAT_MAESTRO_ARGS="$maestro_args" \ diff --git a/scripts/test-android-e2e.sh b/scripts/test-android-e2e.sh index c7a4001..2755468 100755 --- a/scripts/test-android-e2e.sh +++ b/scripts/test-android-e2e.sh @@ -254,19 +254,78 @@ command -v maestro >/dev/null 2>&1 || die "Maestro CLI is not installed" command -v flutter >/dev/null 2>&1 || die "Flutter is not installed" temporary_files=() +anr_watchdog_pid="" cleanup() { + if [[ -n $anr_watchdog_pid ]]; then + kill "$anr_watchdog_pid" 2>/dev/null || true + fi if (( ${#temporary_files[@]} > 0 )); then rm -f "${temporary_files[@]}" fi } trap cleanup EXIT +# A system ANR dialog (" isn't responding", e.g. Pixel Launcher on a +# loaded emulator) occludes the whole a11y tree — Maestro can't see the app +# behind it and every assertion times out. Tap its "Wait" button so a +# system-level hiccup can't fail a flow whose app is healthy. +start_anr_watchdog() { + ( + while :; do + xml=$(adb -s "$device" exec-out uiautomator dump /dev/tty 2>/dev/null || true) + if printf '%s' "$xml" | grep -q "isn't responding"; then + anr_app=$(printf '%s' "$xml" | tr '>' '\n' \ + | sed -n "s/.*text=\"\(.*\) isn't responding\".*/\1/p" | head -1) + # "Wait" only postpones the dialog — a genuinely hung app process + # (Pixel Launcher on a loaded emulator) re-ANRs forever, so + # "Close app" force-stops it and Android restarts it fresh. But + # for system_server/System UI, "Close app" kills the runtime and + # soft-reboots the device, dropping every adb transport — always + # pick "Wait" there and let the transient stall recover. + case "$anr_app" in + *system_server*|*"System UI"*|*settings*) + close_bounds=$(printf '%s' "$xml" | tr '>' '\n' \ + | sed -n 's/.*text="Wait"[^>]*bounds="\(\[[0-9,]*\]\[[0-9,]*\]\)".*/\1/p' | head -1) + action="Wait" ;; + *) + close_bounds=$(printf '%s' "$xml" | tr '>' '\n' \ + | sed -n 's/.*text="Close app"[^>]*bounds="\(\[[0-9,]*\]\[[0-9,]*\]\)".*/\1/p' | head -1) + action="Close app" ;; + esac + if [[ -z $close_bounds ]]; then + close_bounds=$(printf '%s' "$xml" | tr '>' '\n' \ + | sed -n 's/.*text="Wait"[^>]*bounds="\(\[[0-9,]*\]\[[0-9,]*\]\)".*/\1/p' | head -1) + action="Wait" + fi + if [[ $close_bounds =~ \[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\] ]]; then + x=$(( (BASH_REMATCH[1] + BASH_REMATCH[3]) / 2 )) + y=$(( (BASH_REMATCH[2] + BASH_REMATCH[4]) / 2 )) + echo "[anr-watchdog] dismissing '$anr_app' ANR dialog ($action at $x,$y)" >&2 + adb -s "$device" shell input tap "$x" "$y" >/dev/null 2>&1 || true + fi + fi + # Every 3s turned out to hammer the a11y framework hard enough to + # starve Maestro's own UiAutomation binding on loaded emulators; + # 10s still catches ANR dialogs well inside assert timeouts. + sleep 10 + done + ) & + anr_watchdog_pid=$! +} + device=${ANDROID_SERIAL:-} if [[ -z $device ]]; then device=$(adb devices | awk 'NR > 1 && $2 == "device" { print $1; exit }') fi [[ -n $device ]] || die "no online Android emulator or device was found" +# Suppress ANR dialogs for background processes up front; the watchdog below +# still closes foreground ANRs (e.g. Pixel Launcher) by force-stopping them. +adb -s "$device" shell settings put global anr_show_background 0 >/dev/null 2>&1 || true +if [[ ${LOGSEQ_CHAT_ANDROID_E2E_SKIP_ANR_WATCHDOG:-0} != 1 ]]; then + start_anr_watchdog +fi + if [[ -n ${LOGSEQ_CHAT_E2E_BASE_URL:-} ]] \ && [[ $LOGSEQ_CHAT_E2E_BASE_URL =~ ^(http|https)://(127\.0\.0\.1|localhost)(:([0-9]+))?([/?#]|$) ]]; then local_backend_port=${BASH_REMATCH[4]:-} @@ -283,13 +342,13 @@ fi if [[ ${LOGSEQ_CHAT_ANDROID_E2E_SKIP_BUILD:-0} != 1 ]]; then ( cd "$repo_root/flutter" - ANDROID_SERIAL=$device flutter build apk --debug + ANDROID_SERIAL=$device flutter build apk --profile ) fi if [[ ${LOGSEQ_CHAT_ANDROID_E2E_SKIP_INSTALL:-0} != 1 ]]; then - apk="$repo_root/flutter/build/app/outputs/flutter-apk/app-debug.apk" - [[ -f $apk ]] || die "Android debug APK was not produced at $apk" + apk="$repo_root/flutter/build/app/outputs/flutter-apk/app-profile.apk" + [[ -f $apk ]] || die "Android profile APK was not produced at $apk" adb -s "$device" install -r "$apk" >/dev/null fi @@ -395,6 +454,70 @@ if [[ -n ${LOGSEQ_CHAT_E2E_BASE_URL:-} ]] \ done fi +recover_device() { + # Retrying a flow against a dead adb server, a flapped transport + # (get-state answers but the adbd channel is closed), or a crashed + # emulator fails identically — recover connectivity before the next + # attempt. `adb shell echo ok` proves the channel end-to-end; get-state + # alone only proves a stale transport entry. + local device_ok=0 + if timeout 15 adb -s "$device" shell 'echo ok' 2>/dev/null | grep -q ok; then + device_ok=1 + else + echo "[android-e2e] device $device channel dead; reconnecting adb" >&2 + adb -s "$device" reconnect >/dev/null 2>&1 || true + sleep 2 + if timeout 15 adb -s "$device" shell 'echo ok' 2>/dev/null | grep -q ok; then + device_ok=1 + else + echo "[android-e2e] restarting adb server" >&2 + adb kill-server >/dev/null 2>&1 || true + sleep 1 + adb start-server >/dev/null 2>&1 || true + timeout 60 adb -s "$device" wait-for-device 2>/dev/null || true + if timeout 15 adb -s "$device" shell 'echo ok' 2>/dev/null | grep -q ok; then + device_ok=1 + fi + fi + fi + if (( device_ok )); then + # adb reverse rules die with transport flaps even when the device + # itself stayed up — re-add the db-sync tunnel before retrying. + if [[ -n ${local_backend_port:-} ]]; then + adb -s "$device" reverse "tcp:$local_backend_port" "tcp:$local_backend_port" >/dev/null 2>&1 || true + fi + # Clear stale Maestro instrumentation that may still hold the + # UiAutomation binding from the failed attempt. + adb -s "$device" shell am force-stop dev.mobile.maestro >/dev/null 2>&1 || true + return 0 + fi + # The emulator process is gone or hung — kill it if still running, + # then relaunch the runner's AVD. + local emulator_bin avd + emulator_bin="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-}}/emulator/emulator" + avd=$("$emulator_bin" -list-avds 2>/dev/null | head -n 1) + [[ -n $avd ]] || return 1 + echo "[android-e2e] relaunching emulator @$avd" >&2 + timeout 30 adb -s "$device" emu kill >/dev/null 2>&1 || true + sleep 2 + adb kill-server >/dev/null 2>&1 || true + adb start-server >/dev/null 2>&1 || true + nohup "$emulator_bin" "@$avd" -no-window -no-audio -no-boot-anim \ + -gpu swiftshader_indirect -no-snapshot-save >/dev/null 2>&1 & + timeout 600 adb -s "$device" wait-for-device || return 1 + timeout 180 adb -s "$device" shell \ + 'while [ "$(getprop sys.boot_completed)" != "1" ]; do sleep 2; done' \ + || return 1 + # A fresh boot loses the app and the db-sync tunnel. + adb -s "$device" install -r \ + "$repo_root/flutter/build/app/outputs/flutter-apk/app-profile.apk" \ + >/dev/null + if [[ -n ${local_backend_port:-} ]]; then + adb -s "$device" reverse "tcp:$local_backend_port" "tcp:$local_backend_port" + fi + return 0 +} + for flow in "${flows[@]}"; do echo "==> $flow" if [[ $flow = /* ]]; then @@ -457,13 +580,33 @@ for flow in "${flows[@]}"; do ) fi adb -s "$device" logcat -c >/dev/null 2>&1 || true - if ! MAESTRO_CLI_NO_ANALYTICS=1 maestro "${maestro_args[@]}" "$flow_path"; then - # OCaml lui_* FFI exceptions and [NativeEffect] drain traces land in - # logcat — dump it so a wedged pipeline is diagnosable from CI output. - echo "==> $flow failed — device logcat follows" >&2 - adb -s "$device" logcat -d -v brief 2>/dev/null | tail -n 400 >&2 || true - exit 1 - fi + # A previous flow's Maestro instrumentation (dev.mobile.maestro) can + # linger and keep the UiAutomation binding — the next driver session + # then waits the whole startup budget for a binding it can never get. + # Force-stop the stale driver before each flow. + adb -s "$device" shell am force-stop dev.mobile.maestro >/dev/null 2>&1 || true + # The Android driver's default startup budget is only 15s — far too small + # for a loaded CI emulator (it once failed to come up between two flows). + # Per-flow retry additionally covers driver/device hiccups; + # LOGSEQ_CHAT_ANDROID_E2E_RETRIES=0 runs each flow exactly once. + flow_retries=${LOGSEQ_CHAT_ANDROID_E2E_RETRIES:-1} + flow_attempt=0 + while :; do + if MAESTRO_CLI_NO_ANALYTICS=1 MAESTRO_DRIVER_STARTUP_TIMEOUT=300000 \ + maestro "${maestro_args[@]}" "$flow_path"; then + break + fi + flow_attempt=$((flow_attempt + 1)) + if (( flow_attempt > flow_retries )); then + # OCaml lui_* FFI exceptions and [NativeEffect] drain traces land in + # logcat — dump it so a wedged pipeline is diagnosable from CI output. + echo "==> $flow failed — device logcat follows" >&2 + adb -s "$device" logcat -d -v brief 2>/dev/null | tail -n 400 >&2 || true + exit 1 + fi + echo "[android-e2e] $flow failed; retrying ($flow_attempt/$flow_retries)" >&2 + recover_device || echo "[android-e2e] device recovery failed" >&2 + done if [[ $flow == "$sharing_image_flow" ]]; then adb -s "$device" shell run-as "$app_id" rm -f "$app_share_image" fi diff --git a/scripts/test-ios-e2e.sh b/scripts/test-ios-e2e.sh index 870b628..cff04f1 100755 --- a/scripts/test-ios-e2e.sh +++ b/scripts/test-ios-e2e.sh @@ -57,6 +57,25 @@ fi [[ -n $maestro_bin && -x $maestro_bin ]] \ || die "Maestro CLI is not installed. Install it with: brew install mobile-dev-inc/tap/maestro --formula" +# The setup flow drives the Cognito hosted UI in the system browser, where +# hierarchy queries occasionally stall or crash the XCTest driver on a +# loaded runner. Retry the setup once so a driver flake does not fail the +# whole suite; LOGSEQ_CHAT_IOS_E2E_SETUP_RETRIES=0 disables the retry. +setup_retries=${LOGSEQ_CHAT_IOS_E2E_SETUP_RETRIES:-1} +run_setup_flow() { + local attempt=0 + while :; do + if MAESTRO_CLI_NO_ANALYTICS=1 "$maestro_bin" --device "$device" test "$rendered_setup"; then + return 0 + fi + attempt=$((attempt + 1)) + if (( attempt > setup_retries )); then + return 1 + fi + echo "[ios-e2e] setup flow failed; retrying ($attempt/$setup_retries)" >&2 + done +} + device=${LOGSEQ_CHAT_IOS_SIMULATOR_UDID:-} if [[ -z $device ]]; then device=$( @@ -138,7 +157,7 @@ if [[ ${LOGSEQ_CHAT_IOS_E2E_SEED_GRAPH:-0} == 1 || -n $fixture_seed_mode ]]; the mkdir -p "$data_container/Documents" cp -R "$seed_cache_dir/$seed_cache_key/graphs" "$data_container/Documents/" else - MAESTRO_CLI_NO_ANALYTICS=1 "$maestro_bin" --device "$device" test "$rendered_setup" + run_setup_flow data_container=$(xcrun simctl get_app_container "$device" "$app_id" data) graph_database="" for _ in {1..120}; do @@ -170,7 +189,7 @@ if [[ ${LOGSEQ_CHAT_IOS_E2E_SEED_GRAPH:-0} == 1 || -n $fixture_seed_mode ]]; the fi fi if [[ ${flow##*/} == ios-graphs-lifecycle.yaml ]]; then - MAESTRO_CLI_NO_ANALYTICS=1 "$maestro_bin" --device "$device" test "$rendered_setup" + run_setup_flow MAESTRO_CLI_NO_ANALYTICS=1 \ "$maestro_bin" --device "$device" test "$rendered_graphs_lifecycle_fixture" fi diff --git a/shared/native/logseq_chat_core_ffi.c b/shared/native/logseq_chat_core_ffi.c index bed63b8..375b282 100644 --- a/shared/native/logseq_chat_core_ffi.c +++ b/shared/native/logseq_chat_core_ffi.c @@ -15,6 +15,14 @@ #include #define LOGSEQ_CHAT_LOG(...) \ __android_log_print(ANDROID_LOG_ERROR, "logseq_chat", __VA_ARGS__) +#elif defined(__APPLE__) +#include +#define LOGSEQ_CHAT_LOG(...) \ + do { \ + char logseq_chat_log_buf[2048]; \ + snprintf(logseq_chat_log_buf, sizeof logseq_chat_log_buf, __VA_ARGS__); \ + os_log_error(OS_LOG_DEFAULT, "%{public}s", logseq_chat_log_buf); \ + } while (0) #else #define LOGSEQ_CHAT_LOG(...) \ do { \ @@ -96,10 +104,13 @@ const char *logseq_chat_call(const char *request_json) { } needs_unregister = 1; } + LOGSEQ_CHAT_LOG("rpc enter"); caml_acquire_runtime_system(); + LOGSEQ_CHAT_LOG("rpc acquired"); response = call_ocaml(request_json); + LOGSEQ_CHAT_LOG("rpc done"); caml_release_runtime_system(); if (needs_unregister) { caml_c_thread_unregister(); @@ -358,10 +369,13 @@ static const char *call_lui_extension_event( CAMLreturnT(const char *, response); } -#define LUI_RUNTIME_CALL(expression) \ +#define LUI_RUNTIME_CALL(entry_name, expression) \ + LOGSEQ_CHAT_LOG("lui %s enter", entry_name); \ int registration = acquire_ocaml_runtime(); \ if (registration < 0) { return lui_thread_registration_failed(); } \ + LOGSEQ_CHAT_LOG("lui %s acquired", entry_name); \ const char *response = (expression); \ + LOGSEQ_CHAT_LOG("lui %s done", entry_name); \ release_ocaml_runtime(registration); \ return response @@ -369,63 +383,66 @@ const char *logseq_chat_lui_initialize( int32_t platform_code, int32_t host_code, int32_t authentication_code) { - LUI_RUNTIME_CALL(call_lui_initialize( + LUI_RUNTIME_CALL("initialize", call_lui_initialize( platform_code, host_code, authentication_code)); } const char *logseq_chat_lui_appear(int64_t node) { - LUI_RUNTIME_CALL(call_lui_int("logseq_chat_lui_appear", node)); + LUI_RUNTIME_CALL("appear", call_lui_int("logseq_chat_lui_appear", node)); } const char *logseq_chat_lui_press(int64_t node) { - LUI_RUNTIME_CALL(call_lui_int("logseq_chat_lui_press", node)); + LUI_RUNTIME_CALL("press", call_lui_int("logseq_chat_lui_press", node)); } const char *logseq_chat_lui_long_press(int64_t node) { - LUI_RUNTIME_CALL(call_lui_int("logseq_chat_lui_long_press", node)); + LUI_RUNTIME_CALL("long_press", call_lui_int("logseq_chat_lui_long_press", node)); } const char *logseq_chat_lui_text_changed(int64_t node, const char *text) { - LUI_RUNTIME_CALL(call_lui_text(node, text)); + LUI_RUNTIME_CALL("text_changed", call_lui_text(node, text)); } const char *logseq_chat_lui_submit(int64_t node) { - LUI_RUNTIME_CALL(call_lui_int("logseq_chat_lui_submit", node)); + LUI_RUNTIME_CALL("submit", call_lui_int("logseq_chat_lui_submit", node)); } const char *logseq_chat_lui_toggle_changed(int64_t node, int32_t checked) { - LUI_RUNTIME_CALL(call_lui_bool(node, checked)); + LUI_RUNTIME_CALL("toggle_changed", call_lui_bool(node, checked)); } const char *logseq_chat_lui_change(int64_t node) { - LUI_RUNTIME_CALL(call_lui_int("logseq_chat_lui_change", node)); + LUI_RUNTIME_CALL("change", call_lui_int("logseq_chat_lui_change", node)); } const char *logseq_chat_lui_value_changed(int64_t node, double value) { - LUI_RUNTIME_CALL(call_lui_double(node, value)); + LUI_RUNTIME_CALL("value_changed", call_lui_double(node, value)); } const char *logseq_chat_lui_dismiss(int64_t node) { - LUI_RUNTIME_CALL(call_lui_int("logseq_chat_lui_dismiss", node)); + LUI_RUNTIME_CALL("dismiss", call_lui_int("logseq_chat_lui_dismiss", node)); } const char *logseq_chat_lui_double_press(int64_t node) { - LUI_RUNTIME_CALL(call_lui_int("logseq_chat_lui_double_press", node)); + LUI_RUNTIME_CALL("double_press", call_lui_int("logseq_chat_lui_double_press", node)); } const char *logseq_chat_lui_extension_event( int64_t node, const char *identifier, const char *name, const char *text, int64_t value) { LUI_RUNTIME_CALL( + "extension_event", call_lui_extension_event(node, identifier, name, text, value)); } int64_t logseq_chat_lui_root_node(void) { int64_t node = -1; + LOGSEQ_CHAT_LOG("lui root_node enter"); int registration = acquire_ocaml_runtime(); if (registration < 0) return node; + LOGSEQ_CHAT_LOG("lui root_node acquired"); const value *callback = caml_named_value("logseq_chat_lui_root_node"); if (callback == NULL) { LOGSEQ_CHAT_LOG("OCaml LUI callback is not registered: " @@ -445,24 +462,26 @@ int64_t logseq_chat_lui_root_node(void) { } const char *logseq_chat_lui_dispose(void) { - LUI_RUNTIME_CALL(call_lui0("logseq_chat_lui_dispose")); + LUI_RUNTIME_CALL("dispose", call_lui0("logseq_chat_lui_dispose")); } const char *logseq_chat_lui_take_effect(void) { - LUI_RUNTIME_CALL(call_lui0("logseq_chat_lui_take_effect")); + LUI_RUNTIME_CALL("take_effect", call_lui0("logseq_chat_lui_take_effect")); } const char *logseq_chat_lui_resolve_effect(int64_t effect_id, int32_t succeeded, const char *message) { - LUI_RUNTIME_CALL(call_lui_resolve_effect(effect_id, succeeded, message)); + LUI_RUNTIME_CALL("resolve_effect", call_lui_resolve_effect(effect_id, succeeded, message)); } const char *logseq_chat_lui_apply_snapshot(const char *response_json) { - LUI_RUNTIME_CALL(call_lui_string("logseq_chat_lui_apply_snapshot", response_json)); + LUI_RUNTIME_CALL("apply_snapshot", call_lui_string("logseq_chat_lui_apply_snapshot", response_json)); } const char *logseq_chat_lui_apply_host_update(const char *kind, const char *payload_json) { - LUI_RUNTIME_CALL(call_lui_two_strings( - "logseq_chat_lui_apply_host_update", kind, payload_json)); + LUI_RUNTIME_CALL( + "apply_host_update", + call_lui_two_strings( + "logseq_chat_lui_apply_host_update", kind, payload_json)); } diff --git a/shared/test/logseq_chat/repro_wedge_test.ml b/shared/test/logseq_chat/repro_wedge_test.ml new file mode 100644 index 0000000..e746907 --- /dev/null +++ b/shared/test/logseq_chat/repro_wedge_test.ml @@ -0,0 +1,42 @@ +(* Temporary repro: drive the sign-in resolve path headlessly to see if + Signal.stabilize loops on the post-sign-in view. *) + +let ios_profile () = + Lui_protocol.profile Lui_protocol.IOS Lui_protocol.SwiftUIHost + +let graphs = + [ + { Model.id = "g1"; name = "alpha"; is_encrypted = false; is_ready = true }; + { Model.id = "g2"; name = "beta"; is_encrypted = true; is_ready = true }; + ] + +let sign_in_resolve () = + let session = Drive_scenario_test.mount ~profile:(ios_profile ()) () in + let app = session.Drive.Session.app in + ignore (Lui_app.send app (Model.ApplyAuthentication ("signedOut", None))); + ignore (Lui_app.flush app); + ignore (Lui_app.send app Model.SignIn); + ignore (Lui_app.flush app); + let model = Drive.Session.read_model session in + let sign_in_id = + List.find_map + (fun eff -> match eff with Model.SignInEffect id -> Some id | _ -> None) + (model.Model.pending_effects @ model.Model.in_flight_effects) + |> Option.value ~default:(-1) + in + Alcotest.(check bool) "sign-in effect enqueued" true (sign_in_id >= 0); + (* mirror the real drain: applySnapshot of the effect's response, then + resolveEffect — with a populated catalog so the graph picker renders + real rows through the same flush *) + ignore + (Lui_app.send app + (Model.ApplyCoreSnapshot + (Drive_scenario_test.catalog_projection graphs))); + ignore (Lui_app.flush app); + ignore (Lui_app.send app (Model.DequeueEffect sign_in_id)); + ignore (Lui_app.send app (Model.ResolveEffect (sign_in_id, true, ""))); + ignore (Lui_app.flush app); + Drive.Session.dispose session + +let cases = + [ Alcotest.test_case "sign-in resolve" `Quick sign_in_resolve ] diff --git a/shared/test/logseq_chat/test_main.ml b/shared/test/logseq_chat/test_main.ml index 6a26bdc..1d4bc88 100644 --- a/shared/test/logseq_chat/test_main.ml +++ b/shared/test/logseq_chat/test_main.ml @@ -43,4 +43,5 @@ let () = "rpc", Rpc_test.cases; "app", App_test.cases; "drive", Drive_scenario_test.cases; + "repro", Repro_wedge_test.cases; ] diff --git a/tests/e2e/android-local-graph-setup.yaml b/tests/e2e/android-local-graph-setup.yaml index 33e6891..090972e 100644 --- a/tests/e2e/android-local-graph-setup.yaml +++ b/tests/e2e/android-local-graph-setup.yaml @@ -60,7 +60,8 @@ appId: com.logseq.chat - tapOn: id: "button.graph-add.confirm" - extendedWaitUntil: - visible: "android-e2e-smoke" + visible: + id: "button.composer.expand" timeout: 60000 - extendedWaitUntil: visible: diff --git a/tests/e2e/ios-local-graph-setup.yaml b/tests/e2e/ios-local-graph-setup.yaml index 97d6224..9fe313a 100644 --- a/tests/e2e/ios-local-graph-setup.yaml +++ b/tests/e2e/ios-local-graph-setup.yaml @@ -29,6 +29,17 @@ appId: com.logseq.chat # during it have stalled the XCTest driver on CI. Wait it out. - waitForAnimationToEnd: timeout: 30000 +# Cognito hosted UI lives in the system browser, where each hierarchy +# query can stall tens of seconds on a loaded runner (and once crashed the +# XCTest driver). Give the `when` below real headroom instead of burning +# its whole budget on two stalled snapshots. +- extendedWaitUntil: + visible: "Enter your username" + # Each system-browser snapshot can take ~90s+ on a loaded runner (the + # driver retries the WebView query several times), so the budget must + # cover multiple snapshot rounds, not just one. + timeout: 240000 + optional: true - runFlow: when: visible: "Enter your username"