From 86767bf923b504ed70cfc69d1c4098fcb3c4caf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentino=20Me=C4=91imorec?= Date: Thu, 22 Oct 2026 17:09:00 +0200 Subject: [PATCH 1/3] Sync offered a week after setup; Android monkey in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Valentino Međimorec --- .github/workflows/ci.yml | 82 +++++++++- CHANGELOG.md | 2 + apps/desktop/e2e/README.md | 29 +++- apps/desktop/e2e/android-monkey.mjs | 242 ++++++++++++++++++++++++++++ apps/desktop/e2e/appimage.mjs | 17 +- apps/desktop/e2e/flows.mjs | 3 - apps/desktop/e2e/layout.mjs | 13 +- apps/desktop/e2e/webdriver.mjs | 29 +++- apps/desktop/src/App.tsx | 21 +-- apps/desktop/src/SyncPanel.tsx | 38 ++++- apps/desktop/src/VaultGate.tsx | 2 +- docs/development.md | 4 +- docs/features/sync.md | 2 +- docs/features/vault-and-unlock.md | 6 +- 14 files changed, 452 insertions(+), 38 deletions(-) create mode 100644 apps/desktop/e2e/android-monkey.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7825d1a..d1dbda0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,11 +6,17 @@ name: CI # macOS and Windows builds, the slow ones, only run for pull requests into main and manual runs. # The branch rules require these checks by name: "Lint, test, live SSH and VPN", "End to end # (Linux)", "Android build", "Desktop" and "Signed off"; renaming a job means changing the -# rules too. +# rules too. "Android monkey" isn't required: a random walk can find a bug the pull request +# didn't make. on: pull_request: branches: [develop, main] workflow_dispatch: + inputs: + monkey_seed: + description: "Android monkey: the seed to repeat (empty for a random one)" + required: false + default: "" concurrency: group: ci-${{ github.ref }} @@ -303,3 +309,77 @@ jobs: version=$(sed -n 's/^version = "\(.*\)"/\1/p' ../../Cargo.toml | head -1) npx tauri android build --apk --target aarch64 --config "{\"version\":\"$version\"}" + # Android's monkey on the app in an emulator: rounds of random touches, swipes, turns and keys, + # each from a fresh start, over a vault with hosts, keys and a snippet. A crash, a hang (ANR), a + # Rust panic or an error the page throws fails it; the seed is in the log (Run workflow takes + # one to repeat). A debug build, whose WebView can be driven: apps/desktop/e2e/README.md. + android-monkey: + name: Android monkey + runs-on: ubuntu-24.04 + timeout-minutes: 75 + steps: + - uses: actions/checkout@v7 + + - name: C compiler + run: .github/scripts/need-packages.sh build-essential pkg-config unzip + + - uses: actions/setup-java@v6 + with: + distribution: temurin + java-version: 21 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-linux-android + + - uses: Swatinem/rust-cache@v2 + + - uses: actions/setup-node@v7 + with: + node-version: 22 + cache: npm + cache-dependency-path: apps/desktop/package-lock.json + + - name: Android NDK + run: echo "NDK_HOME=$ANDROID_NDK_LATEST_HOME" >>"$GITHUB_ENV" + + # For the emulator's processor. Line numbers are all a panic needs from debug info, and + # keep the APK small enough to install. + - name: Build + working-directory: apps/desktop + env: + CARGO_PROFILE_DEV_DEBUG: line-tables-only + run: | + npm ci + npx tauri android build --debug --apk --target x86_64 + + - name: KVM for the emulator + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Run + uses: reactivecircus/android-emulator-runner@v2 + env: + MONKEY_SEED: ${{ inputs.monkey_seed }} + with: + api-level: 35 + target: google_apis + arch: x86_64 + profile: pixel_6 + cores: 4 + ram-size: 4096M + disk-size: 6000M + disable-animations: true + emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none + script: ADB="$ANDROID_HOME/platform-tools/adb" node apps/desktop/e2e/android-monkey.mjs apps/desktop/src-tauri/gen/android/app/build/outputs/apk/universal/debug/app-universal-debug.apk target/e2e/android-monkey + + - name: Screenshots and logs + if: always() + uses: actions/upload-artifact@v7 + with: + name: android-monkey + path: target/e2e/android-monkey + retention-days: 7 + if-no-files-found: ignore diff --git a/CHANGELOG.md b/CHANGELOG.md index c749beb..d696dd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ prompt. Write them for users. address filled in, and once your account exists Teams opens with the invite. You can also paste an invite there (Joining a team?). An invite opened while sync is off opens as soon as you turn sync on: no need to open the link again. +- A new vault no longer asks at once whether to turn on sync (the first screen offers New account + and Sign in). If sync is still off a week later, MorphGate asks once. - MorphGate can suggest a strong master password: five random words, wherever you set one. - With saved hosts, MorphGate opens on your Hosts page, with Quick connect beside New host. - Hosts, groups, keys, snippets, tunnels and VPNs are tables you can search and filter, each row's diff --git a/apps/desktop/e2e/README.md b/apps/desktop/e2e/README.md index 4a569ee..1b5b7e5 100644 --- a/apps/desktop/e2e/README.md +++ b/apps/desktop/e2e/README.md @@ -4,10 +4,10 @@ These run the real AppImage, deb and rpm binaries, not a dev build, and drive th | Scenario | What it checks | | --- | --- | -| `appimage.mjs` | Creates a vault; SSHes to a throwaway local sshd and runs a command; imports two hosts and their key from a `~/.ssh/config` (one behind the other as its jump host) and connects through it; imports a PuTTY session with a new key in a `.ppk` file, a Termius-style CSV file with a group and a Royal TS document, and signs in with the PuTTY key; connects Vallic Cloud through a stand-in console (`vallic-console.mjs`: device sign-in, rotating tokens, an environment that is this sshd) and signs in to its host; adds AWS and DigitalOcean through stand-in CLIs (`clouds/`); adds a feed (`host-feed.mjs`) whose host connects with the host key it lists, whose wrongly listed host is refused and whose changed snippet asks before it runs; signs in with a key that only has a certificate from the run's user CA (which sshd trusts), from the vault and from an agent; reaches a host by a command its jump host runs (with a login command instead of the shell), and another by a proxy command on this computer (both the relay in `crates/mg-ssh/tests/relay.py`); selects two hosts, moves them into a group and deletes them; signs in to a host with one key from an `ssh-agent` (its own socket) and forwards it (`ssh-add -l` on the host lists it); splits a tab into three panes (beside and below, one through a jump host), broadcasts a command to all of them and then types into one, drags a border and closes them; opens a `morphgate://connect` link from a second launch (which hands it over and exits) and connects from the filled-in quick connect, and refuses an unknown link; refuses a tampered update twice without touching the AppImage; installs the signed update, restarts, unlocks the same vault as the new version, and finds nothing newer | +| `appimage.mjs` | Creates a vault, which isn't offered sync then but is a week later (the page reloaded with the time moved on), once; SSHes to a throwaway local sshd and runs a command; imports two hosts and their key from a `~/.ssh/config` (one behind the other as its jump host) and connects through it; imports a PuTTY session with a new key in a `.ppk` file, a Termius-style CSV file with a group and a Royal TS document, and signs in with the PuTTY key; connects Vallic Cloud through a stand-in console (`vallic-console.mjs`: device sign-in, rotating tokens, an environment that is this sshd) and signs in to its host; adds AWS and DigitalOcean through stand-in CLIs (`clouds/`); adds a feed (`host-feed.mjs`) whose host connects with the host key it lists, whose wrongly listed host is refused and whose changed snippet asks before it runs; signs in with a key that only has a certificate from the run's user CA (which sshd trusts), from the vault and from an agent; reaches a host by a command its jump host runs (with a login command instead of the shell), and another by a proxy command on this computer (both the relay in `crates/mg-ssh/tests/relay.py`); selects two hosts, moves them into a group and deletes them; signs in to a host with one key from an `ssh-agent` (its own socket) and forwards it (`ssh-add -l` on the host lists it); splits a tab into three panes (beside and below, one through a jump host), broadcasts a command to all of them and then types into one, drags a border and closes them; opens a `morphgate://connect` link from a second launch (which hands it over and exits) and connects from the filled-in quick connect, and refuses an unknown link; refuses a tampered update twice without touching the AppImage; installs the signed update, restarts, unlocks the same vault as the new version, and finds nothing newer | | `package.mjs` (deb, rpm) | The binary knows how it was installed, finds the update, and warns that installing asks for an administrator password (installing needs root, so it stops there) | | `teams.mjs` | Two apps side by side (Alice and Bob) with a local reference sync server: both turn on sync; Alice creates an organization and a vault and invites Bob; Bob joins only after the owner's fingerprint matches, and Alice confirms him only after his matches; a host saved in the shared vault reaches Bob, his edit reaches Alice; Bob publishes his SSH key; Alice revokes him (the vault leaves his device) and rotates the vault key; members may create vaults; the activity log tells the story. Then Alice offboards Bob against the local sshd: removal and key rotation, his key found and removed from `authorized_keys`, and the host's shared key replaced (she still logs in) | -| `layout.mjs` | Turns sync on from the offer a new vault gets, fills every page (hosts in a group, a key, a snippet, a tunnel, a team with an invite to a long address), and at 860, 1024, 1280 and 1600 px wide checks that nothing on a page overlaps or runs out of its column. Closing an editor returns to the page it was opened from, and closing a page to the session under it; with no session open, a page takes the tab bar's place | +| `layout.mjs` | Turns sync on from the offer a vault without sync gets a week on, fills every page (hosts in a group, a key, a snippet, a tunnel, a team with an invite to a long address), and at 860, 1024, 1280 and 1600 px wide checks that nothing on a page overlaps or runs out of its column. Closing an editor returns to the page it was opened from, and closing a page to the session under it; with no session open, a page takes the tab bar's place | | `monkey.mjs` | Only with `E2E_ONLY=monkey`: each package (AppImage, deb, rpm) takes `MONKEY_STEPS` seeded random clicks, typing, keys, selects and resizes over a vault with hosts where nothing listens; any page error, blank page, unanswered call or Rust panic fails it, with the seed and the last steps in `monkey-/failure.txt` (`MONKEY_SEED` repeats it) | | `demo.mjs` | Only with `E2E_ONLY=demo`: the README's demo. The AppImage's Hosts page, a click on a host and its recorded session (in a demo shell), drawn by `tools/readme-demo/demo-svg.mjs` as `demo/morphgate.svg` (copy it to `assets/demo/morphgate.svg`) | | `mobile.mjs` | The phone layout in a 390 × 844 window: imports a key and saves a host on the phone screens, connects from the host's sheet, interrupts a command with Ctrl from the extra-key row and repeats one with ↑, keeps the session open behind the host list, browses into a folder over SFTP and downloads a file to Downloads, then locks and unlocks on the phone unlock screen | @@ -20,6 +20,31 @@ E2E_SKIP_BUILD=1 E2E_ONLY=teams apps/desktop/e2e/run.sh # just some scenarios: Screenshots and logs go to `target/e2e/run-