Skip to content

fix: let the WebDriverAgent build be serialised against other native builds - #126

Open
mpretty-cyro wants to merge 26 commits into
mainfrom
fix/serialise-wda-build
Open

fix: let the WebDriverAgent build be serialised against other native builds#126
mpretty-cyro wants to merge 26 commits into
mainfrom
fix/serialise-wda-build

Conversation

@mpretty-cyro

Copy link
Copy Markdown
Collaborator

Preparing simulators runs a full xcodebuild of WebDriverAgent. On a machine where something else may
be compiling a Session client at the same time, that is two native builds at once — the load pattern
that pushes a host into swap and produces test timeouts indistinguishable from product bugs.

BUILD_LOCK_CMD is an optional command prefix that runs the build under whatever mutex the host uses.
Unset by default, so a plain checkout and CI are unaffected.

Note: This is based on #124

`resolveDevnetServices` already probes the backend and validates its key, so this
only decides whether the clients are told about it, keyed off the same
`TEST_PRO_BACKEND` switch Desktop uses.

Both values travel together: the pubkey is what libSession verifies other users'
proofs against, so a device given the QA URL on the production key reads a
QA-signed proof as invalid and stores the sender as non-Pro.

Android's extras are persisted rather than applied live, so the app is relaunched
once after the session starts and before the spec is handed the device.
The derivation is pinned to libsession's committed test vectors, runnable
without a device or a backend via `pnpm test-pro-keys`.

`makeAccountPro` also now checks that the recovery phrase it was handed belongs
to the account under test. The dev route creates a `users` row for whatever key
it is given, so a wrong-account mint answers `redeemed=true` with a real expiry
and only surfaces later as the client reporting `never`.

Mints against the discovered QA backend rather than the compiled-in production
URL, which has no `/dev` routes by design.
Locators match on accessibility id rather than display text so one set serves
both platforms. Two traps are documented where they sit: an id lands on iOS as
the element's `name`, pushing display text to `label`, and a combined
accessibility element can swallow what is being matched -- the status banner
reports a progress value instead of its message, and the conversation header
collapses the Pro badge out of the tree entirely.

The settings-state specs drive the launch-arg mocks, which are display-level and
per-device: they cover how the UI renders Pro state without a backend. The badge
visibility spec needs a real grant instead, because the assertion is another
user's client verifying a cryptographic proof.

`Pro Activated CTA` moves to the mocks on iOS for the same reason. That also
removes a race the real grant cannot win: the client skips a status refresh if
the last one was under a minute ago, and an account's first fetch necessarily
happens during onboarding, before there is anything to buy.
`pnpm create-emulators <n>` creates and boots N Pixel 6 / API 34 AVDs, pins them
to the udids the suite expects, patches the camera to virtualscene and writes the
SDK paths to `.env`. Appium will not boot emulators itself.

`setup_virtual_scene` reads ANDROID_SDK_ROOT but never loaded `.env`, so the
documented step only worked if the shell happened to export it.

Also adds `pnpm test-pro-keys`, the device-free Pro key derivation check.
Every other account a test touches is generated per run and discarded, so a
grant is disposable. The moderation account is a fixed identity reused by every
run and job, on a Pro backend that is shared and never reset — so granting it Pro
would be permanent and would renew on every re-run.

The account-under-test check cannot catch this: a spec that deliberately passes
the admin phrase derives a matching account id and sails through.
Room operations are minutes apart -- allocate at the start of a test, release at
the end -- so a pooled keep-alive socket is routinely closed by uWSGI before it
is picked up again, and the reuse surfaced as `read ECONNRESET` on a request the
server never saw. `Connection: close` removes the pool, so there is no socket to
go stale; retrying would not do, as `POST /rooms` is not idempotent.

`fetch` also reported every transport failure as a bare `TypeError: fetch failed`
with the detail hidden in `cause`, which Playwright never prints -- refused,
reset, timed out and DNS were indistinguishable. The cause is now folded into the
message along with the request that provoked it.
`adb` is not on PATH -- which is why APPIUM_ADB_FULL_PATH exists -- so every
`adb` call in `utilities.ts` failed, including the force-stop/restart pair.
`runScriptAndLog` logs a failure and returns rather than throwing, so this was
silent: on Android the app was never restarted, never re-fetched its Pro status,
and stayed on whatever it had cached from before the grant.

That took out every Android spec that mints Pro and restarts to observe it.
The signing key regenerates on container recreate, and a stale one is not a
connection error -- the client reads every proof as invalid and silently strips
Pro content, which reads as an app bug.

The local backend also runs on a compressed clock, so proofs live ~300s rather
than ~30 days. It is written into config.ini by the entrypoint rather than set as
an environment variable, so `docker inspect` reports the opposite.
`scripts/ci.sh` declared the API level, image target, build tools and device
profile a second time, with nothing keeping them in step with the local script.
Both now read `scripts/android_config.ts`, which is the only place a version
changes. The ABI stays detected rather than declared, since it is a property of
the host rather than a choice, and a wrong one fails immediately instead of
drifting.

Every ci.sh function moves behind a flag: --provision, --snapshot-save,
--start-snapshots and --kill.

Two behaviour changes on CI. Emulators are now started with `-port` rather than
relying on start order, so a leftover emulator can no longer renumber the fleet
and leave the suite talking to the wrong device. And the AVDs take the name
prefix the local script already used, so --provision has to run once per worker
to recreate them.
The New Message screen is a bottom sheet, and `scrollDown` is a raw swipe rather
than a scroll-container operation, so it dragged the sheet itself. Next stayed
present and findable -- the click therefore succeeded and threw nothing -- while
the tap landed outside its clickable region. No error, no navigation, nothing in
the device log.

That took out every Android spec reaching a conversation this way: the eight
message-length specs went 0/8, and 7/8 now pass (the eighth fails during
onboarding, unrelated). iOS was unaffected, where the same gesture acts on a
scroll view.

The swipe was vestigial in any case -- it predates the Next button learning to
avoid the keyboard, and `community_links` and `ons_resolve` already clicked Next
without it. `hideKeyboard` states the original intent instead, and is
best-effort because both drivers throw when there is no keyboard up and "nothing
to dismiss" is the desired end state.
`getAttribute('value')` is iOS-only. UiAutomator2 has no `value` attribute and
rejects the request outright, so the Android run died in the driver before any
assertion could report anything useful.

Reading the toggle and asserting the badge move to `pro_badge.ts`, so the next
spec needing either gets the platform difference handled rather than repeating
it -- badge visibility is a separate per-user setting that a grant never
touches, so anything asserting a peer can see a badge has to turn it on first.
`@session-foundation/mnemonic` was already present as a transitive dependency,
so making it direct costs nothing and replaces a hand-rolled base-1626 decoder
-- along with the wordset, the prefix matching and the checksum word, which are
now its problem rather than ours. `pnpm test-pro-keys` still matches
libsession's committed vectors.

Also drops the `uuid` dependency, which nothing imported: the only UUID in the
repo already comes from `node:crypto`.
The backend does honour SESH_PRO_BACKEND_PROVIDER_TESTING_ENV; the compose stack
just never sets it, writing provider_testing_env into config.ini directly. The
practical warning is unchanged -- the container env reads as "not in testing
mode" -- but the reason given for it was wrong, which would send anyone checking
to the wrong place.

Also records the shape the mode actually applies, and that Android now has the
renewal floor the other two clients already had.
Nearly every failure in this suite is "element not found", and a screenshot
cannot separate the three things that produce it: the element is absent, it is
present but not hittable, or it is present under a different identifier than the
locator expects. Only the tree distinguishes them.

Attached by path rather than as a body, because an in-memory attachment survives
only if a reporter persists it and the default local reporter does not -- the
tree would exist in the report stream and nowhere a developer could open it.

Devices are also registered as soon as their session exists rather than when the
opener returns. Everything in between -- the QA relaunch, onboarding, the first
wait for the landing screen -- was a blind spot with no screenshot, page source
or device log, because capture is keyed on the registry and nothing was in it
yet. Registration deduplicates by udid so the existing calls become no-ops.

The first capture from it identified an intermittent onboarding failure as an
Android "System UI isn't responding" dialog covering the app, rather than
anything in the client or the locators.
An Android "isn't responding" dialog covers the app, so ordinary elements drop
out of the tree and every lookup fails as "element not found" -- attributed to
whichever spec was unlucky rather than to the machine. The one that prompted this
was com.android.systemui freezing under host load, not Session.

One dialog is dismissed with "Wait" (not "Close app", which would kill the app
under test when the ANR is ours) and the lookup retried. A second is not: it
throws naming the ANR and the device, so a saturated host is reported as a
saturated host in the results instead of being absorbed into a spec failure.
Session_Android now reads `sessionProBackendStatus` / `sessionProLoadingState` as
intent extras, so the mock-driven Pro settings specs are no longer iOS-only.

`ProMockContext` holds the two fields both platforms understand and lives in its
own module rather than in either platform's capabilities, since both consume it
and neither owns it. `IOSTestContext` extends it, which makes the boundary
explicit: exactly two fields cross over and the rest are iOS's.

`Pro settings screen (subscribed)` and `Pro settings entry (expired)` are now
cross-platform. The other two stay iOS-only, with the reasons recorded where
they are declared -- Android has no status-banner identifier, and its
loading/error mocks only apply while the Pro state is forced, so a non-Pro user
cannot reach those banners the way iOS can.

The shared 30-day expiry is a cross-repo constant: iOS takes a timestamp while
Android uses a fixed debug offset, and that offset was moved to match so one
spec can assert one rendered string.
On iOS an accessibility identifier becomes the element's `name` and displaces the
display text into `label`, so `text` -- which reads the value -- silently stops
matching the moment an element gains an id. Asserting "this identifier AND this
message" was therefore inexpressible, and the status banner locator had dropped
to xpath to say it.

A reviewer flagged that xpath as fragile and likely to break if the specs are
reused for a future client, which is fair: it encodes attribute layout rather
than identity. `label` states the intent directly and the locator goes back to an
accessibility id.

The two remaining xpath/uiautomator locators in this file are a different
problem -- `pro-badge-text` and `action-item-subtitle` are deliberately shared
across rows, so they are scoped by parent to disambiguate. Those need unique ids
in the apps, which is raised separately.
`ProBadgeText` renders the display name unconditionally and only the badge icon
behind `if (showBadge)`, so scoping the Android locator to `pro-badge-text`
matched every conversation whether the sender was Pro or not. The assertion could
not fail, which is worse than a missing test because it reads as coverage.

iOS was unaffected -- it matches the Pro wordmark in the label rather than the
element's presence.

NOT verified end to end: the seeder is currently failing to find freshly created
accounts on the local devnet, so the spec cannot reach the assertion. The locator
targets the element that is conditional on Pro, which is the property the old one
lacked.
Both elements now carry unique identifiers in the apps, so neither locator needs
to scope by parent. That removes the last xpath and -android uiautomator
selectors from this file.

The two ids they replace were deliberately shared -- `pro-badge-text` is on
conversation list rows too, `action-item-subtitle` on every settings row -- so
the traversal was disambiguating rather than selecting. A reviewer flagged that
as fragile and likely to break if these specs are reused for a future client,
which is fair: it encoded a layout relationship rather than identity.

The iOS expiry matches on `label` rather than `text`, because the new identifier
takes `name` and displaces the rendered duration onto `label`.
`adb emu kill` alone left `create-emulators` unable to boot afterwards, and the
gap cost real debugging time twice: a stale emulator drifts into producing
failures that look exactly like product bugs, so "restart the emulators" is a
common remedy -- and it silently did not work, so the phantom failures appeared
to survive the fix.

Three things needed doing by hand each time. Processes survive the console kill
and hold the ports. The adb server is left wedged, reporting no devices even once
emulators are up. And `hardware-qemu.ini.lock` / `multiinstance.lock` outlive the
process, after which a fresh emulator boots but never registers -- presenting as
"did not report boot_completed", which reads as a broken image rather than a
stale lock.

All three are scoped to the suite's own AVD prefix, so a developer's emulators
and locks are untouched.
The animation check sampled whatever was on screen, so an avatar still showing
the generated placeholder failed as "not animated" -- which is true and useless.
It is an upload that has not landed yet, not an animation problem, and the two
have different owners.

Worse, the placeholder colour is picked by `sha512(address) % 7`, so a fresh
account shows a different one every run. That made a timing race look like a
per-build regression: it was bisected to an unrelated Android commit on the
strength of a single passing run before repeat runs showed it was flaky all
along.

Now the check waits for the placeholder to go, and the two failures read
differently: a palette colour says the picture never loaded, a static non-palette
image says it loaded but was frozen because Pro was false when it composed.

Three consecutive passes on a spec that was failing roughly one run in three.
…ompt

From API 33 the POST_NOTIFICATIONS prompt fires the first time the app posts a
notification -- whenever a message happens to arrive -- so it lands at an
arbitrary point and covers whatever the spec is doing. The harness only handled
it at one fixed moment during onboarding, so anything later was unhandled.

It surfaced as "element not found" attributed to the step that happened to be
running: three separate failures in one sweep were this one dialog, and it had
also been misread as a Pro settings problem and as an avatar problem.

Granting at app open removes the race rather than reacting to it.
`handleNotificationPermissions` stays correct -- it only clicks the dialog if
present, so it becomes a no-op.

`setNotificationPermission` takes a boolean rather than being a one-way grant, so
a spec that wants to assert the prompt can revoke and relaunch. That has to
happen before the app is in use: revoking a granted runtime permission makes
Android kill the process.
Appium's default `enforceAppInstall: false` skips installation when the package is
already present at the same or newer version. Session's versionCode does not change
between local builds, so a rebuilt APK silently never reached the device and specs ran
against whatever was installed last — while ANDROID_APK, the filename and the version
all still looked correct. Two app-side handovers were verified against a three-build-old
binary before this was found.

Nothing else forced a fresh install: uninstallApp exists but only two specs call it, so
the only thing that ever did was recreating the AVDs, which wipes the app as a side
effect. That is why "restart the emulators" appeared to cure so much.
…ers on both platforms

- `Pro settings entry (expired)` asserted the expiry CTA after tapping into Pro settings,
  which had the causality backwards: the CTA is app-open behaviour that was still on
  screen by the time the spec navigated. On Android it also blocks the route to settings,
  so that shape could never have passed there. Now asserts the CTA at app open, dismisses
  it through its own Cancel button, then checks the renew row behind it.
- New `ProRenewPlanRow` for `pro-settings-renew-plan`, which replaces the update-plan row
  in the expired state on both platforms. No app change was needed; the id already existed
  on each.
- `Pro status checking state` and `Pro status error state` move from `iosIt` to
  `bothPlatformsIt` now that Android exposes `pro-settings-status-banner`. One of the two
  reasons they were parked was never true: the loading/error mock applies to a
  never-subscribed user, so only the missing id was real.
- Correct the claim that these specs need no Pro backend. On Android the fixture overrides
  only the displayed type; refreshState comes from a real get_pro_status, so a stale
  TEST_PRO_BACKEND_ED_PK makes them fail looking like an app bug.
Applies the comment house rule to the comments this branch added.

- Drop narration of superseded spec shapes ("an earlier version did", "were iOS-only
  until") and restate the same facts as current behaviour. Kept rather than deleted where
  a blame reader lands on the superseded commit: `origin/main` is 60/60 merge commits, so
  that history is permanent and reachable.
- Remove line-numbered citations into Session_Android; keep the symbols and the contract
  they establish. Line numbers in another repo rot with nothing here able to detect it.
- Replace the reference to iOS's unmerged refresh-unification branch with the local
  obligation it was standing in for: no mock here writes libSession config, so anything
  gated on config is unreachable from this file.
- `android_config.ts` named `scripts/ci.sh` as a second declaration site; that file no
  longer exists, so the warning now names the failure instead of the files.
…builds

Preparing simulators runs a full `xcodebuild` of WebDriverAgent. On a machine where something else may
be compiling a Session client at the same time, that is two native builds at once — the load pattern
that pushes a host into swap and produces test timeouts indistinguishable from product bugs.

`BUILD_LOCK_CMD` is an optional command prefix that runs the build under whatever mutex the host uses.
Unset by default, so a plain checkout and CI are unaffected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant