Skip to content

Pro: QA backend override, a proof-acquisition floor, and a unit-test class-init fix - #2153

Open
mpretty-cyro wants to merge 6 commits into
session-foundation:devfrom
mpretty-cyro:feature/pro-backend-override-qa
Open

Pro: QA backend override, a proof-acquisition floor, and a unit-test class-init fix#2153
mpretty-cyro wants to merge 6 commits into
session-foundation:devfrom
mpretty-cyro:feature/pro-backend-override-qa

Conversation

@mpretty-cyro

Copy link
Copy Markdown
Collaborator

Three independent changes, all Pro-adjacent.

QA backend override

Lets a QA Pro backend be targeted without rebuilding, via QaLaunchConfig — matching iOS's
customProBackendUrl / customProBackendPubkey launch variables, so the Appium suite can
point both platforms at the same backend with one switch.

Both values are required together: a QA URL paired with the production signing key reads
every QA-signed proof as invalid and silently strips Pro content.

Proof-acquisition floor

ProProofGenerationWorker had no floor, and the path is a closed loop: a successful
generate force-refreshes get_pro_status, the fetch asks libsession for a renewal target,
and target = proofExpiry - PRO_RENEWAL_LEAD is permanently in the past whenever a proof
lives for less than the 60-minute lead — so it reschedules immediately, forever. Measured at
~9-11 requests/second against a backend issuing 5-minute proofs.

The floor mirrors iOS (SessionProManager.reconcileProofRenewal) and Desktop
(state/ducks/proBackendData.ts) exactly — 60s while covered, 15s × attempt capped at
900s while dark, darkAttempt reset on cover. The constants carry a comment naming them a
cross-client contract so nobody tunes one in isolation.

Two properties worth preserving in any refactor:

  • It re-arms rather than skips, so throttled work still happens, just later.
    target <= now is the normal "renewal due" signal and dropping it would break real
    renewals.
  • The interval is state-dependent, so an entitlement that never arrives — an abandoned
    purchase, or a server-side grant the backend hasn't ingested — settles at 15-minute
    spacing rather than hammering.

Placed at the top of doWork() rather than in the scheduler, which covers every path
that can schedule acquisition (both FetchProStatusWorker sites and
ProStatusManager.onPurchaseInFlight) rather than one branch, and matches where iOS puts
it. State lives in the existing pro_state name/value table, persisted rather than
in-memory because WorkManager can run the worker across process death and an in-memory
counter would reset and defeat the pacing. No migration.

Note: only reachable locally because the QA backend runs on a compressed clock
(provider_testing_env, ~300s proofs). Production's ~30-day proofs sit well outside the
60-minute lead. Worth fixing regardless — it is a real unbounded loop, and the compressed
clock is exactly what CI will run on.

Unit-test class-init fix

Reading a SessionProtocol constant runs System.loadLibrary("session_util"), so doing it
from ProStatusManager's companion initialiser made the class impossible to initialise
wherever the native library is absent — every JVM unit test. Mockito could not instrument
it, and the nine tests constructing a ConversationViewModel failed with
NoClassDefFoundError.

The constants stay single-sourced from libsession; they are just read on first use rather
than on class load.

Testing

assemblePlayDebug and assemblePlayAutomaticQa green. 198/198 unit tests pass (was
189/198 before the class-init fix).

@mpretty-cyro mpretty-cyro self-assigned this Aug 6, 2026
Lets a QA Pro backend be targeted without rebuilding, matching the iOS
customProBackendUrl/customProBackendPubkey launch variables.

Both values are required together: a QA URL paired with the production signing
key reads every QA-signed proof as invalid and silently strips Pro content.
…ation

Reading a SessionProtocol constant runs System.loadLibrary("session_util"), so
doing it from the companion's initialiser made the class impossible to
initialise wherever the native library is absent -- every JVM unit test. Mockito
could not instrument it, and the nine tests constructing a ConversationViewModel
failed with NoClassDefFoundError.

The constants stay single-sourced from libsession; they are just read on first
use rather than on class load.
@jagerman
jagerman force-pushed the feature/pro-backend-override-qa branch from 924aa0b to 5906064 Compare August 6, 2026 18:08
A successful generate force-refreshes get_pro_status, which asks libsession for a
renewal target, and `proofExpiry - PRO_RENEWAL_LEAD` is permanently in the past for
any proof living less than the 60-minute lead -- so the worker rescheduled itself
immediately and looped.

Mirrors iOS SessionProManager.reconcileProofRenewal and Desktop, constants included:
60s while covered, 15s * attempt capped at 900s while dark, and re-arming rather than
dropping the work, since `target <= now` is also the normal renewal-due signal.

The state is in-memory as it is on the other two platforms; a process restart costs
one extra request rather than a loop.
The debug menu already drives these states through preferences that
ProStatusManager and ProSettingsViewModel read; they were just unreachable from
an automated launch, so the Appium suite could only cover Pro screens on iOS.

sessionProBackendStatus and sessionProLoadingState are named for the state being
simulated rather than for the preference behind them, matching the keys iOS
already accepts, so one cross-platform test has one setup that means the same
thing on both. `useActual` clears an override. Values are mapped explicitly
rather than derived from enum names, so renaming a case cannot silently change
what a test asks for.
Two of the three EXPIRING labels claimed 14 days while the code used 2, which is
how a reader (and a test author) ends up with the wrong value: the label looks
authoritative and is the first thing you see.

EXPIRING_LATER moves 40 -> 30 days so both platforms can assert the same
rendered string. It already sat outside the 7-day window that gates the expiring
CTA and still does, so its behaviour is unchanged -- EXPIRING keeps its 2 days
precisely because it is inside that window and is the only way to trigger the
CTA by hand.
It sat on the ComposeView host in XML while the tap target is the Avatar inside
it. Compose publishes its own semantics tree, so whether the host's description
survived depended on composition timing -- intermittently leaving the avatar
unlabelled for accessibility services, and unfindable by anything addressing it
by description.

Removed from the XML rather than left in both places: the same description on
two nodes of one tree is the ambiguity being fixed, not redundancy. Nothing read
it there -- the id is used as a constraint anchor and for setThemedContent only.

@Bilb Bilb left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

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.

2 participants