A fresh peer for each on-device run, not peerjvmone

The suites added `peerjvmone`, one standing account on relay1 whose only
device came online when somebody ran the interop peer by hand. Nobody did.
On relay1 it had no one-time prekeys left, classical or PQ, and 114
messages it would never collect, and every run added more strangers to it.
Adding it and delivering to it failed in some runs and not others.

scripts/instrumented.sh now creates two accounts with the interop peer
before the run and passes them as `contactPeer` and `echoPeer`: one for the
add-and-send cases, one running `autoreply` for the reply case alone, since
an autoreply peer answers the first new account that writes to it. The
peer runs from `:interop:installDist` with plain java, not a second Gradle
build, and the exit trap stops it and waits for it.

The suites no longer fall back to peerjvmone; without the arguments they
say to use the script. The reply case now fails when no echo arrives
instead of logging a warning and passing. FeatureUseTest waited for the
confirm button, which is on screen but disabled until the lookup resolves,
so it tapped a disabled button; it now waits for the lookup result like
the other two suites.

On relay1: 8 of 10. The reply round trip passes. Still red: the group case
("the group never appeared in the list after Create", not yet diagnosed)
and ChatFlowTest's send, which crashes in the app with "performMeasureAndLayout
called during measure layout" at ChatScreens.kt:1740, the keyboard-follow
scroll recorded on 2026-09-18.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-09-28 07:36:15 -04:00
co-authored by Claude Opus 5.5
parent 32a258bc1f
commit cd5d1d8f44
4 changed files with 86 additions and 28 deletions
@@ -54,14 +54,21 @@ class ChatFlowTest {
} else {
"https"
}
// The contact on the other side. `peerjvmone` is a standing account on
// relay1; the reply case needs somebody answering AS it, which is the
// interop Peer in `autoreply` (see PROVEN below). Override with
// -Pandroid.testInstrumentationRunnerArguments.peer=<handle>, and run the
// reply case ALONE: one autoreply peer pins the first fresh account it
// sees and drops a second one, so two ChatFlow methods cannot share it.
// The contacts on the other side, both created for this run by
// scripts/instrumented.sh. They used to be `peerjvmone`, one standing
// account on relay1 that ran out of prekeys and filled with messages nobody
// collected, so adding it and delivering to it failed in some runs and not
// others. The reply case gets its own peer, running `autoreply`, because
// one autoreply peer answers the first new account that writes to it and
// drops the rest.
private val peerHandle: String
get() = InstrumentationRegistry.getArguments().getString("peer") ?: "peerjvmone"
get() =
InstrumentationRegistry.getArguments().getString("contactPeer")
?: error("no contactPeer: run scripts/instrumented.sh, which creates a fresh one")
private val echoPeer: String
get() =
InstrumentationRegistry.getArguments().getString("echoPeer")
?: error("no echoPeer: run scripts/instrumented.sh, which creates one and runs autoreply")
private lateinit var handle: String
@Before
@@ -117,14 +124,14 @@ class ChatFlowTest {
signUp()
compose.onNodeWithTag("add-contact").performClick()
compose.onNodeWithTag("new-contact").performTextInput(peerHandle)
compose.onNodeWithTag("new-contact").performTextInput(echoPeer)
// Adding is gated on a lookup: the button stays disabled until the
// handle resolves, so a typo cannot become a contact.
compose.onNodeWithTag("lookup-contact").performClick()
awaitTag("lookup-result", 30_000)
compose.onNodeWithTag("confirm-contact").performClick()
awaitNode(hasText(peerHandle, substring = true), 60_000)
compose.onNode(hasText(peerHandle, substring = true)).performClick()
awaitNode(hasText(echoPeer, substring = true), 60_000)
compose.onNode(hasText(echoPeer, substring = true)).performClick()
awaitTag("draft", 20_000)
val text = "ping ${System.currentTimeMillis() % 100000}"
@@ -134,15 +141,10 @@ class ChatFlowTest {
// The app polls every few seconds; the peer echoes every couple.
val echo = "echo: $text"
val arrived =
runCatching {
awaitNode(hasText(echo, substring = true), 90_000)
true
}.getOrDefault(false)
if (!arrived) {
android.util.Log.w("fnchat", "no echo for '$text', is the peer running autoreply?")
return
}
// A missing echo is a failure. This used to log a warning and return,
// which passed the test while proving nothing; with the script running
// the echo peer, silence means the round trip is broken.
awaitNode(hasText(echo, substring = true), 90_000)
compose.onNode(hasText(echo, substring = true)).assertIsDisplayed()
android.util.Log.i("fnchat", "round trip complete: $echo")
}
@@ -56,7 +56,11 @@ class CrudSurfaceTest {
} else {
"https"
}
private val peerHandle = "peerjvmone"
/** Created for this run by scripts/instrumented.sh; see FeatureUseTest. */
private val peerHandle: String
get() =
InstrumentationRegistry.getArguments().getString("contactPeer")
?: error("no contactPeer: run scripts/instrumented.sh, which creates a fresh one")
private lateinit var handle: String
@Before
@@ -48,6 +48,16 @@ class FeatureUseTest {
InstrumentationRegistry.getArguments().getString("relay")
?: "relay1.flamenet.io"
/**
* The contact, an account scripts/instrumented.sh creates for this run.
* Not a standing account: the one these suites used to share had run out of
* prekeys and filled with messages nobody collected.
*/
private val peer: String
get() =
InstrumentationRegistry.getArguments().getString("contactPeer")
?: error("no contactPeer: run scripts/instrumented.sh, which creates a fresh one")
/** A local relay is plain HTTP; anything else is not. */
private val relayScheme: String get() =
if (relay.startsWith("10.0.2.2") ||
@@ -191,18 +201,22 @@ class FeatureUseTest {
compose.onNodeWithTag("add-contact").performScrollTo()
compose.onNodeWithTag("add-contact").performClick()
awaitTag("new-contact", 20_000)
compose.onNodeWithTag("new-contact").performTextInput(PEER)
compose.onNodeWithTag("new-contact").performTextInput(peer)
// Look up, THEN add.
compose.onNodeWithTag("lookup-contact").performScrollTo()
compose.onNodeWithTag("lookup-contact").performClick()
awaitTag("confirm-contact", 30_000)
// Wait for the RESULT, not the button. `confirm-contact` is on screen,
// disabled, until the handle resolves, so waiting for it returned at
// once and the tap landed on a disabled button. ChatFlowTest and
// CrudSurfaceTest already wait for this.
awaitTag("lookup-result", 30_000)
compose.onNodeWithTag("confirm-contact").performScrollTo()
compose.onNodeWithTag("confirm-contact").performClick()
// `buddy-circle-N` as well as `buddy-N`.
awaitNodeMatching(
Regex("buddy-(circle-)?\\d+"),
150_000,
"'$PEER' never became a contact, does that account exist on $relay?",
"'$peer' never became a contact, does that account exist on $relay?",
)
}
@@ -334,9 +348,4 @@ class FeatureUseTest {
readTimeout = 8000
}.responseCode in 200..299
}.getOrDefault(false)
private companion object {
/** A standing account on relay1 that other on-device tests also use. */
const val PEER = "peerjvmone"
}
}
+43
View File
@@ -115,10 +115,50 @@ fi
# as a dead screen and cost an hour; the console screenshot showed the
# sign-up form, filled in, with the submit button below the fold.
# Fresh correspondents, made for this run and used by nobody else.
#
# The suites used to add `peerjvmone`, one standing account on relay1 whose
# only device came online when somebody ran the interop peer by hand. Nobody
# did, so by 2026-09-26 it had no one-time prekeys left, classical or PQ, and
# 114 messages it would never collect, and every run added more strangers to
# it. Adding it and delivering to it failed in some runs and not others.
#
# Two peers, not one: an autoreply peer answers the first new account that
# writes to it and drops the rest, so the reply case gets one to itself.
# Run with plain `java` from an installed classpath rather than a second
# Gradle build, which would fight the test build below for its locks.
case "$RELAY" in
10.0.2.2:*) PEER_BASE="http://127.0.0.1:${RELAY#10.0.2.2:}";;
*) PEER_BASE="https://$RELAY";;
esac
./gradlew --no-daemon -q :interop:installDist
PEER_CP="interop/build/install/interop/lib/*"
PEERS="$(mktemp -d)"
peer() { java -cp "$PEER_CP" io.flamenet.interop.Peer "$@"; }
handle_of() { sed -n 's/.*"handle"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p'; }
CONTACT_PEER="$(peer "$PEERS/contact.json" create "$PEER_BASE" | handle_of)"
ECHO_PEER="$(peer "$PEERS/echo.json" create "$PEER_BASE" | handle_of)"
if [ -z "$CONTACT_PEER" ] || [ -z "$ECHO_PEER" ]; then
echo "REFUSED: could not create the two peers on $PEER_BASE." >&2
exit 1
fi
# Answering for the whole run, and stopped however the script ends. `java`
# itself goes in the background, not the `peer` function: a backgrounded
# function is a subshell, and killing that left the JVM answering for an hour.
java -cp "$PEER_CP" io.flamenet.interop.Peer "$PEERS/echo.json" autoreply 3600 \
>"$PEERS/echo.log" 2>&1 &
ECHO_PID=$!
trap 'kill "$ECHO_PID" 2>/dev/null || true; wait "$ECHO_PID" 2>/dev/null || true; rm -rf "$PEERS"' EXIT
echo "### peers: contact $CONTACT_PEER, echo $ECHO_PEER (autoreply pid $ECHO_PID)"
echo "### relay $RELAY · classes $CLASSES"
set +e
./gradlew --no-daemon :app:connectedDebugAndroidTest \
-Pandroid.testInstrumentationRunnerArguments.relay="$RELAY" \
-Pandroid.testInstrumentationRunnerArguments.contactPeer="$CONTACT_PEER" \
-Pandroid.testInstrumentationRunnerArguments.echoPeer="$ECHO_PEER" \
-Pandroid.testInstrumentationRunnerArguments.class="$CLASSES" 2>&1 \
| tee /tmp/fnmsg-instrumented.log \
| grep -E "FAILED|PASSED|SKIPPED|BUILD|Finished [0-9]+ test|Tests on"
@@ -144,5 +184,8 @@ elif [ "$STATUS" = "0" ]; then
else
echo "### VERDICT: FAILED, see /tmp/fnmsg-instrumented.log"
echo "### and app/build/reports/androidTests/connected/debug/index.html"
# The other half of a failed reply case: did the echo peer hear anything?
echo "### echo peer ($ECHO_PEER) log:"
tail -20 "$PEERS/echo.log" 2>/dev/null || echo " (empty)"
fi
exit $STATUS