> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tuturuuu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Thanh Local End-to-End Environment

> Run the isolated Supabase stack, web, Playwright and mobile simulator/emulator end-to-end tests on the Thanh Mac Studio.

The Thanh Mac Studio (`ssh skr-studio`) hosts a repeatable local end-to-end
environment: an isolated Supabase project with migrations and seed data, the
`apps/web` dev server, Playwright, and the mobile app on a dedicated iOS
Simulator and Android Emulator. Use it to validate changes against real
simulator/emulator runs instead of host-only widget tests.

The entry point is `scripts/thanh-e2e/thanh-e2e.mjs`. It works on any macOS host
with the same toolchain; Thanh defaults are baked in and overridable by
environment variables.

> [!WARNING]
>
> Use Thanh only for this lane. Queue heavy steps (`up`, `seed`, `web`,
> `web-e2e`, `mobile-e2e`) with `ttr resources run --` from a checkout under
> `/Users/thanh/ci-lanes/tuturuuu-*`, and never stop Upskii or other lanes'
> containers, emulators or queue owners.

Android's dummy Firebase input is created exclusively: an existing file or
symlink is preserved, including a dangling symlink. PID state is opened without
following symlinks and classified and read through the same descriptor; malformed
PID values are ignored, while unsafe file types fail explicitly. Run the focused
filesystem regressions with `node --test scripts/thanh-e2e/*.test.mjs`; no simulator
or database setup is required.

## Toolchain

Everything Tuturuuu-specific lives in user space under
`/Users/thanh/ci-lanes/tuturuuu-tools` (`$T`). Source `$T/mobile-env.sh` first;
it sets `DEVELOPER_DIR`, `JAVA_HOME`, `ANDROID_HOME`/`ANDROID_AVD_HOME`,
`PUB_CACHE`, `GRADLE_USER_HOME`, an isolated `XDG_CONFIG_HOME` for Flutter
settings, and `PATH` (Flutter 3.47.7, Temurin 17, Android SDK tools, Bun,
Docker).

| Component | Location |
| - | - |
| Xcode 26.5 + iOS 26.5 runtime | `/Applications/Xcode.app` |
| Flutter 3.47.7 | `$T/flutter-3.47.7-stable-arm64/flutter` |
| JDK 17 (Temurin) | `$T/jdk` |
| Android SDK (platforms 36 + 37, NDK 28.2, API 36 arm64 image) | `$T/android-sdk` |
| AVDs | `$T/android-avd` |
| E2E state (Supabase workdir, env files, logs, screenshots) | `$T/e2e/<project>` |

`xcode-select` on Thanh still points at the Command Line Tools. Dart
native-asset build hooks run with a filtered environment that keeps `PATH` but
drops `DEVELOPER_DIR`, so `$T/bin/xcrun` pins Xcode. Without it, iOS builds fail
in `objective_c/hook/build.dart` with `Bad state: No element`. Remove the shim
after an administrator runs
`sudo xcode-select -s /Applications/Xcode.app/Contents/Developer`.

## Isolation

| Resource | Value | Override |
| - | - | - |
| Supabase project id | `tuturuuu-e2e` | `TUTURUUU_E2E_PROJECT_ID` |
| Supabase ports | `8000`-`8007` (API `8001`, DB `8002`, Studio `8003`, mail `8004`) | `TUTURUUU_E2E_SUPABASE_BASE_PORT` |
| Web dev server | `http://127.0.0.1:7803` | `TUTURUUU_E2E_WEB_PORT` |
| Satellites | Calendar on `http://localhost:7806` | `TUTURUUU_E2E_SATELLITES` (`calendar`, `none`) |
| iOS Simulator | `tuturuuu-ios-iphone17pro` | `TUTURUUU_E2E_IOS_SIM` |
| Android Emulator | `tuturuuu-android-36-pixel9pro` on `emulator-5580` | `TUTURUUU_E2E_AVD`, `TUTURUUU_E2E_AVD_PORT` |

The ports stay canonical on purpose: the web local-E2E auth and captcha bypass
(`apps/web/src/lib/auth/local-e2e.ts`) and the Playwright safety guard
(`apps/web/e2e/helpers/environment.ts`) only trust Supabase `:8001` and web
`:7803` on loopback. With other ports, mobile password sign-in fails with
`Turnstile is not configured`. Isolation comes from the project id instead: the
script refuses the default `tuturuuu` id and `up` fails fast when any port in
the block is already listening, so it never adopts another lane's stack. Only
one stack on the canonical ports can run on a host at a time; stop it with
`down` when you finish. The Supabase config is staged from tracked
`apps/database/supabase` files and rewritten with the isolated helpers from
`apps/database/scripts/run-supabase-isolated.js`.

Docker over SSH cannot use the macOS keychain (`credsStore: desktop`). The
script points `DOCKER_CONFIG` at a credential-free directory in the state dir
and `DOCKER_HOST` at the Docker Desktop socket, so public image pulls work
anonymously and the keychain is never touched.

## Run it

```bash theme={null}
ssh skr-studio
. ~/ci-lanes/tuturuuu-tools/mobile-env.sh
cd ~/ci-lanes/tuturuuu-e2e          # any checkout under ci-lanes/tuturuuu-*
bun install                         # once per checkout
E2E=scripts/thanh-e2e/thanh-e2e.mjs

node $E2E doctor
ttr resources run -- node $E2E up          # start + migrations + seed
ttr resources run -- node $E2E web         # build types/supabase, start web + satellites
node $E2E devices                          # boot simulator + emulator headless
ttr resources run -- node $E2E web-e2e --install -- e2e/auth.noauth.spec.ts
ttr resources run -- node $E2E mobile-e2e ios
ttr resources run -- node $E2E mobile-e2e android
ttr resources run -- node $E2E mobile-e2e android --mode=profile
node $E2E status
node $E2E down            # stop web + Supabase; --web-only, --purge
```

`seed` re-applies migrations and seed data. `env` rewrites
`$T/e2e/<project>/env/web.env` and `mobile.json` from `supabase status`.
`web.log` and other logs live in the state dir's `logs/`.

## Test accounts

The seed creates `local@tuturuuu.com` (root admin) and `user1`-`user4@tuturuuu.com`
with the public local-only seed password from `apps/database/supabase/seed.sql`.
The mobile flow defaults to `local@tuturuuu.com`; override with
`--dart-define=E2E_EMAIL=...` only for other seeded accounts.

## Satellites

Web proxies some owner APIs to satellite apps; Calendar's event API is proxied
to the Calendar app, so without it Agenda shows `ApiException(500): Request
failed` and web logs `Failed to proxy https://calendar.tuturuuu.localhost/...
ECONNREFUSED`. `web` therefore also starts each satellite in
`TUTURUUU_E2E_SATELLITES` (default `calendar`) on its default port with the
same local Supabase env and `WEB_APP_URL`/`INTERNAL_WEB_API_ORIGIN` pointing
back at web; web gets `CALENDAR_APP_URL=http://localhost:7806`. `down` stops
web and satellites. `web` also builds `@tuturuuu/internal-api`, which Calendar
imports from `dist/`.

Satellites run on their Next.js runtime (`next dev`), not Vite+ `dev:app`.
Vite+ dev runs Calendar inside workerd, which loads linked workspace packages
unbundled; `@tuturuuu/internal-api` builds as CommonJS (`NodeNext` without
`"type": "module"`), so startup fails with `ReferenceError: exports is not
defined`, or `Failed to resolve entry for package "@tuturuuu/internal-api"`
before it is built. The Next runtime serves the same route handlers.

### Native Worker imports

Workerd rejects random generation during module initialization. Meet and Parley's
shared chat initializes its unguessable mention marker on first rendering use,
then reuses it for that module instance. Keep the marker lazy; a fixed marker
would let authored links impersonate generated mentions. The import and lifetime
regressions are in `packages/meet-core/src/features/call/lib/assistant-identity-import.test.ts`.
An authenticated native preview returning 500 is not a valid performance sample.

## Mobile wiring

`mobile.json` is passed with `--dart-define-from-file` and routes
`NEXT_PUBLIC_SUPABASE_URL` and every `*_API_BASE_URL` owner to loopback
`127.0.0.1`. Android permits cleartext only to `127.0.0.1`/`localhost`, and the
app rewrites `localhost` to `10.0.2.2`, so the script uses `127.0.0.1` and runs
`adb reverse` for the web and Supabase ports. All satellite API owners point at
the local web server, which owns the live API handlers.

`apps/mobile/integration_test/e2e_flows_test.dart` signs in with the seeded
account, opens Agenda, opens Settings → Preferences, changes the personal
timezone (default `Pacific/Niue`, override with `--dart-define=E2E_TIMEZONE=...`;
pick a zone near the top of the picker, whose list is lazy and lives in a
draggable sheet) and returns to Agenda. It fails if an `ErrorWidget` renders at any
step. At each checkpoint it prints an `E2E_SHOT <name>` marker and holds the
frame; the runner streams `flutter drive` output and captures the device with
`xcrun simctl io ... screenshot` or `adb exec-out screencap`, so screenshots
work in debug, profile and release builds. Shots and `flutter-drive.log` land
in `$T/e2e/<project>/shots/<platform>-<mode>-<timestamp>/`. (On Android,
`IntegrationTestWidgetsFlutterBinding.takeScreenshot` after
`convertFlutterSurfaceToImage` captured only the first frame.)

Before each run the script prepares the gitignored Firebase inputs the same way
mobile CI does: `scripts/mobile-deployment/firebase-service-config.py` copies the
tracked development plist on iOS, and Android gets the synthetic
`google-services.json` CI writes. iOS builds run `pod install`, which can leave
`ios/Podfile.lock` and `project.pbxproj` modified in the E2E checkout; do not
commit those side effects.

`flutter test -d <device> test/...` does not run on the device: files outside
`integration_test/` run on the host VM. Widget tests that resize
`tester.view` also fail under the live binding, so on-device coverage belongs in
`integration_test/`.

## Release-mode gray screen

Release builds replace the debug red error box with a gray `ErrorWidget`. iOS
simulators run debug builds only (Flutter refuses `--profile` and `--release`),
so reproduce release-only rendering on the Android emulator with
`mobile-e2e android --mode=profile` or `--mode=release`. The same `ErrorWidget`
assertion reports the failing screen, and the screenshots show it.

## Web Playwright

`web` and `web-e2e` set `BASE_URL=http://127.0.0.1:7803`, the local Supabase
keys and `TUTURUUU_LOCAL_E2E_AUTH_BYPASS=1`, the same local-only opt-in CI web
E2E uses. `local-e2e.ts` honors it only for the loopback `:7803`/`:8001`
origins; without it the `/api/auth/dev-session` rate-limit reset returns 403
and most auth specs fail in `beforeEach`. `--install` installs the
Playwright Chromium build first. Pass spec paths or Playwright flags after `--`.

## Gotchas

* Browsers must open web at `http://localhost:7803`, not `127.0.0.1`: Next dev
  serves its dev resources (HMR, hydration) only to `localhost`, so a
  `127.0.0.1` page stays unhydrated and the login form remains disabled. The
  script uses `localhost` for `BASE_URL`/`NEXT_PUBLIC_WEB_APP_URL` and keeps
  `127.0.0.1` for mobile, which Android cleartext requires.

* `bootstrap()` installs the app's `FlutterError.onError`. The integration test
  restores the test binding's handler after `app.main()`; otherwise a framework
  error surfaces only as `A test overrode FlutterError.onError ...` and the real
  failure is hidden.

* Mobile login ignores "Continue with email" while OTP availability resolves
  (a short grace period); the test retries the tap until the password stage
  renders.

* Every wait in the flow captures `timeout-<label>.png` and prints the visible
  text on timeout, so a failing run shows the stuck screen.

* Simulator and emulator keyboards are real: the test dismisses the keyboard
  before tapping, and re-enters the email before each Continue attempt because
  the login page can rebuild once startup checks settle. On the Android
  profile build the typed email still did not stick; after 20 seconds the test
  signs in through `AuthCubit.signInWithPassword` (the call the Sign in button
  makes) and logs `E2E_NOTE`, so profile mode still covers Agenda and Settings.

* `mobile-e2e` runs `flutter drive` in its own process group with a 20-minute
  timeout and requires the `All tests passed` line; a killed drive can exit 0.

* The web auth spam guard blocks requests without a native or browser user
  agent; probe mobile APIs with a `Dart/3.x (dart:io)` user agent, not bare
  `curl`.

* A killed `flutter run`/`flutter drive` can leave `adb ... logcat` or
  `simctl ... log stream` children holding stdout open, which pins a
  `ttr resources` slot. Kill only your own leftover streamer.

* `permission_handler_android` compiles against Android SDK 37; keep
  `platforms;android-37.0` installed.

* Plain `./gradlew` tasks need `-Ptarget=lib/main_development.dart` because the
  app has no `lib/main.dart`, and flavored task names such as
  `:app:testDevelopmentDebugUnitTest`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.