Compare commits

...

2 Commits

Author SHA1 Message Date
c10fa48bf5 [iOS & Android] iOS 1.2.4 & Android 1.7.3 2025-10-06 11:54:36 -06:00
acf487db93 wtf 2025-10-06 00:38:58 -06:00
10 changed files with 245 additions and 54 deletions

View File

@@ -16,8 +16,8 @@ android {
applicationId = "com.atridad.openclimb" applicationId = "com.atridad.openclimb"
minSdk = 31 minSdk = 31
targetSdk = 36 targetSdk = 36
versionCode = 30 versionCode = 32
versionName = "1.7.2" versionName = "1.7.3"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -79,12 +79,18 @@ class ClimbRepository(database: OpenClimbDatabase, private val context: Context)
suspend fun insertSession(session: ClimbSession) { suspend fun insertSession(session: ClimbSession) {
sessionDao.insertSession(session) sessionDao.insertSession(session)
dataStateManager.updateDataState() dataStateManager.updateDataState()
triggerAutoSync() // Only trigger sync for completed sessions
if (session.status != SessionStatus.ACTIVE) {
triggerAutoSync()
}
} }
suspend fun updateSession(session: ClimbSession) { suspend fun updateSession(session: ClimbSession) {
sessionDao.updateSession(session) sessionDao.updateSession(session)
dataStateManager.updateDataState() dataStateManager.updateDataState()
triggerAutoSync() // Only trigger sync for completed sessions
if (session.status != SessionStatus.ACTIVE) {
triggerAutoSync()
}
} }
suspend fun deleteSession(session: ClimbSession) { suspend fun deleteSession(session: ClimbSession) {
sessionDao.deleteSession(session) sessionDao.deleteSession(session)
@@ -109,17 +115,14 @@ class ClimbRepository(database: OpenClimbDatabase, private val context: Context)
suspend fun insertAttempt(attempt: Attempt) { suspend fun insertAttempt(attempt: Attempt) {
attemptDao.insertAttempt(attempt) attemptDao.insertAttempt(attempt)
dataStateManager.updateDataState() dataStateManager.updateDataState()
triggerAutoSync()
} }
suspend fun updateAttempt(attempt: Attempt) { suspend fun updateAttempt(attempt: Attempt) {
attemptDao.updateAttempt(attempt) attemptDao.updateAttempt(attempt)
dataStateManager.updateDataState() dataStateManager.updateDataState()
triggerAutoSync()
} }
suspend fun deleteAttempt(attempt: Attempt) { suspend fun deleteAttempt(attempt: Attempt) {
attemptDao.deleteAttempt(attempt) attemptDao.deleteAttempt(attempt)
dataStateManager.updateDataState() dataStateManager.updateDataState()
triggerAutoSync()
} }
suspend fun exportAllDataToZipUri(context: Context, uri: android.net.Uri) { suspend fun exportAllDataToZipUri(context: Context, uri: android.net.Uri) {

View File

@@ -10,6 +10,7 @@ import com.atridad.openclimb.data.format.BackupGym
import com.atridad.openclimb.data.format.BackupProblem import com.atridad.openclimb.data.format.BackupProblem
import com.atridad.openclimb.data.format.ClimbDataBackup import com.atridad.openclimb.data.format.ClimbDataBackup
import com.atridad.openclimb.data.migration.ImageMigrationService import com.atridad.openclimb.data.migration.ImageMigrationService
import com.atridad.openclimb.data.model.SessionStatus
import com.atridad.openclimb.data.repository.ClimbRepository import com.atridad.openclimb.data.repository.ClimbRepository
import com.atridad.openclimb.data.state.DataStateManager import com.atridad.openclimb.data.state.DataStateManager
import com.atridad.openclimb.utils.DateFormatUtils import com.atridad.openclimb.utils.DateFormatUtils
@@ -564,14 +565,25 @@ class SyncService(private val context: Context, private val repository: ClimbRep
val allSessions = repository.getAllSessions().first() val allSessions = repository.getAllSessions().first()
val allAttempts = repository.getAllAttempts().first() val allAttempts = repository.getAllAttempts().first()
// Filter out active sessions and their attempts from sync
val completedSessions = allSessions.filter { it.status != SessionStatus.ACTIVE }
val activeSessionIds =
allSessions.filter { it.status == SessionStatus.ACTIVE }.map { it.id }.toSet()
val completedAttempts = allAttempts.filter { !activeSessionIds.contains(it.sessionId) }
Log.d(
TAG,
"Sync exclusions: ${allSessions.size - completedSessions.size} active sessions, ${allAttempts.size - completedAttempts.size} active session attempts"
)
return ClimbDataBackup( return ClimbDataBackup(
exportedAt = dataStateManager.getLastModified(), exportedAt = dataStateManager.getLastModified(),
version = "2.0", version = "2.0",
formatVersion = "2.0", formatVersion = "2.0",
gyms = allGyms.map { BackupGym.fromGym(it) }, gyms = allGyms.map { BackupGym.fromGym(it) },
problems = allProblems.map { BackupProblem.fromProblem(it) }, problems = allProblems.map { BackupProblem.fromProblem(it) },
sessions = allSessions.map { BackupClimbSession.fromClimbSession(it) }, sessions = completedSessions.map { BackupClimbSession.fromClimbSession(it) },
attempts = allAttempts.map { BackupAttempt.fromAttempt(it) } attempts = completedAttempts.map { BackupAttempt.fromAttempt(it) }
) )
} }
@@ -579,6 +591,20 @@ class SyncService(private val context: Context, private val repository: ClimbRep
backup: ClimbDataBackup, backup: ClimbDataBackup,
imagePathMapping: Map<String, String> = emptyMap() imagePathMapping: Map<String, String> = emptyMap()
) { ) {
// Store active sessions and their attempts before reset
val activeSessions =
repository.getAllSessions().first().filter { it.status == SessionStatus.ACTIVE }
val activeSessionIds = activeSessions.map { it.id }.toSet()
val activeAttempts =
repository.getAllAttempts().first().filter {
activeSessionIds.contains(it.sessionId)
}
Log.d(
TAG,
"Preserving ${activeSessions.size} active sessions and ${activeAttempts.size} active attempts during import"
)
repository.resetAllData() repository.resetAllData()
backup.gyms.forEach { backupGym -> backup.gyms.forEach { backupGym ->
@@ -646,6 +672,24 @@ class SyncService(private val context: Context, private val repository: ClimbRep
} }
} }
// Restore active sessions and their attempts after import
activeSessions.forEach { session ->
try {
Log.d(TAG, "Restoring active session: ${session.id}")
repository.insertSessionWithoutSync(session)
} catch (e: Exception) {
Log.e(TAG, "Failed to restore active session '${session.id}': ${e.message}")
}
}
activeAttempts.forEach { attempt ->
try {
repository.insertAttemptWithoutSync(attempt)
} catch (e: Exception) {
Log.e(TAG, "Failed to restore active attempt '${attempt.id}': ${e.message}")
}
}
dataStateManager.setLastModified(backup.exportedAt) dataStateManager.setLastModified(backup.exportedAt)
Log.d(TAG, "Data state synchronized to imported timestamp: ${backup.exportedAt}") Log.d(TAG, "Data state synchronized to imported timestamp: ${backup.exportedAt}")
} }

View File

@@ -454,4 +454,61 @@ class SyncMergeLogicTest {
dateString1 > dateString2 dateString1 > dateString2
} }
} }
@Test
fun `test active sessions excluded from sync`() {
// Test scenario: Active sessions should not be included in sync data
// This tests the new behavior where active sessions are excluded from sync
// until they are completed
val allLocalSessions =
listOf(
BackupClimbSession(
id = "active_session_1",
gymId = "gym1",
date = "2024-01-01",
startTime = "2024-01-01T10:00:00",
endTime = null,
duration = null,
status = SessionStatus.ACTIVE,
notes = null,
createdAt = "2024-01-01T10:00:00",
updatedAt = "2024-01-01T10:00:00"
),
BackupClimbSession(
id = "completed_session_1",
gymId = "gym1",
date = "2023-12-31",
startTime = "2023-12-31T15:00:00",
endTime = "2023-12-31T17:00:00",
duration = 7200000,
status = SessionStatus.COMPLETED,
notes = "Previous session",
createdAt = "2023-12-31T15:00:00",
updatedAt = "2023-12-31T17:00:00"
)
)
// Simulate filtering that would happen in createBackupFromRepository
val sessionsForSync = allLocalSessions.filter { it.status != SessionStatus.ACTIVE }
// Only completed sessions should be included in sync
assertEquals("Should only include completed sessions in sync", 1, sessionsForSync.size)
// Active session should be excluded
assertFalse(
"Should not contain active session in sync",
sessionsForSync.any {
it.id == "active_session_1" && it.status == SessionStatus.ACTIVE
}
)
// Completed session should be included
assertTrue(
"Should contain completed session in sync",
sessionsForSync.any {
it.id == "completed_session_1" && it.status == SessionStatus.COMPLETED
}
)
}
} }

View File

@@ -465,7 +465,7 @@
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements; CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 14; CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -485,7 +485,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.3; MARKETING_VERSION = 1.2.4;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -508,7 +508,7 @@
CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements; CODE_SIGN_ENTITLEMENTS = OpenClimb/OpenClimb.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 14; CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
@@ -528,7 +528,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.3; MARKETING_VERSION = 1.2.4;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@@ -592,7 +592,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 14; CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist; INFOPLIST_FILE = SessionStatusLive/Info.plist;
@@ -603,7 +603,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.2.3; MARKETING_VERSION = 1.2.4;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;
@@ -622,7 +622,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements; CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 14; CURRENT_PROJECT_VERSION = 15;
DEVELOPMENT_TEAM = 4BC9Y2LL4B; DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist; INFOPLIST_FILE = SessionStatusLive/Info.plist;
@@ -633,7 +633,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 1.2.3; MARKETING_VERSION = 1.2.4;
PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive; PRODUCT_BUNDLE_IDENTIFIER = com.atridad.OpenClimb.SessionStatusLive;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES; SKIP_INSTALL = YES;

View File

@@ -378,7 +378,6 @@ class SyncService: ObservableObject {
print("Renamed local image: \(filename) -> \(consistentFilename)") print("Renamed local image: \(filename) -> \(consistentFilename)")
// Update problem's image path in memory for consistency // Update problem's image path in memory for consistency
// Note: This would require updating the problem in the data manager
} catch { } catch {
print("Failed to rename local image, using original: \(error)") print("Failed to rename local image, using original: \(error)")
} }
@@ -397,12 +396,24 @@ class SyncService: ObservableObject {
private func createBackupFromDataManager(_ dataManager: ClimbingDataManager) -> ClimbDataBackup private func createBackupFromDataManager(_ dataManager: ClimbingDataManager) -> ClimbDataBackup
{ {
// Filter out active sessions and their attempts from sync
let completedSessions = dataManager.sessions.filter { $0.status != .active }
let activeSessionIds = Set(
dataManager.sessions.filter { $0.status == .active }.map { $0.id })
let completedAttempts = dataManager.attempts.filter {
!activeSessionIds.contains($0.sessionId)
}
print(
"iOS SYNC: Excluding \(dataManager.sessions.count - completedSessions.count) active sessions and \(dataManager.attempts.count - completedAttempts.count) active session attempts from sync"
)
return ClimbDataBackup( return ClimbDataBackup(
exportedAt: DataStateManager.shared.getLastModified(), exportedAt: DataStateManager.shared.getLastModified(),
gyms: dataManager.gyms.map { BackupGym(from: $0) }, gyms: dataManager.gyms.map { BackupGym(from: $0) },
problems: dataManager.problems.map { BackupProblem(from: $0) }, problems: dataManager.problems.map { BackupProblem(from: $0) },
sessions: dataManager.sessions.map { BackupClimbSession(from: $0) }, sessions: completedSessions.map { BackupClimbSession(from: $0) },
attempts: dataManager.attempts.map { BackupAttempt(from: $0) } attempts: completedAttempts.map { BackupAttempt(from: $0) }
) )
} }
@@ -412,6 +423,17 @@ class SyncService: ObservableObject {
) throws { ) throws {
do { do {
// Store active sessions and their attempts before import
let activeSessions = dataManager.sessions.filter { $0.status == .active }
let activeSessionIds = Set(activeSessions.map { $0.id })
let activeAttempts = dataManager.attempts.filter {
activeSessionIds.contains($0.sessionId)
}
print(
"iOS IMPORT: Preserving \(activeSessions.count) active sessions and \(activeAttempts.count) active attempts during import"
)
// Update problem image paths to point to downloaded images // Update problem image paths to point to downloaded images
let updatedBackup: ClimbDataBackup let updatedBackup: ClimbDataBackup
if !imagePathMapping.isEmpty { if !imagePathMapping.isEmpty {
@@ -456,6 +478,24 @@ class SyncService: ObservableObject {
// Use existing import method which properly handles data restoration // Use existing import method which properly handles data restoration
try dataManager.importData(from: zipData, showSuccessMessage: false) try dataManager.importData(from: zipData, showSuccessMessage: false)
// Restore active sessions and their attempts after import
for session in activeSessions {
print("iOS IMPORT: Restoring active session: \(session.id)")
dataManager.sessions.append(session)
if session.id == dataManager.activeSession?.id {
dataManager.activeSession = session
}
}
for attempt in activeAttempts {
dataManager.attempts.append(attempt)
}
// Save restored data
dataManager.saveSessions()
dataManager.saveAttempts()
dataManager.saveActiveSession()
// Update local data state to match imported data timestamp // Update local data state to match imported data timestamp
DataStateManager.shared.setLastModified(backup.exportedAt) DataStateManager.shared.setLastModified(backup.exportedAt)
print("Data state synchronized to imported timestamp: \(backup.exportedAt)") print("Data state synchronized to imported timestamp: \(backup.exportedAt)")

View File

@@ -158,7 +158,7 @@ class ClimbingDataManager: ObservableObject {
} }
} }
private func saveSessions() { internal func saveSessions() {
if let data = try? encoder.encode(sessions) { if let data = try? encoder.encode(sessions) {
userDefaults.set(data, forKey: Keys.sessions) userDefaults.set(data, forKey: Keys.sessions)
// Share with widget - convert to widget format // Share with widget - convert to widget format
@@ -176,7 +176,7 @@ class ClimbingDataManager: ObservableObject {
} }
} }
private func saveAttempts() { internal func saveAttempts() {
if let data = try? encoder.encode(attempts) { if let data = try? encoder.encode(attempts) {
userDefaults.set(data, forKey: Keys.attempts) userDefaults.set(data, forKey: Keys.attempts)
// Share with widget - convert to widget format // Share with widget - convert to widget format
@@ -197,7 +197,7 @@ class ClimbingDataManager: ObservableObject {
} }
} }
private func saveActiveSession() { internal func saveActiveSession() {
if let activeSession = activeSession, if let activeSession = activeSession,
let data = try? encoder.encode(activeSession) let data = try? encoder.encode(activeSession)
{ {
@@ -326,9 +326,6 @@ class ClimbingDataManager: ObservableObject {
saveSessions() saveSessions()
DataStateManager.shared.updateDataState() DataStateManager.shared.updateDataState()
successMessage = "Session started successfully"
clearMessageAfterDelay()
// MARK: - Start Live Activity for new session // MARK: - Start Live Activity for new session
if let gym = gym(withId: gymId) { if let gym = gym(withId: gymId) {
Task { Task {
@@ -336,9 +333,6 @@ class ClimbingDataManager: ObservableObject {
for: newSession, gymName: gym.name) for: newSession, gymName: gym.name)
} }
} }
// Trigger auto-sync if enabled
syncService.triggerAutoSync(dataManager: self)
} }
func endSession(_ sessionId: UUID) { func endSession(_ sessionId: UUID) {
@@ -356,8 +350,6 @@ class ClimbingDataManager: ObservableObject {
saveActiveSession() saveActiveSession()
saveSessions() saveSessions()
DataStateManager.shared.updateDataState() DataStateManager.shared.updateDataState()
successMessage = "Session completed successfully"
clearMessageAfterDelay()
// Trigger auto-sync if enabled // Trigger auto-sync if enabled
syncService.triggerAutoSync(dataManager: self) syncService.triggerAutoSync(dataManager: self)
@@ -380,14 +372,14 @@ class ClimbingDataManager: ObservableObject {
saveSessions() saveSessions()
DataStateManager.shared.updateDataState() DataStateManager.shared.updateDataState()
successMessage = "Session updated successfully"
clearMessageAfterDelay()
// Update Live Activity when session is updated // Update Live Activity when session is updated
updateLiveActivityForActiveSession() updateLiveActivityForActiveSession()
// Trigger auto-sync if enabled // Only trigger sync if session is completed
syncService.triggerAutoSync(dataManager: self) if session.status != .active {
syncService.triggerAutoSync(dataManager: self)
}
} }
} }
@@ -406,8 +398,6 @@ class ClimbingDataManager: ObservableObject {
sessions.removeAll { $0.id == session.id } sessions.removeAll { $0.id == session.id }
saveSessions() saveSessions()
DataStateManager.shared.updateDataState() DataStateManager.shared.updateDataState()
successMessage = "Session deleted successfully"
clearMessageAfterDelay()
// Update Live Activity when session is deleted // Update Live Activity when session is deleted
updateLiveActivityForActiveSession() updateLiveActivityForActiveSession()
@@ -435,12 +425,6 @@ class ClimbingDataManager: ObservableObject {
saveAttempts() saveAttempts()
DataStateManager.shared.updateDataState() DataStateManager.shared.updateDataState()
successMessage = "Attempt logged successfully"
// Trigger auto-sync if enabled
syncService.triggerAutoSync(dataManager: self)
clearMessageAfterDelay()
// Update Live Activity when new attempt is added // Update Live Activity when new attempt is added
updateLiveActivityForActiveSession() updateLiveActivityForActiveSession()
} }
@@ -450,14 +434,9 @@ class ClimbingDataManager: ObservableObject {
attempts[index] = attempt attempts[index] = attempt
saveAttempts() saveAttempts()
DataStateManager.shared.updateDataState() DataStateManager.shared.updateDataState()
successMessage = "Attempt updated successfully"
clearMessageAfterDelay()
// Update Live Activity when attempt is updated // Update Live Activity when attempt is updated
updateLiveActivityForActiveSession() updateLiveActivityForActiveSession()
// Trigger auto-sync if enabled
syncService.triggerAutoSync(dataManager: self)
} }
} }
@@ -465,14 +444,9 @@ class ClimbingDataManager: ObservableObject {
attempts.removeAll { $0.id == attempt.id } attempts.removeAll { $0.id == attempt.id }
saveAttempts() saveAttempts()
DataStateManager.shared.updateDataState() DataStateManager.shared.updateDataState()
successMessage = "Attempt deleted successfully"
clearMessageAfterDelay()
// Update Live Activity when attempt is deleted // Update Live Activity when attempt is deleted
updateLiveActivityForActiveSession() updateLiveActivityForActiveSession()
// Trigger auto-sync if enabled
syncService.triggerAutoSync(dataManager: self)
} }
func attempts(forSession sessionId: UUID) -> [Attempt] { func attempts(forSession sessionId: UUID) -> [Attempt] {

View File

@@ -131,7 +131,6 @@ final class LiveActivityManager {
) )
await currentActivity.update(.init(state: updatedContentState, staleDate: nil)) await currentActivity.update(.init(state: updatedContentState, staleDate: nil))
print("Live Activity updated successfully")
} }
/// Call this when a ClimbSession ends to end the Live Activity /// Call this when a ClimbSession ends to end the Live Activity

View File

@@ -252,4 +252,78 @@ final class OpenClimbTests: XCTestCase {
XCTAssertNotNil(parsedDate) XCTAssertNotNil(parsedDate)
XCTAssertEqual(date.timeIntervalSince1970, parsedDate!.timeIntervalSince1970, accuracy: 1.0) XCTAssertEqual(date.timeIntervalSince1970, parsedDate!.timeIntervalSince1970, accuracy: 1.0)
} }
// MARK: - Active Session Preservation Tests
func testActiveSessionPreservationDuringImport() throws {
// Test that active sessions are preserved during import operations
// This tests the fix for the bug where active sessions disappear after sync
// Simulate an active session that exists locally but not in import data
let activeSessionId = UUID()
let gymId = UUID()
// Test data structure representing local active session
let localActiveSession: [String: Any] = [
"id": activeSessionId.uuidString,
"gymId": gymId.uuidString,
"status": "active",
"date": "2024-01-01",
"startTime": "2024-01-01T10:00:00Z",
]
// Test data structure representing server sessions (without the active one)
let serverSessions: [[String: Any]] = [
[
"id": UUID().uuidString,
"gymId": gymId.uuidString,
"status": "completed",
"date": "2023-12-31",
"startTime": "2023-12-31T15:00:00Z",
"endTime": "2023-12-31T17:00:00Z",
]
]
// Verify test setup
XCTAssertEqual(localActiveSession["status"] as? String, "active")
XCTAssertEqual(serverSessions.count, 1)
XCTAssertEqual(serverSessions[0]["status"] as? String, "completed")
// Verify that the active session ID is not in the server sessions
let serverSessionIds = serverSessions.compactMap { $0["id"] as? String }
XCTAssertFalse(serverSessionIds.contains(activeSessionId.uuidString))
// Test that we can identify an active session
if let status = localActiveSession["status"] as? String {
XCTAssertTrue(status == "active")
} else {
XCTFail("Failed to extract session status")
}
// Test session ID validation
if let sessionIdString = localActiveSession["id"] as? String,
let sessionId = UUID(uuidString: sessionIdString)
{
XCTAssertEqual(sessionId, activeSessionId)
} else {
XCTFail("Failed to parse session ID")
}
// Test that combining sessions preserves both local active and server completed
var combinedSessions = serverSessions
combinedSessions.append(localActiveSession)
XCTAssertEqual(combinedSessions.count, 2)
// Verify both session types are present
let hasActiveSession = combinedSessions.contains { session in
(session["status"] as? String) == "active"
}
let hasCompletedSession = combinedSessions.contains { session in
(session["status"] as? String) == "completed"
}
XCTAssertTrue(hasActiveSession, "Combined sessions should contain active session")
XCTAssertTrue(hasCompletedSession, "Combined sessions should contain completed session")
}
} }