Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5f7cc00
lui migration: foundation (deps, worker/service interfaces, extension…
RCmerci Sep 23, 2026
09a3352
Port worker-session runtime to lui-compatible journal_worker_* modules
RCmerci Sep 23, 2026
aec5d2b
lui migration: port worker service to Journal_worker
RCmerci Sep 23, 2026
aeb2752
lui migration: app-layer shim, platform codec, native bridge
RCmerci Sep 23, 2026
649c88c
lui migration: prefer JOURNAL_APPLE_SDK_ROOT in link flags script
RCmerci Sep 23, 2026
4349970
Port Flutter host from bonsai_flutter to lui_flutter_backend
RCmerci Sep 23, 2026
2742c7c
Port worker test suite to lui service
RCmerci Sep 23, 2026
363befe
Port app test suite to Journal_view/Journal_ids lui shims
RCmerci Sep 23, 2026
ea3356a
Update boundary test for lui flutter host inventory
RCmerci Sep 23, 2026
01f4d98
lui migration: drop deleted macos runtime flow script, refresh gmp to…
RCmerci Sep 23, 2026
ec7da1c
lui migration: regenerate opam lock files without bonsai pins
RCmerci Sep 23, 2026
7fccdb1
Port Apple host + apple-tests from BonsaiSwiftUI to LUIAppleBackend
RCmerci Sep 23, 2026
ba9224a
Drop stale bonsai cases from the macos regression + list harnesses
RCmerci Sep 23, 2026
9c49591
Port app/application.ml from Bonsai to the Lui_app reducer architecture
RCmerci Sep 23, 2026
a8edcda
lui migration: format touched files, update boundary invariants for L…
RCmerci Sep 23, 2026
62289a8
lui migration: link libsqlite3 for the OCaml complete object
RCmerci Sep 23, 2026
1a0d7a7
build app via 'dune build @macos-app'; fold SDK lib path into static …
RCmerci Sep 23, 2026
ce189e7
add dune alias @ios-app; make ios-simulator build lg-free and fix .ap…
RCmerci Sep 23, 2026
c599a0d
fix .app signing: drop invalid adhoc keychain group on macOS; plain-a…
RCmerci Sep 23, 2026
b957534
Fix startup stall: bring journal view into lui schema compliance
RCmerci Sep 23, 2026
5676639
Restore platform request failure delivery so OCaml never waits forever
RCmerci Sep 23, 2026
d9544ac
Update iOS sim testing notes: keychain trap, failure-delivery path, d…
RCmerci Sep 23, 2026
f56e37b
Support real signing for iOS sim builds via JOURNAL_IOS_TEAM_ID
RCmerci Sep 23, 2026
6c80e5d
Record DEVELOPMENT_TEAM=K378MFWK59 in iOS sim testing notes
RCmerci Sep 23, 2026
b41b35d
Fix iOS sim golden path: domain-lock wakeup, render-loop dedup, schem…
RCmerci Sep 23, 2026
716067d
docs: update iOS sim testing skill — embedded entitlements + LUI runt…
RCmerci Sep 23, 2026
b006f8e
Make OCaml emit exceptions loud in journal_lui_bridge.c
RCmerci Sep 23, 2026
46e941b
view: align iOS rendering with the bonsai-era UI
RCmerci Sep 23, 2026
c7c35ec
deps: bump lui pin to merged main (menu-item + icon-fit fixes)
RCmerci Sep 23, 2026
300f975
Align bar chrome and stop journal-list layout storm
RCmerci Sep 23, 2026
4447884
ui: apply the 40pt icon cell to collapsed leaves only
RCmerci Sep 23, 2026
fb45172
deps: pin lui to 6035f9e (covers menu-item expansion, spinner, menu-i…
RCmerci Sep 23, 2026
597f3f2
deps: pin lui to b343560; adapt bar glyphs to the bordered default
RCmerci Sep 24, 2026
00ac54f
Mount bottom-bar capsules as lui toolbar inside box chrome
RCmerci Sep 24, 2026
b9a8b37
ios: hoist journal chrome into native toolbars
RCmerci Sep 24, 2026
6f2e733
ios: bump lui pin to toolbar artifact regeneration
RCmerci Sep 24, 2026
a336995
ios: bump lui pin to rebased toolbar branch
RCmerci Sep 24, 2026
9f6eae2
ios: bump lui pin to merged toolbar support
RCmerci Sep 24, 2026
2134793
lui: pin to 271fe33 (dyn remounts via reconcile_subtree)
RCmerci Sep 24, 2026
74f1c08
lui: pin to merged 38ada4a (dyn remounts via reconcile_subtree)
RCmerci Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 47 additions & 13 deletions .agents/skills/testing-ios-simulator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,52 @@ description: How to build, sign, install, and drive the logseq_journal iOS app o

# Testing logseq_journal on the iOS Simulator

## Build environment
## Build environment (lui stack, branch devin/lui-migration)

- The `bonsai-ui` opam switch (`eval $(opam env --switch=bonsai-ui --set-switch)`) already has most deps; the blueprint's `logseq-journal` switch may be bare. `opam install --deps-only --with-test --dry-run .` shows what's missing; typically only `bonsai_swiftui`, `bonsai_swiftui_test`, `bonsai_swiftui_tool`, `eio_main`.
- `bonsai-swiftui` CLI comes from `bonsai_swiftui_tool` (opam), repo `~/repos/bonsai-ui` must be at the pinned rev (check `bonsai-swiftui.sexp` / blueprint).
- If `bonsai-swiftui build ios --simulator` fails with "The iOS Simulator switch ... is incomplete: missing dune": run `bonsai-swiftui toolchain install iossimulator` (~15-25 min, installs ~240 cross pkgs into `~/.opam/bonsai-swiftui-ios-simulator`).
- If build fails with "Reachable SDK package ocaml-ios64-simulator ... missing from logseq_journal.opam.locked": the lockfile lacks the simulator SDK entry — add `"ocaml-ios64-simulator" {= "5.1.1"}` next to `"ocaml-ios64"`.
- DISK SPACE: a 103MB debug.dylib + DerivedData needs several GB free. With <500MB free, codesign fails with "internal error in Code Signing subsystem" (misleading — it's just ENOSPC). Check `df -h` first; `xcrun simctl delete <unused-udid>` and `opam clean` free space quickly.
- OCaml switch: `eval "$(opam env --switch=5.5.0 --set-switch)"` (or `logseq-journal` — check `opam switch list`).
- The lui checkout must exist at `~/repos/lui` (blueprint clones it; swift/Package.swift reads `JOURNAL_LUI_PACKAGE_PATH`, default `../../lui/platform/apple`).
- Build the .app: `dune build @ios-app` — it runs
`vtool -set-build-version 7 <minos> <sdk> -replace -output journal_complete_iossim.o native_embed.exe.o`
then `tool/build_journal_apple.sh ios-simulator` → `_build/apple/ios-simulator/LogseqJournal.app`.
- Producing the sim complete object manually (equivalent to what @ios-app does):
1. `export JOURNAL_APPLE_SDK_ROOT=$(xcrun --show-sdk-path); dune build app/native_embed.exe.o` (macOS object)
2. `vtool -set-build-version 7 26.0 26.5 -replace -output journal_complete_iossim.o _build/default/app/native_embed.exe.o` (platform 7 = IOSSIMULATOR)
3. `JOURNAL_OCAML_OBJECT=$PWD/_build/default/app/journal_complete_iossim.o tool/build_journal_apple.sh ios-simulator`
- A "proper" cross-link with the target toolchain's ocamlopt is NOT currently feasible: target `ld` rejects host-built `.cmx` ("building for iOS Simulator, but linking in object file built for macOS"). The vtool restamp of the merged macOS complete object works because it is a single `ld -r` object.
- iOS-sim OCaml toolchain provisioning: `LG_IOS_DEPLOYMENT_TARGET=26.0 ac_cv_func_pipe2=no ac_cv_func_dup3=no ac_cv_func_shmat=no ~/repos/lg/scripts/lg-mobile setup ios simulator` — the `ac_cv_*` overrides are REQUIRED: configure's link-check finds `pipe2`/`dup3`/`shmat` in libSystem.tbd but iOS headers don't declare them, and without the overrides crossopt fails in `pipe_unix.c`.
- `journal_lui_bridge.o` inside `_build/default/app/` is only a compile check — safe to ignore.

## Signing / entitlements
## Signing / entitlements (iOS 26.5+ simulator — verified 26.5 AND 27.0)

- `bonsai-swiftui build ios --simulator` produces an unsigned app (`CODE_SIGNING_ALLOWED=NO`, empty entitlements). Empirically on iOS 26.5 simulator this app still signs in, unlocks the E2EE graph, syncs, and uploads — keychain -34018 did NOT reproduce. If it does fail, the adhoc rebuild documented in the blueprint works:
`xcodebuild -project apple/BonsaiLogseqJournal.xcodeproj -scheme BonsaiLogseqJournal-iOS -configuration Debug -destination 'generic/platform=iOS Simulator' -derivedDataPath apple/DerivedData -clonedSourcePackagesDirPath _build/bonsai-swiftui/dependencies/packages -disableAutomaticPackageResolution -onlyUsePackageVersionsFromResolvedFile -skipPackageUpdates ARCHS=arm64 CODE_SIGNING_ALLOWED=YES CODE_SIGN_IDENTITY=- build`
- If you must sign manually: use `.../BonsaiLogseqJournal-iOS.build/BonsaiLogseqJournal.app-Simulated.xcent` (contains FAKETEAMID application-identifier + keychain-access-groups), NOT `.app.xcent` (empty) and NOT the raw `config/entitlements/*.entitlements` (unexpanded `$(AppIdentifierPrefix)`). Do not remove `BonsaiLogseqJournal.debug.dylib` — the 59KB main binary is just a launcher for it; removing it makes the app fail to launch ("did not return a process handle").
- **Any `codesign --entitlements` blob makes the binary fail to exec on the iOS 27.0 sim** — `simctl launch` reports "No such process" / "Launchd job spawn failed". Verified with: the build script's expanded `keychain-access-groups=[com.logseq.journal]`, a `FAKETEAMID.`-prefixed variant with `application-identifier`, and even `get-task-allow` alone — ALL fail at exec. Only linker-signed (no entitlements) or plain-adhoc (`codesign --sign -` without `--entitlements`) binaries launch.
- Launchable recipe: flat .app = `Info.plist` + raw swift product `swift/.build/arm64-apple-ios-simulator/debug/JournalApp`, NO codesign. iOS bundles are flat — a stray `Contents/` dir makes `simctl install` fail with "Missing bundle ID".
- **SOLVED — embedded entitlements**: the sim reads entitlements from the `__TEXT,__entitlements` section, NOT the code signature (signature-carried entitlements are validated as *macOS* entitlements and get the exec killed, error 163 — that was the old "no entitlement-bearing binary can exec" trap). `tool/build_journal_apple.sh ios-simulator` now passes `-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __entitlements` (like Xcode) with `application-identifier` + `keychain-access-groups`, then signs plain-adhoc. Result: `dune build @ios-app` produces a launchable binary whose keychain WORKS — Amplify sign-in, `localAccount`, E2EE graph-key storage all succeed with NO Apple certificate. DEVELOPMENT_TEAM default baked in is `K378MFWK59` (override with `JOURNAL_IOS_TEAM_ID`; the value is also baked into the section so it does not need a matching signature).
- A signing identity still helps for real-device runs: an Apple Development cert was installed for dev@logseq.com (Logseq Inc., team K378MFWK59) — `security find-identity` lists it.
- macOS app caveats: (a) `config/entitlements/macos-debug-profile.entitlements` historically shipped literal `$(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)` — killed at exec (Killed:9, error 163). (b) Plain-adhoc macOS builds DO reach the sign-in dialog (generic-password reads to the app's own group work), but Amplify sign-in still fails -34018 for its access-group keychain ops; re-signing adhoc WITH `keychain-access-groups=[com.logseq.journal]` → AMFI spawn kill (error 163). No signing identity exists on the box → Cognito sign-in cannot complete on macOS either.
- Startup wakeup storm (observed on iOS): ~25-30k `wakeup` callbacks in the first ~40s (~700/s) — OCaml cross-thread pump enqueue floods the MainActor task queue; the first `platformRequest` delivery waits behind the flood (~25s delay), then the system settles to 0% CPU. Not fatal, but adds startup latency and floods instrumented logs.
- **Dead-end-on-all-failures signature (post-5676639)**: with `journal_ocaml_platform_failure` delivering Errors, OCaml's `managed_startup` ignores them (`Error _ -> Effect.ignore`), so `Reconcile_authenticated_user` never reaches the graph_service → `state.manager = None` → the root emits `timeline_page` with an INCOMPLETE tree → the `journal-chrome` extensions get fewer children than their guards require (`JournalChrome.View` renders `EmptyView` unless childIDs.count==3 for `feedback` / ==4 for `journal`) → whole subtree invisible → **blank screen that is a settled dead-end, not a hang** (idle process, no pending continuations). Diagnose by logging patch heads in `JournalRuntime.apply` and counting `insert-child` ops per extension id vs the guard counts in `JournalChrome.swift:91-123`.

## Install / launch / record
## LUI runtime gotchas (found 2026-09-23, golden path verified)

- `xcrun simctl install booted <path>.app`; `xcrun simctl launch booted com.example.bonsaiFlutterLogseqJournalHost` (bundle id from bonsai-swiftui.sexp ios section).
- `open -a Simulator` shows the window; `xcrun simctl io booted recordVideo out.mov` records device-only video (SIGINT to stop); `xcrun simctl io booted screenshot out.png`.
- **Worker→UI wakeup deadlock (iOS)**: an OCaml `Condition` waiter thread calling `deliver` deadlocks — on wake it holds the shared output mutex while re-acquiring its domain lock, which the UI thread holds parked in CFRunLoop. Fix is `Worker.Private.set_output_wakeup`: the wakeup hook runs on the *producing* domain and hops straight onto the UI pump. Same class of bug as the earlier STW starvation — never block a host-domain systhread inside the worker mailbox path.
- **Full-remount render loop**: `Lui_elements.dyn`'s switch uses `equal=(fun _ _ -> false)` — EVERY publish remounts the entire tree (~30+ node ids/cycle). Mount-time emitters feed it: fresh `SecureField`/`Input` fire `TextChanged("")`, `journal-asset-settings` fires its `.task` `deliver()` on each mount, and naive reducers that rebuild identical records keep it alive (~10Hz, 100% CPU). Defenses now in place — `Signal.cutoff ( == )` on the model signal, `Editor.apply_text_edit` returns `None` for identical documents, `days:` extension handler gated by `!asset_settings <> Some settings`, e2ee `Text_edit` guarded by `==`. If a new mount-echo emitter appears, the same dedup pattern applies at the reducer.
- **`V.empty ()` mounts NOTHING (returns node 0)** — extension children are positional on the native side. `journal-chrome` Swift view requires exactly 3 children for `mode:feedback` and 4 for `mode:journal`; an "absent" slot must still mount a real (zero-size) node — `V.column []`/`V.column [x]` placeholders — or `childIDs.count` collapses and the host renders `EmptyView` (blank screen, zero errors).
- **`list-item` rows are inert without `press-enabled`** — `Navigation_link` must set `Lui_protocol.PressEnabled` (and mount the label as a child: `list-item` requires text or children, else the backend fatals).
- **Property-value vocabularies are small**: `RoleValue` accepts only `treeitem|navigation|navigation-heading` (NOT toolbar placements like `bottom_bar`); `VariantValue` only `default|primary|secondary|outline|ghost|destructive` (`plain`→`ghost`, `prominent`→`primary`). Unsupported values throw in `set_prop` during emit.
- **Emit errors were swallowed**: `emit_patch` in `journal_lui_bridge.c` returns 0 on exception → partial op stream → silently blank screens. To diagnose schema violations, add `caml_format_exception(Extract_exception(result))` print there and run a lui build with detailed `set_prop`/`insert_child` messages (local lui branch `devin/set-prop-error-detail` has them: kind/property/value and parent/child kinds — candidate for upstream PR).
- **Extension `standardChildren` must match actual children**: `journal-media`/`journal-list` mount standard children, so `standard_children=true` + the `children` extension-kind whitelist must stay in sync across `journal_lui_native.ml`, `JournalExtensions.swift`, and `journal_extension_registry.dart` (fingerprint mismatch → `unsupported child kind` at emit).
- **Password entry races**: typing into a remount-per-publish field loses focus / stale `''` echo can wipe it. Paste instead: `printf '%s' "$PW" | pbcopy && xcrun simctl pbsync host <udid>`, click field, `cmd+v`.
- **V.Sheet modal presentation freezes on the iOS sim** (menu open + sheet present → stuck overlay, 0% CPU, input dead; same hazard class as the old bonsai render loop). Workaround for sim-only modal testing: render `modal` content inline (`V.column [base; V.Navigation_stack.create ...]` in `application.ml`'s sheet wrapper) — reducer paths are identical. NEVER commit the patch.

## Install / launch / record / diagnose

- `xcrun simctl install <udid> <app>`; `xcrun simctl launch <udid> com.logseq.journal`.
- stderr capture: `xcrun simctl launch --console-pty <udid> com.logseq.journal > console.log` — the app writes no os_log output of its own; stdout/stderr is the only channel. `xcrun simctl launch` accepts trailing `KEY=VALUE` env pairs (e.g. `OCAMLRUNPARAM=v`).
- `open -a Simulator` shows the window; `xcrun simctl io <udid> recordVideo out.mov` (SIGINT to stop); `xcrun simctl io <udid> screenshot out.png`.
- Parked-app signature: `ps -o %cpu` ≈ 0 steady; `sample <pid>` shows the OCaml worker domain in `domain_thread_func → camlIomux__Poll$poll_689 → caml_iomux_poll → poll()` with only a unix socket + self-pipe in `lsof` and no TCP — the app is idle-waiting, not computing.
- LJP2 traffic instrumentation (temporary edits): `JournalRuntime.swift` `platformRequest`/`wakeup` closures and `JournalApplicationPlatform.request`/`services.response` — `FileHandle.standardError.write("[LJP2-DBG] ...")` at each boundary shows whether OCaml issues requests and whether responses return.
- After `simctl install`, resolve the data container fresh via `xcrun simctl get_app_container <udid> com.logseq.journal data` — app-created files (worker dirs, sqlite) appear under `Library/Application Support`; an empty container means the worker never reached storage.
- `%cpu` is cumulative: steady ~100% = render loop; ~3% or less = idle.

## Seeding files for the fileImporter (Files picker)

Expand All @@ -44,6 +72,12 @@ It then appears under Browse → "On My iPhone" in the fileImporter picker.
- UI landmarks: detail toolbar paperclip = "Attach file" (journal-asset-import); ellipsis.circle on the detail-root media group = "Attachment actions" (journal-media-actions) with "Replace file…"/"Reuse existing…"; account person icon on Journals root → "Attachment settings" sheet (Recent journal days stepper, uploads list).
- Tap precision: document-picker files select via the icon/thumbnail, not the name label; journal rows navigate via tapping the row's ">" area.

## Known environment traps

- Multiple install/uninstall cycles can leave launchd app records stale — a fresh `xcrun simctl shutdown`+`boot` (or `erase`) clears it; don't confuse this with a bad binary.
- `xcrun simctl spawn <udid> log show --predicate 'process == "JournalApp"'` shows only UIKit-internal messages for this app — its own diagnostics go to stderr only.
- Booting a second simulator device while another app's launchd state is confused can surface "denied by service delegate (SBMainWorkspace)" — retry once SpringBoard is fully up.

## Devin Secrets Needed

- `LOGSEQ_JOURNAL_USERNAME`, `LOGSEQ_JOURNAL_PASSWORD`, `LOGSEQ_JOURNAL_E2EE_PASSWORD`
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
# Generated SwiftUI hosts and native artifact staging
/apple/
/.bonsai-swiftui/
/swift/.build/

# Local environment and cache directories
.env
Expand All @@ -35,3 +36,4 @@
/journal.sqlite3-shm
/journal.sqlite3-wal
/.dir-locals.el
app/journal_complete_iossim.o
Loading