From cc46b8609523719478efa58fab5a9e147a366c48 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Tue, 1 Sep 2026 20:30:29 +0200 Subject: [PATCH 1/7] fix(sync): preserve upload checkpoint recovery evidence --- .../113-upload-checkpoint-recovery.md | 7 + .../app/JvmResumableNextcloudUploadTest.kt | 138 ++++++++++++++++++ .../app/JvmResumableNextcloudUpload.kt | 16 +- 3 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 changes/unreleased/113-upload-checkpoint-recovery.md diff --git a/changes/unreleased/113-upload-checkpoint-recovery.md b/changes/unreleased/113-upload-checkpoint-recovery.md new file mode 100644 index 000000000..e4ef2e58c --- /dev/null +++ b/changes/unreleased/113-upload-checkpoint-recovery.md @@ -0,0 +1,7 @@ +category: fix +issue: 113 +pull: none +platforms: android, desktop +user-facing: yes + +Superseded resumable uploads now retain durable size, content hash, and publication state during cleanup, so an ambiguous server result cannot mistake an already published directory replacement for an abandoned stage. diff --git a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/JvmResumableNextcloudUploadTest.kt b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/JvmResumableNextcloudUploadTest.kt index 020827518..5b11abd02 100644 --- a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/JvmResumableNextcloudUploadTest.kt +++ b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/JvmResumableNextcloudUploadTest.kt @@ -130,6 +130,130 @@ class JvmResumableNextcloudUploadTest { } } + @Test + fun `superseded ambiguous publication is reconciled with its durable generation evidence`() { + val source = sparseFile(25L * 1024L * 1024L) + val plan = nextcloudUploadTransferPlan(source.length()) as NextcloudUploadTransferPlan.Chunked + val oldHash = "sha256:" + "11".repeat(32) + val checkpoint = newFileSyncUploadCheckpoint( + UPLOAD_ID, + "local-1", + plan, + contentRevision = "content-1", + contentHash = oldHash, + ).copy( + uploadedChunks = plan.chunkCount, + commitInFlight = true, + assembledStageEtag = "published-stage", + ) + val remote = RecordingUploadRemote(collectionCreated = true) + val persisted = mutableListOf() + try { + jvmResumableNextcloudUpload( + source, "archive.bin", "local-2", "directory-etag", checkpoint, + newUploadId = { "fedcba98-7654-3210-fedc-ba9876543210" }, + persistCheckpoint = persisted::add, + remote = remote, + contentRevision = "content-2", + contentHash = "sha256:" + "22".repeat(32), + ) + + assertEquals( + listOf( + DiscardedUpload( + assembledStageEtag = "published-stage", + expectedStageSizeBytes = checkpoint.sizeBytes, + expectedStageContentHash = oldHash, + publicationInFlight = true, + ), + ), + remote.discardedUploads, + ) + assertEquals("fedcba98-7654-3210-fedc-ba9876543210", persisted.first().uploadId) + } finally { + source.delete() + } + } + + @Test + fun `superseded ambiguous assembly is discarded only with its durable content evidence`() { + val source = sparseFile(25L * 1024L * 1024L) + val plan = nextcloudUploadTransferPlan(source.length()) as NextcloudUploadTransferPlan.Chunked + val oldHash = "sha256:" + "33".repeat(32) + val checkpoint = newFileSyncUploadCheckpoint( + UPLOAD_ID, + "local-1", + plan, + contentHash = oldHash, + ).copy( + uploadedChunks = plan.chunkCount, + commitInFlight = true, + ) + val remote = RecordingUploadRemote(collectionCreated = true) + try { + jvmResumableNextcloudUpload( + source, "large.bin", "local-2", null, checkpoint, + newUploadId = { "fedcba98-7654-3210-fedc-ba9876543210" }, + persistCheckpoint = {}, + remote = remote, + contentHash = "sha256:" + "44".repeat(32), + ) + + assertEquals( + listOf( + DiscardedUpload( + assembledStageEtag = null, + expectedStageSizeBytes = checkpoint.sizeBytes, + expectedStageContentHash = oldHash, + publicationInFlight = false, + ), + ), + remote.discardedUploads, + ) + } finally { + source.delete() + } + } + + @Test + fun `failed superseded checkpoint cleanup blocks a replacement upload`() { + val source = sparseFile(25L * 1024L * 1024L) + val plan = nextcloudUploadTransferPlan(source.length()) as NextcloudUploadTransferPlan.Chunked + val checkpoint = newFileSyncUploadCheckpoint( + UPLOAD_ID, + "local-1", + plan, + contentHash = "sha256:" + "55".repeat(32), + ).copy( + uploadedChunks = plan.chunkCount, + commitInFlight = true, + ) + val remote = RecordingUploadRemote(collectionCreated = true, cleanupComplete = false) + var allocatedReplacement = false + val persisted = mutableListOf() + try { + assertFailsWith { + jvmResumableNextcloudUpload( + source, "large.bin", "local-2", null, checkpoint, + newUploadId = { + allocatedReplacement = true + "fedcba98-7654-3210-fedc-ba9876543210" + }, + persistCheckpoint = persisted::add, + remote = remote, + contentHash = "sha256:" + "66".repeat(32), + ) + } + + assertFalse(allocatedReplacement) + assertTrue(persisted.isEmpty()) + assertTrue(remote.uploadedChunkNumbers.isEmpty()) + assertEquals(1, remote.discardedUploads.size) + } finally { + source.delete() + } + } + @Test fun `expired collection resets progress before any bytes are skipped`() { val source = sparseFile(25L * 1024L * 1024L) @@ -480,6 +604,7 @@ class JvmResumableNextcloudUploadTest { ) : JvmResumableNextcloudUploadRemote { val uploadedChunkNumbers = mutableListOf() val discardedStageEtags = mutableListOf() + val discardedUploads = mutableListOf() val finalizationEvents = mutableListOf() var discardCount = 0 var resolvePublishedCount = 0 @@ -589,10 +714,23 @@ class JvmResumableNextcloudUploadTest { ): Boolean { discardCount += 1 discardedStageEtags += assembledStageEtag + discardedUploads += DiscardedUpload( + assembledStageEtag, + expectedStageSizeBytes, + expectedStageContentHash, + publicationInFlight, + ) return cleanupComplete } } + private data class DiscardedUpload( + val assembledStageEtag: String?, + val expectedStageSizeBytes: Long?, + val expectedStageContentHash: String?, + val publicationInFlight: Boolean, + ) + private companion object { const val UPLOAD_ID = "01234567-89ab-cdef-0123-456789abcdef" } diff --git a/ui/src/jvmMain/kotlin/dev/obiente/nextcloudnative/app/JvmResumableNextcloudUpload.kt b/ui/src/jvmMain/kotlin/dev/obiente/nextcloudnative/app/JvmResumableNextcloudUpload.kt index 432957ecb..00acd46b7 100644 --- a/ui/src/jvmMain/kotlin/dev/obiente/nextcloudnative/app/JvmResumableNextcloudUpload.kt +++ b/ui/src/jvmMain/kotlin/dev/obiente/nextcloudnative/app/JvmResumableNextcloudUpload.kt @@ -161,6 +161,18 @@ fun cleanupJvmFileSyncOwnedUploads( return JvmFileSyncUploadCleanupResult(updated, unresolved) } +private fun JvmResumableNextcloudUploadRemote.discardCheckpointUpload( + checkpoint: FileSyncUploadCheckpoint, + relativePath: String, +): Boolean = discardOwnedUpload( + uploadId = checkpoint.uploadId, + relativePath = relativePath, + assembledStageEtag = checkpoint.assembledStageEtag, + expectedStageSizeBytes = checkpoint.contentHash?.let { checkpoint.sizeBytes }, + expectedStageContentHash = checkpoint.contentHash, + publicationInFlight = checkpoint.commitInFlight && checkpoint.assembledStageEtag != null, +) + /** * Runs the same crash-safe chunk state machine for every JVM platform. * @@ -253,7 +265,7 @@ fun jvmResumableNextcloudUpload( } if (plan is NextcloudUploadTransferPlan.Direct) { checkpoint?.let { - check(remote.discardOwnedUpload(it.uploadId, relativePath, it.assembledStageEtag)) { + check(remote.discardCheckpointUpload(it, relativePath)) { "An unverified upload stage still requires recovery." } } @@ -270,7 +282,7 @@ fun jvmResumableNextcloudUpload( val resumable = matchingCheckpoint?.takeIf { !it.commitInFlight } if (checkpoint != null && resumable == null) { - check(remote.discardOwnedUpload(checkpoint.uploadId, relativePath, checkpoint.assembledStageEtag)) { + check(remote.discardCheckpointUpload(checkpoint, relativePath)) { "An unverified upload stage still requires recovery." } } From 4c2e487691db6fc69e22386fe11591d97927c520 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Tue, 1 Sep 2026 20:31:33 +0200 Subject: [PATCH 2/7] chore(changelog): link pull request --- changes/unreleased/113-upload-checkpoint-recovery.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/unreleased/113-upload-checkpoint-recovery.md b/changes/unreleased/113-upload-checkpoint-recovery.md index e4ef2e58c..dcd825d91 100644 --- a/changes/unreleased/113-upload-checkpoint-recovery.md +++ b/changes/unreleased/113-upload-checkpoint-recovery.md @@ -1,6 +1,6 @@ category: fix issue: 113 -pull: none +pull: 431 platforms: android, desktop user-facing: yes From 01740ab9b65b79c9f609542aed69428ac1dc5a51 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Tue, 1 Sep 2026 20:53:28 +0200 Subject: [PATCH 3/7] fix(sync): reconcile checkpoints before preflight --- .../app/DesktopFileSyncUploadExecution.kt | 16 +++- ...sktopFileSyncReplacementPublicationTest.kt | 78 +++++++++++++++++++ .../app/JvmResumableNextcloudUpload.kt | 2 +- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncUploadExecution.kt b/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncUploadExecution.kt index 2a7ac190e..16f8f39c2 100644 --- a/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncUploadExecution.kt +++ b/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncUploadExecution.kt @@ -130,12 +130,20 @@ internal fun executeDesktopFileSyncUpload( val expectedDirectoryEtag = requireNotNull(expectedRemoteEtag) val transferPlan = nextcloudUploadTransferPlan(source.length()) if (transferPlan is NextcloudUploadTransferPlan.Chunked) { - val recoveringPublication = checkpoint?.let { - it.commitInFlight && it.localRevision == exactLocal.revision && it.transferPlan == transferPlan - } == true + val matchingCheckpoint = checkpoint?.takeIf { + it.localRevision == exactLocal.revision && it.contentRevision == exactLocal.revision && + it.contentHash == exactLocal.contentHash && it.transferPlan == transferPlan + } + if (checkpoint != null && matchingCheckpoint == null) { + check( + remote.resumableUploadRemote(shouldContinue, expectedDirectoryEtag) + .discardCheckpointUpload(checkpoint, relativePath), + ) { "An unverified upload stage still requires recovery." } + } + val recoveringPublication = matchingCheckpoint?.commitInFlight == true if (!recoveringPublication) remote.requireDirectoryGeneration(relativePath, expectedDirectoryEtag) return resumeDesktopFileSyncUpload( - source, relativePath, exactLocal, expectedDirectoryEtag, checkpoint, + source, relativePath, exactLocal, expectedDirectoryEtag, matchingCheckpoint, persistCheckpoint, remote, shouldContinue, replacingDirectoryEtag = expectedDirectoryEtag, ) diff --git a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncReplacementPublicationTest.kt b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncReplacementPublicationTest.kt index 45a57ca7d..227041fae 100644 --- a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncReplacementPublicationTest.kt +++ b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncReplacementPublicationTest.kt @@ -1,8 +1,10 @@ package dev.obiente.nextcloudnative.app +import java.io.RandomAccessFile import java.nio.file.Files import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertTrue import okhttp3.OkHttpClient import okhttp3.Protocol @@ -185,6 +187,82 @@ class DesktopFileSyncReplacementPublicationTest { assertTrue(requestedPaths.none { it.endsWith("/archive.bin") }) } + @Test + fun `superseded published replacement is reconciled before directory preflight`() { + val uploadId = "01234567-89ab-cdef-0123-456789abcdef" + val oldPayload = ByteArray(21 * 1024 * 1024) { 1 } + val oldHash = hashExactJvmFileSyncContent(oldPayload.inputStream(), oldPayload.size.toLong()) + val requests = mutableListOf() + val client = OkHttpClient.Builder().addInterceptor { chain -> + requests += chain.request() + when (chain.request().method) { + "GET" -> response(chain.request(), 200, oldPayload) + "PROPFIND" -> response(chain.request(), 207, publishedListing(uploadId, oldPayload.size.toLong())) + "DELETE" -> response(chain.request(), 204) + else -> error("Superseded recovery must not ${chain.request().method} a new upload") + } + }.build() + val tree = DesktopFileSyncRemoteTree( + NextcloudSession("https://cloud.example.test", "alice", "secret"), + "alice", + "Vault", + client, + ownedUploadIds = setOf(uploadId), + ownedStageEtags = mapOf(uploadId to "stage-etag"), + ownedUploadPaths = mapOf(uploadId to "archive.bin"), + ownedReplacementBackupEtags = mapOf(uploadId to "directory-etag"), + ) + val source = Files.createTempFile("nextcloud-sync-superseded-replacement", ".tmp").toFile() + RandomAccessFile(source, "rw").use { it.setLength(oldPayload.size.toLong()) } + val newHash = source.inputStream().buffered().use { input -> + hashExactJvmFileSyncContent(input, source.length()) + } + val plan = nextcloudUploadTransferPlan(source.length()) as NextcloudUploadTransferPlan.Chunked + val checkpoint = newFileSyncUploadCheckpoint( + uploadId, + "local-1", + plan, + contentHash = oldHash, + ).copy( + uploadedChunks = plan.chunkCount, + commitInFlight = true, + assembledStageEtag = "stage-etag", + ) + try { + assertFailsWith { + executeDesktopFileSyncUpload( + source = source, + relativePath = "archive.bin", + exactLocal = LocalSyncEntry( + "archive.bin", + SyncEntryKind.File, + "local-2", + source.length(), + contentHash = newHash, + ), + expectedRemoteEtag = "directory-etag", + checkpoint = checkpoint, + replacingType = true, + persistCheckpoint = {}, + retainCleanup = {}, + completeCleanup = {}, + remote = tree, + shouldContinue = { true }, + ) + } + + assertEquals( + listOf("DELETE", "PROPFIND", "GET", "PROPFIND", "PROPFIND", "DELETE", "PROPFIND"), + requests.map { it.method }, + ) + assertTrue(requests.none { it.method == "PUT" || it.method == "MOVE" }) + assertTrue(requests[5].url.encodedPath.endsWith(".nextcloud-native-backup-$uploadId")) + assertTrue(requests.last().url.encodedPath.endsWith("/archive.bin")) + } finally { + assertTrue(source.delete()) + } + } + private fun stagedListing(uploadId: String?, sizeBytes: Long): String = """ diff --git a/ui/src/jvmMain/kotlin/dev/obiente/nextcloudnative/app/JvmResumableNextcloudUpload.kt b/ui/src/jvmMain/kotlin/dev/obiente/nextcloudnative/app/JvmResumableNextcloudUpload.kt index 00acd46b7..c79dd54a0 100644 --- a/ui/src/jvmMain/kotlin/dev/obiente/nextcloudnative/app/JvmResumableNextcloudUpload.kt +++ b/ui/src/jvmMain/kotlin/dev/obiente/nextcloudnative/app/JvmResumableNextcloudUpload.kt @@ -161,7 +161,7 @@ fun cleanupJvmFileSyncOwnedUploads( return JvmFileSyncUploadCleanupResult(updated, unresolved) } -private fun JvmResumableNextcloudUploadRemote.discardCheckpointUpload( +internal fun JvmResumableNextcloudUploadRemote.discardCheckpointUpload( checkpoint: FileSyncUploadCheckpoint, relativePath: String, ): Boolean = discardOwnedUpload( From de4bdcc1f404742877bb375ba9f2e4c44e94c0c0 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Tue, 1 Sep 2026 21:10:40 +0200 Subject: [PATCH 4/7] test(sync): assert recovery preflight ordering --- .../app/DesktopFileSyncReplacementPublicationTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncReplacementPublicationTest.kt b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncReplacementPublicationTest.kt index 227041fae..d4a1a3f51 100644 --- a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncReplacementPublicationTest.kt +++ b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncReplacementPublicationTest.kt @@ -257,7 +257,7 @@ class DesktopFileSyncReplacementPublicationTest { ) assertTrue(requests.none { it.method == "PUT" || it.method == "MOVE" }) assertTrue(requests[5].url.encodedPath.endsWith(".nextcloud-native-backup-$uploadId")) - assertTrue(requests.last().url.encodedPath.endsWith("/archive.bin")) + assertEquals(requests[1].url.encodedPath, requests.last().url.encodedPath) } finally { assertTrue(source.delete()) } From f0230a1ecd90935152597e21dbb727a03f1ecb85 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Tue, 1 Sep 2026 21:22:47 +0200 Subject: [PATCH 5/7] fix(sync): recover legacy publication checkpoints --- .../DesktopFileSyncRemoteTypeReplacement.kt | 4 +- ...sktopFileSyncReplacementPublicationTest.kt | 40 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncRemoteTypeReplacement.kt b/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncRemoteTypeReplacement.kt index 1cedd1fd3..83a851526 100644 --- a/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncRemoteTypeReplacement.kt +++ b/ui/src/desktopMain/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncRemoteTypeReplacement.kt @@ -143,7 +143,9 @@ internal fun DesktopFileSyncRemoteTree.reconcilePublishedReplacement( val expectedBackupEtag = ownedReplacementBackupEtags[uploadId] ?: return null val destination = resolvePhysical(relativePath, shouldContinue) ?: return null if (destination.isDirectory) return null - if (expectedSizeBytes == null || expectedContentHash == null) return false + // Older checkpoints predate durable size/hash evidence. Let the caller fall back to the + // recorded stage ETag so it can restore the protected directory without trusting the file. + if (expectedSizeBytes == null || expectedContentHash == null) return null if (destination.entry.size != expectedSizeBytes) { return discardReplacementBackup(relativePath, uploadId, assembledStageEtag = null, shouldContinue) } diff --git a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncReplacementPublicationTest.kt b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncReplacementPublicationTest.kt index d4a1a3f51..3e41e698c 100644 --- a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncReplacementPublicationTest.kt +++ b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncReplacementPublicationTest.kt @@ -156,6 +156,46 @@ class DesktopFileSyncReplacementPublicationTest { assertTrue(requests.none { it.method == "DELETE" || it.method == "GET" }) } + @Test + fun `legacy checkpoint restores its directory backup using the recorded stage etag`() { + val uploadId = "01234567-89ab-cdef-0123-456789abcdef" + val requests = mutableListOf() + val client = OkHttpClient.Builder().addInterceptor { chain -> + requests += chain.request() + when (chain.request().method) { + "PROPFIND" -> response(chain.request(), 207, publishedListing(uploadId, sizeBytes = 5)) + "DELETE" -> response(chain.request(), if (".upload" in chain.request().url.encodedPath) 404 else 204) + "MOVE" -> response(chain.request(), 201) + else -> error("Legacy recovery must not ${chain.request().method} either generation") + } + }.build() + val tree = DesktopFileSyncRemoteTree( + NextcloudSession("https://cloud.example.test", "alice", "secret"), + "alice", + "Vault", + client, + ownedUploadIds = setOf(uploadId), + ownedUploadPaths = mapOf(uploadId to "archive.bin"), + ownedReplacementBackupEtags = mapOf(uploadId to "directory-etag"), + ) + + val cleaned = tree.resumableUploadRemote(shouldContinue = { true }).discardOwnedUpload( + uploadId = uploadId, + relativePath = "archive.bin", + assembledStageEtag = "published-etag", + expectedStageSizeBytes = null, + expectedStageContentHash = null, + publicationInFlight = true, + ) + + assertTrue(cleaned) + assertTrue(requests.any { it.method == "DELETE" && it.url.encodedPath.endsWith("/archive.bin") }) + val restore = requests.single { it.method == "MOVE" } + assertTrue(restore.url.encodedPath.endsWith(".nextcloud-native-backup-$uploadId")) + assertTrue(restore.header("Destination").orEmpty().endsWith("/archive.bin")) + assertTrue(requests.none { it.method == "GET" }) + } + @Test fun `recovery scan traverses an owned backup at its physical path`() { val uploadId = "01234567-89ab-cdef-0123-456789abcdef" From c2c361bb6f87e7d74d10bd3f161af911de879c46 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Tue, 1 Sep 2026 22:11:43 +0200 Subject: [PATCH 6/7] docs(changelog): scope upload recovery to desktop --- changes/unreleased/113-upload-checkpoint-recovery.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changes/unreleased/113-upload-checkpoint-recovery.md b/changes/unreleased/113-upload-checkpoint-recovery.md index dcd825d91..0b4eceea5 100644 --- a/changes/unreleased/113-upload-checkpoint-recovery.md +++ b/changes/unreleased/113-upload-checkpoint-recovery.md @@ -1,7 +1,7 @@ category: fix issue: 113 pull: 431 -platforms: android, desktop +platforms: desktop user-facing: yes Superseded resumable uploads now retain durable size, content hash, and publication state during cleanup, so an ambiguous server result cannot mistake an already published directory replacement for an abandoned stage. From 3440b5ced07f97854a84040b9ea571ab00f48298 Mon Sep 17 00:00:00 2001 From: veryCrunchy Date: Tue, 1 Sep 2026 23:13:41 +0200 Subject: [PATCH 7/7] fix(tests): align recovery fixtures with current behavior --- .../app/DesktopFileSyncCleanupCancellationTest.kt | 3 ++- .../dev/obiente/nextcloudnative/app/JvmSupportIntakeTest.kt | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncCleanupCancellationTest.kt b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncCleanupCancellationTest.kt index 1bc9eb13d..540ac9ac3 100644 --- a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncCleanupCancellationTest.kt +++ b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/DesktopFileSyncCleanupCancellationTest.kt @@ -99,6 +99,7 @@ class DesktopFileSyncCleanupCancellationTest { """.trimIndent(), ).build(), ) + server.enqueue(MockResponse.Builder().code(412).build()) val directory = Files.createTempDirectory("desktop-sync-cleanup-block-").toFile() val localRoot = directory.resolve("local").apply { mkdirs() } val session = NextcloudSession(server.url("/").toString(), "alice", "secret") @@ -134,7 +135,7 @@ class DesktopFileSyncCleanupCancellationTest { assertIs(result) assertEquals(FileSyncRejectionScope.Preflight, result.scope) - assertEquals(2, server.requestCount) + assertEquals(3, server.requestCount) assertEquals(listOf(cleanup), store.loadPair(pair.id).coordinator.pairs.single().pendingUploadCleanups) } finally { directory.deleteRecursively() diff --git a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/JvmSupportIntakeTest.kt b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/JvmSupportIntakeTest.kt index 5f934a9d6..b8f776c5d 100644 --- a/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/JvmSupportIntakeTest.kt +++ b/ui/src/desktopTest/kotlin/dev/obiente/nextcloudnative/app/JvmSupportIntakeTest.kt @@ -2219,7 +2219,9 @@ class JvmSupportIntakeTest { val submission = launch(Dispatchers.Default) { fixture.intake.submit("A refresh failed.", "nightly", emptyList()) } - val upload = requireNotNull(fixture.server.takeRequest(2, TimeUnit.SECONDS)) + val upload = requireNotNull( + fixture.server.takeRequest(WINDOWS_REQUEST_START_TIMEOUT_SECONDS, TimeUnit.SECONDS), + ) assertTrue(fixture.intake.cancel()) submission.join()