Live frames ride sendMessage, which is reachable-only — and phone→watch reachability drops exactly when the user is swiping on the phone (wrist down). A frame that failed to send was staged but never retried until a reachability edge, and a frame lost outright desynced the run until the next human transition — sometimes forever, since a reconnect could even re-send the stale staged frame and yank the peer backwards. Four fixes, symmetric on both bridges and both run screens: - A send that fails while nominally reachable now retries with a short backoff (a few times per staged message) instead of being swallowed. - Receiving a frame that outranks the staged outbound one drops the staged frame, so a reconnect re-send can't move the run backwards; a delivery the staged frame outranks is ignored as stale. - Every staleness comparison now tie-breaks the shared version sequence on the frame's wall-clock anchor (LiveProgress.isNewer) — after a lost frame both devices can mint the same version, and the later human action must win. - Durable repair: when the absorbed workout doc shows sets completed beyond anything the open run screen recorded or followed, it jumps forward to the first unfinished set's work page — a lost frame now degrades to a briefly-stale page instead of a stuck one.
48 lines
2.2 KiB
Swift
48 lines
2.2 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import Workouts
|
|
|
|
/// Pins the live-mirror staleness ordering (`LiveProgress.isNewer`): the shared per-run
|
|
/// version sequence decides, and the phase anchor breaks ties. The tie-break exists
|
|
/// because the sequence isn't collision-free — each side catches its counter up only from
|
|
/// frames it *receives*, so after a lost frame both devices can mint the same version
|
|
/// independently, and the later wall-clock action must win. Equal version *and* equal
|
|
/// anchor is the same frame redelivered, which must NOT rank as newer (a redelivery would
|
|
/// otherwise re-jump the follower's page).
|
|
struct LiveProgressOrderingTests {
|
|
private static let t0 = Date(timeIntervalSince1970: 1_700_000_000)
|
|
|
|
private func frame(version: Int, start: Date) -> LiveProgress {
|
|
LiveProgress(workoutID: "W", logID: "L", exerciseName: "Bench", phase: .work,
|
|
setIndex: 0, setCount: 3, detail: "8 reps",
|
|
phaseStart: start, phaseEnd: nil, version: version)
|
|
}
|
|
|
|
@Test func higherVersionWinsRegardlessOfAnchor() {
|
|
let older = frame(version: 3, start: Self.t0.addingTimeInterval(10))
|
|
let newer = frame(version: 4, start: Self.t0) // earlier anchor, higher version
|
|
#expect(newer.isNewer(than: older))
|
|
#expect(!older.isNewer(than: newer))
|
|
}
|
|
|
|
@Test func collidedVersionsTieBreakOnAnchor() {
|
|
let phoneFrame = frame(version: 4, start: Self.t0)
|
|
let watchFrame = frame(version: 4, start: Self.t0.addingTimeInterval(2))
|
|
#expect(watchFrame.isNewer(than: phoneFrame))
|
|
#expect(!phoneFrame.isNewer(than: watchFrame))
|
|
}
|
|
|
|
@Test func redeliveryIsNotNewer() {
|
|
let sent = frame(version: 4, start: Self.t0)
|
|
#expect(!sent.isNewer(than: sent))
|
|
}
|
|
|
|
@Test func versionAndStartOverloadMatchesFrameOverload() {
|
|
let f = frame(version: 4, start: Self.t0)
|
|
#expect(f.isNewer(thanVersion: 3, start: Self.t0.addingTimeInterval(99)))
|
|
#expect(f.isNewer(thanVersion: 4, start: Self.t0.addingTimeInterval(-1)))
|
|
#expect(!f.isNewer(thanVersion: 4, start: Self.t0))
|
|
#expect(!f.isNewer(thanVersion: 5, start: .distantPast))
|
|
}
|
|
}
|