Skip to content

Session Pro on Desktop: the same fifteen specs, and three mobile specs that were not asserting - #127

Open
mpretty-cyro wants to merge 5 commits into
fix/serialise-wda-buildfrom
feat/desktop-pro-specs
Open

Session Pro on Desktop: the same fifteen specs, and three mobile specs that were not asserting#127
mpretty-cyro wants to merge 5 commits into
fix/serialise-wda-buildfrom
feat/desktop-pro-specs

Conversation

@mpretty-cyro

Copy link
Copy Markdown
Collaborator

Brings Desktop up to the same fifteen Session Pro specs that iOS and Android already run, and fixes
three mobile specs that were passing without asserting what they claimed.

Note

Stacked on #126, so only the five commits below belong to this PR.
Needs session-ios#740 — the iOS countdown locator matches an accessibility identifier added there.
The Desktop app-side changes are session-desktop#1979.

Results

Desktop 15/15, 2.0 min
iOS 8/8 message length, 4/4 changed Pro specs
Android 4/4 changed Pro specs, 2/2 two-device message length

Three mobile specs were not asserting what they claimed

Found reviewing the fifteen for correctness rather than for whether they pass.

  • The subscribed and expired Pro settings specs asserted only presence, so an app rendering the
    active-only sections on an expired plan passed both. Each now asserts the other state's rows are
    absent — that is what distinguishes the screens.
  • Neither pin-limit spec verified anything was pinned. The pin icon is asserted only on Android,
    so the iOS half proved nothing beyond "no CTA appeared" — equally true if pinning silently did
    nothing. Both now assert conversation order, and the non-Pro spec asserts the over-limit
    conversation was not pinned: an app that showed the CTA and pinned anyway satisfied the old
    assertion.
  • The message-length specs sent to Note to Self, so they only proved the sender's composer allows
    the longer limit. Over-standard-length messages carry Pro features the receiving client
    validates, so a missing proof lets the sender send happily while nothing arrives. The sending cases
    now use a second device and assert the recipient rendered the message.

Plus the iOS countdown locator, which matched an unscoped xpath built by interpolating the expected
number — so the absence cases built a selector for the literal undefined and passed
unconditionally
. Two specs (1799 and 9799 chars) have never actually asserted anything on iOS
until now.

Desktop harness

@pro is derived from a spec declaring a Pro context rather than hand-declared, so the tag cannot
disagree with the setup. Restarts relaunch a window on the same user-data directory, because Session
Desktop asks the Pro backend for status only at startup. The local file server is mapped through —
worth noting it needs the server's Ed25519 key where the mobile clients need its X25519 one; both
are 64 hex characters, so the wrong one fails no format check.

makeAccountPro moves to run/shared/ and drops its two mobile type dependencies, so Desktop
consuming it is not a layering violation.

Reviewing

Intermediate commits are not individually buildable — the grant move and the specs that import it
span commits, and splitting them further needed hunk-level staging. The branch tip is what was
verified.

makeAccountPro mints against the Pro backend rather than driving a device, so it was already
platform-neutral - but it lived in run/test/utils/, alongside open_app and capabilities_ios, where
a Desktop import reads as reaching into the mobile harness.

Moved to run/shared/, next to the other platform-neutral helpers, and decoupled from the two mobile
types it referenced: the user parameter becomes a structural ProAccountUnderTest that mobile's User
satisfies as-is, and the platform union is inlined. The module now imports nothing from
run/test/utils/, so Desktop consuming it is not a layering violation.
…erver

Four pieces the Desktop Pro specs need, none of which existed:

Pro mocks (run/desktop/pro_mocks.ts) translate the shared ProMockContext vocabulary into the env
session-desktop reads, so the same two fields mean the same thing in a Desktop spec as in a
bothPlatformsIt one. SESSION_PRO is implied by asking for a Pro state at all - forgetting it renders
an empty screen rather than erroring. The run's ambient SESSION_PRO is preserved when a test asks for
no Pro context, so the pre-existing message-length cases keep taking availability from the run.

The @Pro tag is derived from that context rather than declared per test, so it cannot disagree with
what the spec actually sets up - mobile's hand-written isPro can.

Restarts (run/desktop/restart.ts) relaunch a window on the same user-data directory. Session Desktop
asks the Pro backend for status once, at startup, so a grant made while the app is running is
invisible until it comes back up. The old process must be killed first: Electron's single-instance
lock means a second launch against the same NODE_APP_INSTANCE exits instead of opening a window,
which surfaces as firstWindow timing out rather than as a launch error. It lives outside DesktopWrapper
to keep the invariant that the wrapper never launches or kills Electron.

The local file server is mapped from the harness's own FILE_SERVER_URL plus a new
FILE_SERVER_ED_PUBKEY. Two keys for one server and they are not interchangeable: the mobile clients
need the X25519 key for onion encryption, Desktop needs the Ed25519 one because it embeds it in the
returned URL for the download leg to re-derive from. Both are 64 hex characters, so the wrong one
fails no format check.

communityRooms is plumbed through to match sessionIt, so getCommunities() behaves the same on both
platforms against a local SOGS.
Adds the Pro settings locators, the badge toggle, the edit-profile-picture Pro badge and the picked-
image preview, plus the verbs the specs drive them with: subscribeToPro, waitForProActive,
enableProBadge, uploadProfilePicture.

Two things here are load-bearing rather than incidental:

CTA feature rows render their bullet as an icon-font glyph inside the same text node as the copy, so
innerText carries a private-use character. Stripping that range lets the shared cross-platform CTA
table be compared verbatim - without it the failure is unreadable, because the glyph has no width in
a terminal and expected and actual print identically.

A CTA with only a negative button is legitimate - an acknowledgement rather than an offer - but
checkCTAStrings reads buttons positionally, so collapsing them asserted Close against a confirm
button that does not exist.

The uploadProfilePicture flow waits for the picked image's preview, not for the Save button: Save
reports itself enabled before the image is processed and handleUpload then returns silently on
!avatarChanged, so an early click is swallowed and nothing clicks again. Telling the upload path from
the upsell-CTA path is a race between the two outcomes rather than a fixed wait, which would
otherwise cost its full duration on every Pro account.
The same fifteen the mobile suite runs: the four Pro settings states, both pinned-conversation
limits, the four Pro message lengths, the three animated display picture cases and the Pro badge a
second client verifies.

Each spec asserts the ABSENCE of the other state's rows as well as the presence of its own. That is
what distinguishes the screens - presence alone would pass an app rendering both - and the same
applies to the pin limits, which assert the pin actually took rather than only that no CTA appeared.

Three notes on where Desktop genuinely differs:

There is no app-open expiry CTA here. handleTriggeredCTAs returns early when fromAppStart is true and
fires off a stored flag, so no status mock can produce it. Raised as a product divergence rather than
worked around.

The Pro settings status banner renders in every state, empty on success, so it is matched with its
text - a bare presence assertion on that id cannot fail.

The Pro message-length and badge specs take a real grant rather than a display mock: an
over-standard-length message carries Pro features the RECIPIENT validates, so without a proof it is
composed and sent happily and never arrives.
Three gaps found reviewing the fifteen Pro specs for correctness rather than for whether they pass.

The subscribed and expired Pro settings specs asserted only the presence of their own rows, so an app
rendering the active-only sections on an expired plan passed both. Each now asserts the other state's
rows are absent, which is what distinguishes the screens.

Neither pinned-conversation limit spec verified anything was pinned. The pin icon is asserted only on
Android, so the iOS half proved nothing beyond "no CTA appeared" - equally true if pinning silently
did nothing. Both now assert the conversation order, which works on either platform, and the non-Pro
spec additionally asserts the over-limit conversation was NOT pinned: an app that showed the CTA and
pinned anyway satisfied a CTA-only assertion.

The message-length specs sent to Note to Self, so they only ever proved the sender's composer allows
the longer limit. Over-standard-length messages carry Pro features the receiving client validates, so
a missing or invalid proof lets the sender compose and send happily while nothing arrives - which
Note to Self cannot show. The cases that send now use a second device and assert the recipient
rendered the message; the two that cannot send need no recipient and stay on one device.

The iOS countdown locator matched an unscoped xpath built by interpolating the expected number, so the
absence cases built a selector for the literal "undefined" and passed unconditionally. It now matches
the character-limit-text identifier and reads the count from label - the identifier owns name.
Needs session-ios#740.
@mpretty-cyro
mpretty-cyro force-pushed the feat/desktop-pro-specs branch from eb8ffb2 to 24e5504 Compare August 13, 2026 05:32
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