Test terminal generation resume boundary

This commit is contained in:
fy59 2026-09-18 07:59:43 +02:00
parent 1fc15d6214
commit 823184d99e
2 changed files with 52 additions and 4 deletions

View file

@ -17,7 +17,7 @@ internal sealed interface ForegroundGenerationResult {
internal class SyncGenerationForegroundCoordinator(private val repository: TrainlogRepository) {
private val service = SyncGenerationService(repository)
private fun resumableGeneration(runId: String, consumerPeerId: String? = null): CapturedSyncGeneration? =
internal fun resumableGeneration(runId: String, consumerPeerId: String? = null): CapturedSyncGeneration? =
repository.inSyncGenerationTransaction { db ->
db.rawQuery(
"SELECT generation_id,manifest_sha256,staging_path FROM sync_generations " +
@ -209,9 +209,9 @@ internal class SyncGenerationForegroundCoordinator(private val repository: Train
* CONTRACT: an explicit retry for the same run republishes that
* exact generation and resumes ACK handling; it never recaptures
* mutable application state under an existing identity.
* INVARIANT: only an active generation for the correlated desktop
* consumer is resumable; acknowledged or archived runs remain
* excluded from new foreground work. */
* INVARIANT: a completed inbound ledger makes the run ineligible;
* an acknowledged outbound generation remains resumable only to
* finish its missing inbound half. */
val captured =
resumableGeneration(
request.getString("run_id"),

View file

@ -26,6 +26,54 @@ import org.robolectric.annotation.Config
class SyncGenerationServiceTest {
private val context: Context = ApplicationProvider.getApplicationContext()
@Test
fun acknowledgedOutboundRemainsResumableUntilInboundIsTerminal() {
val sourceName = "generation-resume-source-${UUID.randomUUID()}.db"
val destinationName = "generation-resume-destination-${UUID.randomUUID()}.db"
val root = Files.createTempDirectory("trainlog-generation-resume-").toFile()
val source = TrainlogRepository(context, sourceName)
val destination = TrainlogRepository(context, destinationName)
try {
val sourceService = SyncGenerationService(source)
val destinationService = SyncGenerationService(destination)
val runId = "sy_${UUID.randomUUID()}"
val outbound =
sourceService.capture(
java.io.File(root, "source-owned"),
destinationService.peerId(),
runId,
)
val outboundDirectory =
sourceService.publish(outbound, java.io.File(root, "source-objects"))
sourceService.acceptAcknowledgement(
destinationService.consume(outboundDirectory).toByteArray()
)
val coordinator = SyncGenerationForegroundCoordinator(source)
assertEquals(
outbound.generationId,
coordinator.resumableGeneration(runId, destinationService.peerId())?.generationId,
)
val inbound =
destinationService.capture(
java.io.File(root, "destination-owned"),
sourceService.peerId(),
runId,
)
sourceService.consume(
destinationService.publish(inbound, java.io.File(root, "destination-objects"))
)
assertEquals(null, coordinator.resumableGeneration(runId, destinationService.peerId()))
} finally {
source.close()
destination.close()
context.deleteDatabase(sourceName)
context.deleteDatabase(destinationName)
root.deleteRecursively()
}
}
@Test
fun v19MigrationPreservesDataAndInventsNoGenerationState() {
val name = "generation-migration-${UUID.randomUUID()}.db"