fix: let a saved answer be accepted while the network is blocked

When two devices disagree about where a book was left off, Liseur
offers the reader both positions. Accepting the other device's is
purely local work: the position was fetched and written down by an
earlier sync, so choosing it changes nothing but this phone, which is
why it works on a plane. The new local network check was refusing it
anyway. Sending this device's position, which does reach the server,
is still refused.
This commit is contained in:
Chmouel Boudjnah 2026-09-11 06:43:39 +02:00
parent 3270d7eba6
commit d333ea9dca
3 changed files with 31 additions and 33 deletions

View file

@ -59,28 +59,20 @@ class LocalNetworkGuardedSync(
}
/**
* Settling a conflict sends the answer to the server, so it is
* guarded too.
* Keeping this device's position sends it to the server, so it is
* guarded; taking the server's is not.
*
* Both of these are reached from the reader's own tap, and both
* already answer `Failed(Offline)` when there is no network at all.
* A blocked one is the same thing said differently: the write is
* not going to arrive, and a timeout the reader watches is the
* worst way to tell them so. The status line is left alone, as it
* is for a preview.
* The asymmetry is the two operations', not this class's. Taking
* the server's position applies an answer an earlier run already
* brought back and wrote down, and touches nothing but this
* phone's database — which is why it works on a plane, and must go
* on working behind a blocked network for the same reason. Keeping
* this device's dials, and already answers `Failed(Offline)` when
* there is no network at all; a blocked one is that said
* differently, and a timeout the reader sits and watches is the
* worst way to say it. The status line is left alone, as it is for
* a preview: one book being settled is not a run.
*/
override suspend fun takeRemotePosition(
bookUrl: String,
atRevision: Long,
peerId: String?,
expectedAccountKey: String?,
): ResolveOutcome =
if (blocked()) {
ResolveOutcome.Failed(SyncFailure.LocalNetworkBlocked)
} else {
delegate.takeRemotePosition(bookUrl, atRevision, peerId, expectedAccountKey)
}
override suspend fun keepLocalPosition(bookUrl: String, peerId: String?): ResolveOutcome =
if (blocked()) {
ResolveOutcome.Failed(SyncFailure.LocalNetworkBlocked)

View file

@ -79,13 +79,15 @@ class LocalNetworkGuardedSyncTest {
}
/**
* Settling a conflict writes the answer to the server, so it is
* refused too rather than left to sit on a timeout the reader is
* watching. The status line is not touched: this is one book being
* settled, not a run.
* Keeping this device's position writes it to the server, so it is
* refused rather than left to sit on a timeout the reader is
* watching. Taking the server's applies an answer an earlier run
* already wrote down and touches nothing but this phone, so it goes
* through — the same reason it works on a plane. Neither touches
* the status line: this is one book being settled, not a run.
*/
@Test
fun `settling a conflict is refused rather than dialled`() = runTest {
fun `sending an answer is refused but accepting one still works`() = runTest {
val peer = FakePeer(PeerPositionSync.CATALOG)
val reporting = SyncReporting()
val guarded = guard(peer, reporting, "http://192.168.1.20:8083")
@ -94,8 +96,8 @@ class LocalNetworkGuardedSyncTest {
val taken = guarded.takeRemotePosition("book", 1L, null, null)
assertEquals(ResolveOutcome.Failed(SyncFailure.LocalNetworkBlocked), kept)
assertEquals(ResolveOutcome.Failed(SyncFailure.LocalNetworkBlocked), taken)
assertEquals(0, peer.resolutions)
assertEquals(ResolveOutcome.Done, taken)
assertEquals(1, peer.resolutions)
assertEquals(PositionSyncStatus.Idle, reporting.statusOf(PeerPositionSync.CATALOG))
}