ComposerPersistenceTests.kt (7062B)
1 package tera.smoke 2 3 import java.nio.ByteBuffer 4 import kotlin.test.Test 5 import kotlin.test.assertEquals 6 import kotlin.test.assertFailsWith 7 import kotlin.test.assertFalse 8 import kotlin.test.assertNotNull 9 import kotlin.test.assertNull 10 import kotlin.test.assertTrue 11 import kotlinx.coroutines.runBlocking 12 import uniffi.tera_core.FfiAddCommandType 13 import uniffi.tera_core.FfiComposerFormRecord 14 import uniffi.tera_core.FfiComposerListEntry 15 import uniffi.tera_core.FfiComposerMediaRecord 16 import uniffi.tera_core.FfiComposerRepairReason 17 import uniffi.tera_core.FfiComposerSaveRequest 18 import uniffi.tera_core.FfiComposerScopeRecord 19 import uniffi.tera_core.FfiConverterTypeFfiComposerListEntry 20 import uniffi.tera_core.FfiConverterTypeFfiComposerSaveReceipt 21 import uniffi.tera_core.FfiEventTimingKind 22 import uniffi.tera_core.FfiRecoveryDisposition 23 import uniffi.tera_core.TeraAppException 24 import uniffi.tera_core.classifyErrorRecovery 25 import uniffi.tera_core.composerReserveId 26 27 class ComposerPersistenceTests { 28 private val scope = FfiComposerScopeRecord(1u, PUBLIC_KEY, "nearby") 29 30 private fun request() = FfiComposerSaveRequest( 31 schemaVersion = 1u, scope = scope, id = composerReserveId().id, 32 expectedRevision = null, editSequence = ULong.MAX_VALUE - 1uL, 33 form = FfiComposerFormRecord( 34 schemaVersion = 1u, 35 commandType = FfiAddCommandType.CREATE_EVENT, 36 content = " private incomplete\n\u0000é ", 37 identifier = null, 38 title = null, 39 summary = null, 40 location = null, 41 eventTiming = FfiEventTimingKind.ALL_DAY, 42 eventStartDate = "2026-09-", 43 eventEndDate = "", 44 eventStartUnixS = ULong.MAX_VALUE, 45 eventEndUnixS = null, 46 eventTimezone = "Mars/unfinished", 47 priceAmount = "12.", 48 currency = null, 49 unit = null, 50 quantity = "-", 51 foodPublishedAtUnixS = null, 52 foodStatus = null, 53 media = listOf(FfiComposerMediaRecord(1u, "media:abc", "ab".repeat(32), "image/png", 24uL, 2u, 2u, "", UNIX_S)), 54 ), 55 ) 56 57 @Test 58 fun actualNativeComposerPreservesPartialFormsExactUnsignedReceiptsAndRestart(): Unit = runBlocking { 59 SmokeFixture().use { fixture -> 60 val runtime = fixture.runtime() 61 val input = request() 62 val saved = try { 63 val first = runtime.composerSave(input) 64 assertEquals(1uL, first.draft.revision) 65 assertEquals(scope, first.draft.scope) 66 assertEquals(input.id, first.draft.id) 67 assertEquals(input.form, first.draft.form) 68 assertEquals(ULong.MAX_VALUE - 1uL, first.draft.editSequence) 69 assertFalse(first.replayed) 70 val next = input.copy(expectedRevision = 1uL, editSequence = ULong.MAX_VALUE, 71 form = input.form.copy(content = "newest incomplete")) 72 val receipt = runtime.composerSave(next) 73 assertEquals(2uL, receipt.draft.revision) 74 assertEquals(ULong.MAX_VALUE, receipt.draft.editSequence) 75 assertEquals(next.form, receipt.draft.form) 76 assertEquals(receipt, FfiConverterTypeFfiComposerSaveReceipt.lift( 77 FfiConverterTypeFfiComposerSaveReceipt.lower(receipt))) 78 val stale = assertFailsWith<TeraAppException.Failure> { runtime.composerSave(next) } 79 assertEquals("composer_revision_conflict", stale.report.code) 80 assertEquals(listOf("reload_composer"), stale.report.recoveryActions) 81 assertEquals(FfiRecoveryDisposition.STALE_REVISION, classifyErrorRecovery(1u, stale.report.code).disposition) 82 assertTrue(runtime.phase1DraftHeads(100u).isEmpty()) 83 receipt 84 } finally { 85 runtime.shutdown() 86 runtime.close() 87 } 88 val reopened = fixture.runtime() 89 try { 90 assertEquals(saved.draft, reopened.composerLoad(scope, input.id)) 91 val second = reopened.composerSave(request()) 92 val page = reopened.composerList(1u, scope, 1u, null) 93 assertEquals(scope, page.scope) 94 assertEquals(1, page.entries.size) 95 val cursor = assertNotNull(page.nextCursor) 96 val last = reopened.composerList(1u, scope, 1u, cursor) 97 assertNull(last.nextCursor) 98 assertEquals(setOf(input.id, second.draft.id), (page.entries + last.entries).map { 99 (it as FfiComposerListEntry.Draft).summary.id 100 }.toSet()) 101 val foreign = scope.copy(localNetworkId = "elsewhere") 102 assertEquals("composer_scope_mismatch", 103 assertFailsWith<TeraAppException.Failure> { reopened.composerLoad(foreign, input.id) }.report.code) 104 assertEquals("composer_scope_mismatch", 105 assertFailsWith<TeraAppException.Failure> { reopened.composerList(1u, foreign, 1u, cursor) }.report.code) 106 assertTrue(reopened.composerList(1u, foreign, 10u, null).entries.isEmpty()) 107 } finally { 108 reopened.shutdown() 109 reopened.close() 110 } 111 } 112 } 113 114 @Test 115 fun unsupportedNestedVersionsAndMalformedVariantsFailClosed(): Unit = runBlocking { 116 SmokeFixture().use { fixture -> 117 val runtime = fixture.runtime() 118 try { 119 val input = request() 120 for (invalid in listOf( 121 input.copy(schemaVersion = 2u), 122 input.copy(scope = scope.copy(schemaVersion = 2u)), 123 input.copy(form = input.form.copy(schemaVersion = 2u)), 124 input.copy(form = input.form.copy(media = listOf(input.form.media[0].copy(schemaVersion = 2u)))), 125 )) { 126 val failure = assertFailsWith<TeraAppException.Failure> { runtime.composerSave(invalid) } 127 assertEquals(1u.toUShort(), failure.report.schemaVersion) 128 assertEquals("composer_schema_unsupported", failure.report.code) 129 assertFalse(failure.report.retryable) 130 } 131 assertTrue(runtime.composerList(1u, scope, 10u, null).entries.isEmpty()) 132 val repair = FfiComposerListEntry.Repair("00".repeat(16), ULong.MAX_VALUE, FfiComposerRepairReason.CORRUPT_RECORD) 133 assertEquals(repair, FfiConverterTypeFfiComposerListEntry.lift(FfiConverterTypeFfiComposerListEntry.lower(repair))) 134 for (bytes in listOf(byteArrayOf(), byteArrayOf(0, 0, 0, 99), byteArrayOf(0, 0, 0, 1))) { 135 assertFailsWith<RuntimeException> { FfiConverterTypeFfiComposerListEntry.read(ByteBuffer.wrap(bytes)) } 136 } 137 } finally { 138 runtime.shutdown() 139 runtime.close() 140 } 141 } 142 } 143 }