"Checking what this server uses…" was also the failure state

🔴 **The relay probe had no deadline and no way to say it failed.**
`discovered` is null both while the probe is in flight and after it has
come back with nothing, so `identityDescription` shows "Checking what
this server uses…" in both cases. A relay that never answers leaves the
sign-up form on that sentence permanently — no error, no retry, nothing
to act on.

Found by the instrumented suite on a clean emulator: nine of ten tests
failed with `nav-contacts did not appear within 120000ms`, and the screen
dump showed the form still checking.

⚠️ **The reason it never answered is worth writing down.** The app was
pointed at `http://10.0.2.2:18810`, and Android has blocked cleartext
HTTP by default since API 28 — there is no `usesCleartextTraffic` and no
network security config here. The request never left the device.

🔴 That refusal is CORRECT and is not what changed. `SiteConfig` already
says why: a bare hostname becoming `http://` would hand key material to
anyone on the path "and it would work, which is the worst outcome." What
was wrong is that it failed silently, so somebody self-hosting who omits
the scheme gets a dead screen and no idea why.

Now: bounded at fifteen seconds — far longer than a healthy relay needs —
and a failed probe is a distinct state that says
"Couldn't reach that server. Check the address — and note it has to be
https, not http."

Same defect as android#19 one screen earlier, which I fixed this morning
without noticing the probe had it too.

Also adds `scripts/instrumented.sh`, which closes two ways an on-device
run silently points at production: the suites DEFAULT to live hosts when
no relay is named, and a saved emulator snapshot restores whatever the
AVD was last doing — observed today as a snapshot signed in as
`fnreviewbeta` against relay.flamenet.io, under a run told to use a local
relay. The script has no default, refuses both live hosts by name, cold
boots with `-no-snapshot-load`, and exits 3 on a skip because a skip is
not a pass.
This commit is contained in:
Paul Hitt
2026-09-05 15:41:49 -04:00
parent d2d5a0b496
commit d8d48d8651
3 changed files with 131 additions and 2 deletions
@@ -339,10 +339,23 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
describeJob = viewModelScope.launch {
kotlinx.coroutines.delay(400)
val config = SiteConfig.from(value) ?: return@launch
val found = withContext(Dispatchers.IO) {
io.flamenet.client.RelayDiscovery.describe(config.baseUrl)
// ⚠️ **Bounded.** `describe` has no deadline of its own, and a
// host that accepts a connection and never answers would leave the
// sign-up form on "Checking what this server uses…" indefinitely.
// Fifteen seconds is far longer than any healthy relay needs.
val found = withTimeoutOrNull(RELAY_PROBE_TIMEOUT_MS) {
withContext(Dispatchers.IO) {
io.flamenet.client.RelayDiscovery.describe(config.baseUrl)
}
}
if (_relayInput.value != value) return@launch
if (found == null) {
// Say so rather than leaving the spinner-in-words up. The
// commonest cause is an http:// address, which Android refuses
// outright, so the message names it.
_site.value = config.copy(probeFailed = true)
return@launch
}
// 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.
@@ -3570,6 +3583,9 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
*/
private const val SIGN_IN_TIMEOUT_MS = 60_000L
/** How long to wait for a relay to describe itself before saying it did not. */
private const val RELAY_PROBE_TIMEOUT_MS = 15_000L
/**
* The presence chips, in order.
*
@@ -29,6 +29,18 @@ data class SiteConfig(
* it with `copy(...)`.
*/
val discovered: io.flamenet.client.RelayDiscovery.Descriptor? = null,
/**
* The probe was made and came back with nothing.
*
* 🔴 **Without this, "still asking" and "asked and could not reach it" are
* the same state.** `discovered` is null in both, so a relay that never
* answers leaves "Checking what this server uses…" on screen forever and
* the form permanently unusable — no error, no retry, nothing to act on.
* Seen 2026-09-05 pointing the app at a plain-HTTP relay, which Android
* blocks outright from API 28: the request never left, and the screen said
* it was still thinking about it.
*/
val probeFailed: Boolean = false,
) {
enum class AccountProvider {
@@ -95,6 +107,9 @@ data class SiteConfig(
/** What to tell the user this relay does about identity, in their words. */
val identityDescription: String
get() = when {
probeFailed ->
"Couldn't reach that server. Check the address — and note it has " +
"to be https, not http."
discovered == null -> "Checking what this server uses…"
accountProvider == AccountProvider.SHARED ->
"One Flamenet account, shared with Flamenet Files."