From f693019a7c15661ac45c9b7de592d5db77d6d094 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 04:44:30 +0000 Subject: [PATCH 01/25] e2e: dismiss system ANR dialogs during Android flows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two consecutive CI runs failed the very first assertion (button.hosted-sign-in, 30s timeout) with a healthy app rendered behind a 'Pixel Launcher isn't responding' dialog — a system ANR occludes the entire a11y tree so Maestro sees only the dialog window. Start a background watchdog that polls 'uiautomator dump' every 3s and taps the ANR dialog's Wait button; skip with LOGSEQ_CHAT_ANDROID_E2E_SKIP_ANR_WATCHDOG=1. The runner self-test stubs an ANR dump and asserts the Wait tap lands. --- scripts/test-android-e2e-runner.sh | 26 ++++++++++++++++++++++++ scripts/test-android-e2e.sh | 32 ++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/scripts/test-android-e2e-runner.sh b/scripts/test-android-e2e-runner.sh index 01416d0..055e030 100755 --- a/scripts/test-android-e2e-runner.sh +++ b/scripts/test-android-e2e-runner.sh @@ -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 Wait 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..06308e9 100755 --- a/scripts/test-android-e2e.sh +++ b/scripts/test-android-e2e.sh @@ -254,19 +254,51 @@ 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 + wait_bounds=$(printf '%s' "$xml" | tr '>' '\n' \ + | sed -n 's/.*text="Wait"[^>]*bounds="\(\[[0-9,]*\]\[[0-9,]*\]\)".*/\1/p' | head -1) + if [[ $wait_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 system ANR dialog (Wait at $x,$y)" >&2 + adb -s "$device" shell input tap "$x" "$y" >/dev/null 2>&1 || true + fi + fi + sleep 3 + 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" +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]:-} From 7e998979cdd7de6bcaf8451a15fa764cc724d9c7 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 04:46:45 +0000 Subject: [PATCH 02/25] e2e: harden iOS setup against system-browser hierarchy stalls A PR run failed the setup flow after ~98s in 'when: Enter your username': each hierarchy query against the Cognito hosted UI (system browser) was stalling 15-30s+ on the loaded runner, then the XCTest driver threw an unhandled assertion mid-snapshot and Maestro exited 1. Give the condition real headroom via a hoisted optional extendedWaitUntil (120s) and retry the setup flow once so a driver crash does not fail the suite; LOGSEQ_CHAT_IOS_E2E_SETUP_RETRIES=0 opts out. --- scripts/test-ios-e2e.sh | 23 +++++++++++++++++++++-- tests/e2e/ios-local-graph-setup.yaml | 8 ++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) 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/tests/e2e/ios-local-graph-setup.yaml b/tests/e2e/ios-local-graph-setup.yaml index 97d6224..3940b07 100644 --- a/tests/e2e/ios-local-graph-setup.yaml +++ b/tests/e2e/ios-local-graph-setup.yaml @@ -29,6 +29,14 @@ 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" + timeout: 120000 + optional: true - runFlow: when: visible: "Enter your username" From 09e64da472c17132088f387517e11c8c4e6fcf17 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 04:52:03 +0000 Subject: [PATCH 03/25] e2e: widen Cognito hosted-UI wait to 240s on iOS Each Safari WebView hierarchy fetch took ~93s on the loaded runner before returning, so a 120s budget only covered about one snapshot round. 240s gives the condition 2-3 real evaluations. --- tests/e2e/ios-local-graph-setup.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/e2e/ios-local-graph-setup.yaml b/tests/e2e/ios-local-graph-setup.yaml index 3940b07..9fe313a 100644 --- a/tests/e2e/ios-local-graph-setup.yaml +++ b/tests/e2e/ios-local-graph-setup.yaml @@ -35,7 +35,10 @@ appId: com.logseq.chat # its whole budget on two stalled snapshots. - extendedWaitUntil: visible: "Enter your username" - timeout: 120000 + # 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: From 15084677f9e5bfe79b1553d6cc1dfce7597897ca Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 05:09:01 +0000 Subject: [PATCH 04/25] e2e: force-close ANR'd apps instead of tapping Wait + more AVD RAM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The watchdog fired every ~11s for 4+ minutes but the Pixel Launcher ANR kept re-triggering: 'Wait' only postpones the dialog while the process stays hung. Tap 'Close app' instead so Android restarts the hung process (fall back to 'Wait' when absent), suppress background ANR dialogs via 'settings put global anr_show_background 0', and raise AVD RAM to 4G — lowmemorykiller stalls are a plausible trigger under Gradle + emulator CPU contention. Self-test updated to stub and assert the Close app tap. --- .github/workflows/e2e.yml | 3 +++ scripts/test-android-e2e-runner.sh | 4 ++-- scripts/test-android-e2e.sh | 18 ++++++++++++++---- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d94c74c..3544db0 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -493,6 +493,9 @@ jobs: profile: pixel_6 disable-animations: true emulator-boot-timeout: 600 + # Default AVD RAM (~2G) lets the launcher hit lowmemorykiller + # stalls → foreground ANR dialogs that occlude the a11y tree. + emulator-options: -memory 4096 -cores 4 -no-snapshot-save script: | eval $(opam env) export LOGSEQ_CHAT_OPAM_SWITCH="$(opam switch show)" diff --git a/scripts/test-android-e2e-runner.sh b/scripts/test-android-e2e-runner.sh index 055e030..7828ccf 100755 --- a/scripts/test-android-e2e-runner.sh +++ b/scripts/test-android-e2e-runner.sh @@ -409,7 +409,7 @@ fi case "$*" in *"exec-out uiautomator dump"*) printf '%s\n' \ - '' + '' ;; esac EOF @@ -424,7 +424,7 @@ PATH="$mock_bin:$PATH" \ 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 Wait button" + || die "Android E2E runner's ANR watchdog did not tap the dialog's Close app button" PATH="$mock_bin:$PATH" \ ANDROID_SERIAL=test-device \ diff --git a/scripts/test-android-e2e.sh b/scripts/test-android-e2e.sh index 06308e9..bb7f6fa 100755 --- a/scripts/test-android-e2e.sh +++ b/scripts/test-android-e2e.sh @@ -274,12 +274,19 @@ 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 - wait_bounds=$(printf '%s' "$xml" | tr '>' '\n' \ - | sed -n 's/.*text="Wait"[^>]*bounds="\(\[[0-9,]*\]\[[0-9,]*\]\)".*/\1/p' | head -1) - if [[ $wait_bounds =~ \[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\] ]]; then + # "Wait" only postpones the dialog — a genuinely hung process (Pixel + # Launcher on a loaded emulator) re-ANRs forever. "Close app" + # force-stops it so Android restarts it fresh. + close_bounds=$(printf '%s' "$xml" | tr '>' '\n' \ + | sed -n 's/.*text="Close app"[^>]*bounds="\(\[[0-9,]*\]\[[0-9,]*\]\)".*/\1/p' | head -1) + 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) + 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 system ANR dialog (Wait at $x,$y)" >&2 + echo "[anr-watchdog] dismissing system ANR dialog (Close app at $x,$y)" >&2 adb -s "$device" shell input tap "$x" "$y" >/dev/null 2>&1 || true fi fi @@ -295,6 +302,9 @@ if [[ -z $device ]]; then 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 From 2b904d22f0a8214e324f9b4a1cf6a73f6e1c6b12 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 05:28:56 +0000 Subject: [PATCH 05/25] Revert "e2e: raise AVD RAM for launcher ANR headroom" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit emulator-options made the action pick the latest emulator (37.1.11), whose qemu binary fails to load on ubuntu-latest (libpulse.so.0 missing) — the emulator never booted. Keep the Close-app watchdog and anr_show_background settings, which address the observed dialog directly. --- .github/workflows/e2e.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3544db0..d94c74c 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -493,9 +493,6 @@ jobs: profile: pixel_6 disable-animations: true emulator-boot-timeout: 600 - # Default AVD RAM (~2G) lets the launcher hit lowmemorykiller - # stalls → foreground ANR dialogs that occlude the a11y tree. - emulator-options: -memory 4096 -cores 4 -no-snapshot-save script: | eval $(opam env) export LOGSEQ_CHAT_OPAM_SWITCH="$(opam switch show)" From 481482bf3ed18545c38d5381da537986f09b8938 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 05:49:44 +0000 Subject: [PATCH 06/25] e2e: extend Android driver startup budget + retry each flow once android-signed-out went green for the first time in CI, then the next flow died before starting: the Maestro Android driver failed to come up within its 15s default on a loaded emulator. Export MAESTRO_DRIVER_STARTUP_TIMEOUT=180000 and retry each flow once (LOGSEQ_CHAT_ANDROID_E2E_RETRIES=0 opts out). --- scripts/test-android-e2e.sh | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/scripts/test-android-e2e.sh b/scripts/test-android-e2e.sh index bb7f6fa..b8b4f68 100755 --- a/scripts/test-android-e2e.sh +++ b/scripts/test-android-e2e.sh @@ -499,13 +499,27 @@ 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 + # 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=180000 \ + 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 + done if [[ $flow == "$sharing_image_flow" ]]; then adb -s "$device" shell run-as "$app_id" rm -f "$app_share_image" fi From b5f452d034ad29be84f65edd950e9d8f19a60969 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 06:20:08 +0000 Subject: [PATCH 07/25] apple: route patch-apply diagnostics through os.Logger Swift print() never reaches the captured device-simulator.log, so the previous DEBUG-only apply traces were invisible in CI. os_log output lands in the harness log: each applied patch generation, stale-epoch drops (silent wedge suspect), and apply failures. --- apple/Sources/LogseqChat/LGChatRuntime.swift | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/apple/Sources/LogseqChat/LGChatRuntime.swift b/apple/Sources/LogseqChat/LGChatRuntime.swift index e47bcfe..5c6c4c4 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? @@ -563,21 +566,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.info("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)") } } } From fd809c7bcf0b05e3055e7f132ec540386d157d43 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 06:47:46 +0000 Subject: [PATCH 08/25] apple: elevate patch diagnostics to notice/error + trace effect failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit info-level Logger entries are filtered out of the captured simulator log — promote the apply trace to notice, stale-epoch drops to warning (already error-level), and always log effect-executor failures and deduped core responses (both previously invisible). --- apple/Sources/LogseqChat/LGChatRuntime.swift | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/apple/Sources/LogseqChat/LGChatRuntime.swift b/apple/Sources/LogseqChat/LGChatRuntime.swift index 5c6c4c4..5a37b0c 100644 --- a/apple/Sources/LogseqChat/LGChatRuntime.swift +++ b/apple/Sources/LogseqChat/LGChatRuntime.swift @@ -194,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 } @@ -394,14 +395,9 @@ public final class LGChatRuntime { cancelOutlinerAutosaveBeforeExecuting(effect) let resolution = await effectExecutor.execute(effect) - #if DEBUG 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)) @@ -570,7 +566,7 @@ public final class LGChatRuntime { 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.info("apply patch generation=\(Self.patchGeneration(patch) ?? -1, privacy: .public)") + Self.log.notice("apply patch generation=\(Self.patchGeneration(patch) ?? -1, privacy: .public)") try self.renderer.apply(decoded: decoded) self.lastError = nil } catch { From a04894a2bf54aa372b8c02f584896e4f105879e4 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 07:34:59 +0000 Subject: [PATCH 09/25] ffi: route OCaml exception logging through os_log on Apple MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The else-branch fprintf(stderr) never reaches the captured simulator log, so an OCaml exception inside resolveEffect/applySnapshot on iOS is invisible — matching the silent sheet-stuck wedge. os_log_error lands in the unified log that Maestro's debug artifacts capture. --- shared/native/logseq_chat_core_ffi.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/shared/native/logseq_chat_core_ffi.c b/shared/native/logseq_chat_core_ffi.c index bed63b8..701a7ca 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 { \ From 53e53d30aeb4158790f569b25eeb84b6232658f5 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 07:50:59 +0000 Subject: [PATCH 10/25] e2e: raise Android driver startup budget to 300s The loaded emulator took >180s to install and start the Maestro driver this run; the retry then raced the same slow startup. --- scripts/test-android-e2e.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test-android-e2e.sh b/scripts/test-android-e2e.sh index b8b4f68..8b04031 100755 --- a/scripts/test-android-e2e.sh +++ b/scripts/test-android-e2e.sh @@ -506,7 +506,7 @@ for flow in "${flows[@]}"; do flow_retries=${LOGSEQ_CHAT_ANDROID_E2E_RETRIES:-1} flow_attempt=0 while :; do - if MAESTRO_CLI_NO_ANALYTICS=1 MAESTRO_DRIVER_STARTUP_TIMEOUT=180000 \ + if MAESTRO_CLI_NO_ANALYTICS=1 MAESTRO_DRIVER_STARTUP_TIMEOUT=300000 \ maestro "${maestro_args[@]}" "$flow_path"; then break fi From 8fa6afee3f0ccae25b9763a9c184ce966defa026 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 08:47:03 +0000 Subject: [PATCH 11/25] ios: surface core-action boundaries and effect lifecycle in sim log logger.info already bracketed every performAsyncAndWait with 'Core action started/returned', but os_log info is dropped from the captured device-simulator.log (only notice+ survives). Mirror to notice so the stalled action is directly visible, and log the drain's effect start/resolved/startSync boundaries to pin the exact await that wedges post create-graph. --- apple/Sources/LogseqChat/LGChatRuntime.swift | 3 +++ apple/Sources/LogseqChatModel/ViewModel.swift | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apple/Sources/LogseqChat/LGChatRuntime.swift b/apple/Sources/LogseqChat/LGChatRuntime.swift index 5a37b0c..e40ba4a 100644 --- a/apple/Sources/LogseqChat/LGChatRuntime.swift +++ b/apple/Sources/LogseqChat/LGChatRuntime.swift @@ -394,7 +394,9 @@ 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) + Self.log.notice("effect resolved id=\(effect.id, privacy: .public) kind=\(effect.kind, privacy: .public) succeeded=\(resolution.succeeded, privacy: .public)") if !resolution.succeeded { Self.log.error("effect failed id=\(effect.id, privacy: .public) kind=\(effect.kind, privacy: .public) message=\(resolution.message, privacy: .public)") } @@ -419,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 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 } From bcd8fc00ffe9440c2b1f67249f74ecdd9cc7148e Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 09:18:55 +0000 Subject: [PATCH 12/25] ffi: bracket every lui_*/rpc entry with enter/acquired/done logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The iOS wedge leaves the drain stalled with zero diagnostics — the last visible event is 'effect resolved' followed by silence, so the hang is either inside an OCaml call on the executor thread or in caml_acquire_runtime_system itself. Logging at all three boundaries distinguishes them in the next run's device-simulator.log: 'enter' without 'acquired' means the OCaml domain lock was never released by a previous call; 'acquired' without 'done' means the hang is inside the named OCaml entry point. --- shared/native/logseq_chat_core_ffi.c | 47 +++++++++++++++++----------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/shared/native/logseq_chat_core_ffi.c b/shared/native/logseq_chat_core_ffi.c index 701a7ca..375b282 100644 --- a/shared/native/logseq_chat_core_ffi.c +++ b/shared/native/logseq_chat_core_ffi.c @@ -104,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(); @@ -366,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 @@ -377,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: " @@ -453,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)); } From 282b22830af443ca2883156ce2c2b3982c35aa9f Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 09:35:48 +0000 Subject: [PATCH 13/25] ocaml-signal: repin to stabilize round-cap branch + sign-in resolve repro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signal.stabilize has no round cap — a task that unconditionally re-dirties the scheduler loops forever inside Lui_app.flush, the leading hypothesis for the iOS e2e wedge (executor dead inside a native.* FFI call after 'effect resolved'). Repin ocaml-signal to devin/stabilize-round-cap (logseq/ocaml-signal@b9cb81f) which raises Stabilization_limit_exceeded instead — the exception crosses the FFI boundary as lui_exception, surfacing the wedge in logs instead of a silent hang. The headless repro drives the sign-in resolve + snapshot flush sequence through the real app (green locally — the loop, if it exists, is in a different flush). --- logseq_chat.opam | 2 +- logseq_chat.opam.locked | 4 +- shared/test/logseq_chat/repro_wedge_test.ml | 42 +++++++++++++++++++++ shared/test/logseq_chat/test_main.ml | 1 + 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 shared/test/logseq_chat/repro_wedge_test.ml diff --git a/logseq_chat.opam b/logseq_chat.opam index a896962..85f263d 100644 --- a/logseq_chat.opam +++ b/logseq_chat.opam @@ -33,7 +33,7 @@ pin-depends: [ ["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#b076fc66a681ef33a142771f327bb1b53d4400aa"] ["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/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; ] From a022aece553bfaf09873a1cfa2c0a48f0bbe2a27 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 10:45:07 +0000 Subject: [PATCH 14/25] e2e: pin lui_flutter_backend to semantics-freeze fix for validation --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index bf5ef6d..a716698 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: 752af27a6adbc023e1f5a762ce49dfeada3edd96 + resolved-ref: 752af27a6adbc023e1f5a762ce49dfeada3edd96 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 7d4f074..5744288 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: 2a2cc62dd4583734b7b7f6c226c587e950c5985e + ref: 752af27a6adbc023e1f5a762ce49dfeada3edd96 path: platform/flutter webview_flutter: ^4.14.1 From b4b3092b9774b820cf9c942493818675fe606a64 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 12:05:46 +0000 Subject: [PATCH 15/25] e2e: pin lui_flutter_backend to semantics-heal fix (498bd22) for validation --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 3150fb3..6262d57 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -159,8 +159,8 @@ packages: dependency: "direct main" description: path: "platform/flutter" - ref: 88f9b4a4c8fd9f0227656a986655479c495c0232 - resolved-ref: 88f9b4a4c8fd9f0227656a986655479c495c0232 + ref: 498bd2298752ddd8254de4567f77369eec289acb + resolved-ref: 498bd2298752ddd8254de4567f77369eec289acb 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..294736e 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: 498bd2298752ddd8254de4567f77369eec289acb path: platform/flutter webview_flutter: ^4.14.1 From 668c48aaeee9558b4014c09fdde9ebcb25ec3a4a Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 12:43:55 +0000 Subject: [PATCH 16/25] e2e: pin lui_flutter_backend to extended semantics-heal fix (d055aa4) --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 6262d57..c530bf4 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -159,8 +159,8 @@ packages: dependency: "direct main" description: path: "platform/flutter" - ref: 498bd2298752ddd8254de4567f77369eec289acb - resolved-ref: 498bd2298752ddd8254de4567f77369eec289acb + ref: d055aa4b05f6cece2ae28e35a635ae1cdea893d0 + resolved-ref: d055aa4b05f6cece2ae28e35a635ae1cdea893d0 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 294736e..cc2708c 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: 498bd2298752ddd8254de4567f77369eec289acb + ref: d055aa4b05f6cece2ae28e35a635ae1cdea893d0 path: platform/flutter webview_flutter: ^4.14.1 From dbdab796389045c54dbac165a4f5439b8e29f43e Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 13:19:40 +0000 Subject: [PATCH 17/25] e2e: pin lui_flutter_backend to throttled semantics-heal (ed44ebc) --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index c530bf4..32d06ed 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -159,8 +159,8 @@ packages: dependency: "direct main" description: path: "platform/flutter" - ref: d055aa4b05f6cece2ae28e35a635ae1cdea893d0 - resolved-ref: d055aa4b05f6cece2ae28e35a635ae1cdea893d0 + ref: ed44ebc8858e80d9430949fc9ce2aeee5f9901e8 + resolved-ref: ed44ebc8858e80d9430949fc9ce2aeee5f9901e8 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 cc2708c..73001ae 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: d055aa4b05f6cece2ae28e35a635ae1cdea893d0 + ref: ed44ebc8858e80d9430949fc9ce2aeee5f9901e8 path: platform/flutter webview_flutter: ^4.14.1 From bd492684e706d03dc236aa1a1163fa144c5e1408 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 13:50:09 +0000 Subject: [PATCH 18/25] e2e: build Android profile APK + pin lui 62e2e69 (toolbar a11y identifier) - Android e2e runs a profile APK: no debug-mode asserts, so the performLayout/flushSemantics abort that froze the a11y tree (ghost sheet / dropped identifiers) cannot abort mid-emit. AOT build also starts faster, reducing emulator ANRs. - apple Package.swift/Package.resolved + opam pin point at lui 62e2e69 (lui#53): the hoisted toolbar node's identifier now rides a discoverable toolbar item, making toolbar.outliner.editor assertable on iOS. Single SHA for all three lui references. --- apple/Package.resolved | 2 +- apple/Package.swift | 2 +- logseq_chat.opam | 2 +- scripts/test-android-e2e-runner.sh | 4 ++-- scripts/test-android-e2e.sh | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) 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/logseq_chat.opam b/logseq_chat.opam index ba0f1fc..f0eddbc 100644 --- a/logseq_chat.opam +++ b/logseq_chat.opam @@ -31,7 +31,7 @@ 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#b9cb81fc910519fc579a90960267acb851483bca"] ["mldoc.dev" "git+https://github.com/logseq/mldoc.git#553dea6ed8694352527a189747f787365469c9cb"] diff --git a/scripts/test-android-e2e-runner.sh b/scripts/test-android-e2e-runner.sh index 7828ccf..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" \ diff --git a/scripts/test-android-e2e.sh b/scripts/test-android-e2e.sh index 8b04031..434e304 100755 --- a/scripts/test-android-e2e.sh +++ b/scripts/test-android-e2e.sh @@ -325,13 +325,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 From f177c512ecfa7c6aeb6ad7a29a8b72570cd2752b Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 14:17:26 +0000 Subject: [PATCH 19/25] e2e: bump lui_flutter_backend pin for context-menu identifier fix (e3d6f0d) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composer attachment menu items carry accessibility-identifier, which the context-menu validator rejected — the app crashed into the error surface when expanding the composer. lui e3d6f0d adds it to the allow-list, matching the Apple backend. --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 32d06ed..965fcea 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -159,8 +159,8 @@ packages: dependency: "direct main" description: path: "platform/flutter" - ref: ed44ebc8858e80d9430949fc9ce2aeee5f9901e8 - resolved-ref: ed44ebc8858e80d9430949fc9ce2aeee5f9901e8 + ref: e3d6f0d83b8920632020e867817ac228e6f647d7 + resolved-ref: e3d6f0d83b8920632020e867817ac228e6f647d7 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 73001ae..b0610a2 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: ed44ebc8858e80d9430949fc9ce2aeee5f9901e8 + ref: e3d6f0d83b8920632020e867817ac228e6f647d7 path: platform/flutter webview_flutter: ^4.14.1 From 632d725d1cded7e2cd7c0ac5e3b24e9c394eb87e Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 14:47:42 +0000 Subject: [PATCH 20/25] e2e: recover dead adb/emulator between flow retries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last run: the adb connection died after the graph-setup retry and every subsequent flow failed instantly on tcp:closed — the retry loop re-ran against a dead device. recover_device now runs before each retry: adb get-state → kill/start-server + wait-for-device → relaunch the AVD + reinstall the APK + re-establish adb reverse as last resort. --- scripts/test-android-e2e.sh | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/scripts/test-android-e2e.sh b/scripts/test-android-e2e.sh index 434e304..4e5b1c3 100755 --- a/scripts/test-android-e2e.sh +++ b/scripts/test-android-e2e.sh @@ -437,6 +437,40 @@ if [[ -n ${LOGSEQ_CHAT_E2E_BASE_URL:-} ]] \ done fi +recover_device() { + # Retrying a flow against a dead adb server or crashed emulator fails + # identically — recover connectivity before the next attempt. + if adb -s "$device" get-state >/dev/null 2>&1; then + return 0 + fi + echo "[android-e2e] device $device unreachable; 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 + adb -s "$device" get-state >/dev/null 2>&1 && return 0 + # The emulator process itself is gone — 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 + 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 @@ -519,6 +553,7 @@ for flow in "${flows[@]}"; do 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" From 53268288f6925e601fefe0ddcc9daca3f18d0369 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 15:12:45 +0000 Subject: [PATCH 21/25] e2e: fix android-local-graph-setup post-create assert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After button.graph-add.confirm the app navigates straight into the new graph's journals screen — the graph name never renders as text (the flow previously waited 60s for it and timed out). Assert button.composer.expand instead: it's emitted on the loaded journals screen and matches how iOS waits via journals.graph-loaded. --- tests/e2e/android-local-graph-setup.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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: From 2d6b5c85a5b9cf87726d69abcddb99c11831f122 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 15:22:15 +0000 Subject: [PATCH 22/25] e2e: pin flutter backend to semantics-heal v2 + contextmenu id fix a37fb01 = lui devin/1790400000-flutter-no-layoutbuilder-callbacks HEAD (d39569c, heal never-emitted semantics subtrees) plus the context-menu accessibility-identifier allow-list cherry-pick (lui#54). Repin to the merge SHA once both land. --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 965fcea..589fcbc 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -159,8 +159,8 @@ packages: dependency: "direct main" description: path: "platform/flutter" - ref: e3d6f0d83b8920632020e867817ac228e6f647d7 - resolved-ref: e3d6f0d83b8920632020e867817ac228e6f647d7 + ref: a37fb017cff803001f8aafc60514d85cfd8f924d + resolved-ref: a37fb017cff803001f8aafc60514d85cfd8f924d 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 b0610a2..580d34f 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: e3d6f0d83b8920632020e867817ac228e6f647d7 + ref: a37fb017cff803001f8aafc60514d85cfd8f924d path: platform/flutter webview_flutter: ^4.14.1 From 41fbd44f7b90d85c3360bb323bc739ed4d8e146f Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 15:53:08 +0000 Subject: [PATCH 23/25] e2e: robust adb recovery, safer ANR dismissals, semantics-heal v3 pin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - recover_device: probe the adbd channel end-to-end (adb shell echo ok) instead of get-state — the transport can report 'device' while the channel is dead; escalate reconnect -> server restart -> emu kill + relaunch; always re-add adb reverse (rules die with transport flaps). - anr watchdog: log the ANR'd app name and never 'Close app' on system_server/System UI — that soft-reboots the runtime and drops every adb transport mid-suite. - flutter pin -> bbcbd1a: semantics heal v3 (extension subtrees + multi-owner PipelineRoots; covers the journals pane's extension mount) plus contextmenu id allow-list. --- flutter/pubspec.lock | 4 +- flutter/pubspec.yaml | 2 +- scripts/test-android-e2e.sh | 73 +++++++++++++++++++++++++++++-------- 3 files changed, 60 insertions(+), 19 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 589fcbc..5be27b3 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -159,8 +159,8 @@ packages: dependency: "direct main" description: path: "platform/flutter" - ref: a37fb017cff803001f8aafc60514d85cfd8f924d - resolved-ref: a37fb017cff803001f8aafc60514d85cfd8f924d + ref: bbcbd1aa51c92a35a8fcf33b16ffe9cf64b76587 + resolved-ref: bbcbd1aa51c92a35a8fcf33b16ffe9cf64b76587 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 580d34f..de461ea 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: a37fb017cff803001f8aafc60514d85cfd8f924d + ref: bbcbd1aa51c92a35a8fcf33b16ffe9cf64b76587 path: platform/flutter webview_flutter: ^4.14.1 diff --git a/scripts/test-android-e2e.sh b/scripts/test-android-e2e.sh index 4e5b1c3..5c634b0 100755 --- a/scripts/test-android-e2e.sh +++ b/scripts/test-android-e2e.sh @@ -274,19 +274,33 @@ 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 - # "Wait" only postpones the dialog — a genuinely hung process (Pixel - # Launcher on a loaded emulator) re-ANRs forever. "Close app" - # force-stops it so Android restarts it fresh. - close_bounds=$(printf '%s' "$xml" | tr '>' '\n' \ - | sed -n 's/.*text="Close app"[^>]*bounds="\(\[[0-9,]*\]\[[0-9,]*\]\)".*/\1/p' | head -1) + 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 system ANR dialog (Close app at $x,$y)" >&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 @@ -438,23 +452,50 @@ if [[ -n ${LOGSEQ_CHAT_E2E_BASE_URL:-} ]] \ fi recover_device() { - # Retrying a flow against a dead adb server or crashed emulator fails - # identically — recover connectivity before the next attempt. - if adb -s "$device" get-state >/dev/null 2>&1; then + # 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 return 0 fi - echo "[android-e2e] device $device unreachable; 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 - adb -s "$device" get-state >/dev/null 2>&1 && return 0 - # The emulator process itself is gone — relaunch the runner's AVD. + # 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 From 82fb8b45106e988175daa574cab8b9fbbc2bac1f Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 16:24:38 +0000 Subject: [PATCH 24/25] e2e: free stale Maestro UiAutomation binding; soften watchdog poll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Back-to-back driver-startup timeouts on flow N+1 correlate with the previous flow's dev.mobile.maestro instrumentation lingering on the UiAutomation binding — force-stop it before each flow and in recover_device. The 3s uiautomator-dump watchdog adds a11y pressure on top; 10s still catches ANR dialogs inside assert timeouts. --- scripts/test-android-e2e.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/test-android-e2e.sh b/scripts/test-android-e2e.sh index 5c634b0..2755468 100755 --- a/scripts/test-android-e2e.sh +++ b/scripts/test-android-e2e.sh @@ -304,7 +304,10 @@ start_anr_watchdog() { adb -s "$device" shell input tap "$x" "$y" >/dev/null 2>&1 || true fi fi - sleep 3 + # 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=$! @@ -483,6 +486,9 @@ recover_device() { 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, @@ -574,6 +580,11 @@ for flow in "${flows[@]}"; do ) fi adb -s "$device" logcat -c >/dev/null 2>&1 || true + # 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; From 0bb4fc4b24cfd7d984368f64fc1304f742a4b051 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Fri, 25 Sep 2026 18:24:04 +0000 Subject: [PATCH 25/25] flutter: repin lui to 11668eb (anchored-stack semantics boundary fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sibling OverlayPortal anchors under one boundary (attachment + task-status in row.composer.controls) merge their grafted children, keeping only the first identifier — the task-status menu's SemanticsNode never reached the a11y tree. An explicit container Semantics per MenuAnchor keeps each portal's identifier on its own node. --- flutter/pubspec.lock | 4 ++-- flutter/pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 5be27b3..c901071 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -159,8 +159,8 @@ packages: dependency: "direct main" description: path: "platform/flutter" - ref: bbcbd1aa51c92a35a8fcf33b16ffe9cf64b76587 - resolved-ref: bbcbd1aa51c92a35a8fcf33b16ffe9cf64b76587 + 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 de461ea..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: bbcbd1aa51c92a35a8fcf33b16ffe9cf64b76587 + ref: 11668eb7e5e26af8974901c5d158c1c0cafa40b3 path: platform/flutter webview_flutter: ^4.14.1