Two apps, three defects that only driving them could find

The relay is asked what it is, and the answer went nowhere. SiteConfig is a
data class whose only constructor property was `baseUrl`; `discovered` was a
`var` in the body, so the generated equals() ignored it. MutableStateFlow
conflates on equality, which made

    _site.value = config.also { it.discovered = found }

a no-op against the value already there. The descriptor was fetched correctly
and discarded. The sign-in screen sat on "Checking what this server uses…"
forever — the one line whose whole job is to say, BEFORE the passphrase is
typed, whether that passphrase leaves the device — and accountProvider stayed
LOCAL for a relay that had just answered SHARED. Now a constructor property,
written with copy(), with the first JVM unit tests the :app module has ever
had. Confirmed live: the screen reads "One Flamenet account, shared with
Flamenet Files."

Two XCUITest queries matched more than one element and so could tap neither:

- "👍" is in the emoji picker's Gestures row, in its Recent row once picked,
  and in ChatView's quick-reaction bar. Picker buttons now carry a
  section-scoped identifier.
- "Sign out" is both the row in ProfileView and the destructive button in the
  confirmation it opens; SwiftUI keeps both in the tree, so the query was
  ambiguous before the dialog was even reached. Both now identified.

Neither was load — both fail the same way on an idle machine.

Also adds completeSignIn to UITestSupport, the counterpart to completeSignUp.
Sign-ups are rate limited at three an hour per address, so a test that only
needs AN identity should reuse one rather than mint another and fail with a
429 that reads like a broken sign-up screen. CrossPlatformUITests takes
FN_TEST_PASSWORD and signs in when given one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-08-25 20:42:21 -04:00
co-authored by Claude Opus 5
parent b051814a6c
commit 053cefbce4
4 changed files with 92 additions and 4 deletions
@@ -172,7 +172,10 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
io.flamenet.client.RelayDiscovery.describe(config.baseUrl)
}
if (_relayInput.value != value) return@launch
_site.value = config.also { it.discovered = found }
// copy(), not a mutation: SiteConfig is a data class and the flow
// conflates on equality, so an in-place write is indistinguishable
// from the value already there and never reaches the UI.
_site.value = config.copy(discovered = found)
}
}
@@ -14,7 +14,22 @@ import java.net.URI
* - **Self-hosted, local** — their relay, its own account table. Not a lesser
* mode: everything works, it simply is not the same login as Files.
*/
data class SiteConfig(val baseUrl: String) {
data class SiteConfig(
val baseUrl: String,
/**
* What the relay said about itself, or null until it has answered.
*
* ⚠️ **A constructor property on purpose.** It used to be a `var` in the
* class body, which left it out of the generated `equals()` — and
* `MutableStateFlow` conflates on equality, so writing back a config that
* differed ONLY by its descriptor was silently dropped. The relay was
* asked, it answered, and the answer went nowhere: the sign-in screen sat
* on "Checking what this server uses…" forever and `accountProvider` stayed
* LOCAL for a shared-accounts relay. Keep it in the constructor and write
* it with `copy(...)`.
*/
val discovered: io.flamenet.client.RelayDiscovery.Descriptor? = null,
) {
enum class AccountProvider {
/** The shared FlameNet account service. One credential across products. */
@@ -65,8 +80,6 @@ data class SiteConfig(val baseUrl: String) {
* Defaults to LOCAL until the descriptor has been fetched, which is the
* safe direction: the passphrase goes only to the server the user typed.
*/
var discovered: io.flamenet.client.RelayDiscovery.Descriptor? = null
val accountProvider: AccountProvider
get() = if (discovered?.usesSharedAccounts == true) {
AccountProvider.SHARED