app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

commit 95e74b76287369d1cd507d049272913dcabec766
parent f2a93277920c34c17de868dff458c7098c816738
Author: triesap <tyson@radroots.org>
Date:   Mon, 10 Aug 2026 02:46:02 +0000

desktop: reduce the host to platform composition

- enforce the approved initial and minimum desktop window dimensions
- retain macOS title-bar and icon setup behind the desktop entry point
- remove the superseded parallel native core gateway and its legacy test
- exercise generated recovery through the sole desktop runtime adapter

Diffstat:
Dapp/desktop/src/main/kotlin/org/harvestcircle/application/HarvestCircleCoreGateway.kt | 307-------------------------------------------------------------------------------
Mapp/desktop/src/main/kotlin/org/harvestcircle/desktop/Main.kt | 8++++----
Dapp/desktop/src/test/kotlin/org/harvestcircle/application/HarvestCircleCoreGatewayTest.kt | 24------------------------
Mapp/desktop/src/test/kotlin/org/harvestcircle/application/NativeGeneratedRecoveryTest.kt | 25+++++++++----------------
Mapp/desktop/src/test/kotlin/org/harvestcircle/desktop/MainTest.kt | 9+++++++++
5 files changed, 22 insertions(+), 351 deletions(-)

diff --git a/app/desktop/src/main/kotlin/org/harvestcircle/application/HarvestCircleCoreGateway.kt b/app/desktop/src/main/kotlin/org/harvestcircle/application/HarvestCircleCoreGateway.kt @@ -1,307 +0,0 @@ -package org.harvestcircle.application - -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.runBlocking -import org.harvestcircle.ffi.AppSnapshotDto -import org.harvestcircle.ffi.GeneratedRecoveryRequest -import org.harvestcircle.ffi.HarvestCircleAppCore -import org.harvestcircle.ffi.HarvestCircleChangeObserver -import org.harvestcircle.ffi.HarvestCircleException -import org.harvestcircle.ffi.IdentityDto -import org.harvestcircle.ffi.ObserverSubscription -import org.harvestcircle.ffi.RemovalRequest -import org.harvestcircle.ffi.RequestContextDto -import org.harvestcircle.ffi.SnapshotChangeDto -import org.harvestcircle.ffi.WireErrorCategory -import org.harvestcircle.ffi.WireErrorCode -import org.harvestcircle.ffi.WireRecoveryAction -import java.util.concurrent.atomic.AtomicLong - -interface RemovalTicket : AutoCloseable { - val publicKeyHex: String - val deletesLocalCredential: Boolean - val signsOut: Boolean - val expiresAtSeconds: Long -} - -interface GeneratedRecoveryTicket : AutoCloseable { - val requestId: String - val identity: IdentityDto - - fun takeRecoveryNsec(): String - - suspend fun acknowledge(): AppSnapshotDto - - suspend fun cancel(): Boolean -} - -data class HarvestCircleChange( - val snapshot: AppSnapshotDto, - val previousRevision: ULong?, -) - -sealed interface HarvestCircleCommand { - data class ImportIdentity( - val bytes: ByteArray, - ) : HarvestCircleCommand - - data class SelectIdentity( - val publicKeyHex: String, - ) : HarvestCircleCommand - - data class ActivateIdentity( - val publicKeyHex: String, - ) : HarvestCircleCommand - - data object SignOut : HarvestCircleCommand - - data object RefreshProfile : HarvestCircleCommand -} - -data class HarvestCircleCommandReceipt( - val requestId: String, - val committedRevision: ULong, - val snapshot: AppSnapshotDto, -) - -data class HarvestCircleCommandFailure( - val code: WireErrorCode, - val category: WireErrorCategory, - val retryable: Boolean, - val recoveryAction: WireRecoveryAction, - val correlationId: String?, - val safeMessage: String, -) - -data class HarvestCircleShutdownReceipt( - val finalRevision: ULong, - val closed: Boolean, -) - -sealed interface HarvestCircleCommandResult { - data class Accepted( - val receipt: HarvestCircleCommandReceipt, - ) : HarvestCircleCommandResult - - data class Rejected( - val failure: HarvestCircleCommandFailure, - ) : HarvestCircleCommandResult -} - -interface HarvestCircleCoreGateway : AutoCloseable { - fun snapshot(): AppSnapshotDto - - suspend fun subscribeChanges(onChange: (HarvestCircleChange) -> Unit): AutoCloseable - - suspend fun execute(command: HarvestCircleCommand): HarvestCircleCommandResult - - suspend fun bootstrap(): AppSnapshotDto - - suspend fun beginGeneratedIdentity(): GeneratedRecoveryTicket - - suspend fun requestIdentityRemoval(publicKeyHex: String): RemovalTicket - - suspend fun confirmIdentityRemoval(ticket: RemovalTicket): AppSnapshotDto - - fun shutdown(): HarvestCircleShutdownReceipt -} - -class NativeHarvestCircleCoreGateway( - private val core: HarvestCircleAppCore, -) : HarvestCircleCoreGateway { - private val nextRequest = AtomicLong(1) - private val shutdownLock = Any() - private var shutdownReceipt: HarvestCircleShutdownReceipt? = null - - override fun snapshot(): AppSnapshotDto = core.snapshot() - - override suspend fun subscribeChanges(onChange: (HarvestCircleChange) -> Unit): AutoCloseable { - val subscription = - core.subscribeChangesV2( - object : HarvestCircleChangeObserver { - override fun onChange(change: SnapshotChangeDto) { - onChange(HarvestCircleChange(change.snapshot, change.previousRevision)) - } - }, - ) - return NativeSubscription(subscription) - } - - override suspend fun execute(command: HarvestCircleCommand): HarvestCircleCommandResult { - val context = requestContext() - return try { - val snapshot = - when (command) { - is HarvestCircleCommand.ImportIdentity -> - try { - core.importIdentity(context, command.bytes).snapshot - } finally { - command.bytes.fill(0) - } - is HarvestCircleCommand.SelectIdentity -> core.selectIdentity(command.publicKeyHex) - is HarvestCircleCommand.ActivateIdentity -> core.activateIdentity(command.publicKeyHex) - HarvestCircleCommand.SignOut -> core.signOut() - HarvestCircleCommand.RefreshProfile -> core.refreshActiveProfile() - } - HarvestCircleCommandResult.Accepted( - HarvestCircleCommandReceipt(context.requestId, snapshot.revision, snapshot), - ) - } catch (error: CancellationException) { - throw error - } catch (error: Exception) { - HarvestCircleCommandResult.Rejected(error.toHarvestCircleCommandFailure(context.requestId)) - } - } - - override suspend fun bootstrap(): AppSnapshotDto = core.bootstrap() - - override suspend fun beginGeneratedIdentity(): GeneratedRecoveryTicket { - val requestId = nextRequestId() - return try { - val request = core.beginGeneratedIdentity() - try { - NativeGeneratedRecoveryTicket(core, request, ::requestContext, requestId, request.identity()) - } catch (error: Exception) { - request.close() - throw error - } - } catch (error: CancellationException) { - throw error - } catch (error: Exception) { - throw HarvestCircleGatewayException( - error.toHarvestCircleCommandFailure( - requestId, - "The generated key could not be prepared.", - ), - ) - } - } - - override suspend fun requestIdentityRemoval(publicKeyHex: String): RemovalTicket = - NativeRemovalTicket(core.requestIdentityRemoval(publicKeyHex)) - - override suspend fun confirmIdentityRemoval(ticket: RemovalTicket): AppSnapshotDto { - require(ticket is NativeRemovalTicket) { "Removal ticket does not belong to native core" } - return core.confirmIdentityRemoval(requestContext(), ticket.request) - } - - override fun shutdown(): HarvestCircleShutdownReceipt = - synchronized(shutdownLock) { - shutdownReceipt ?: run { - val receipt = runBlocking { core.shutdownV2() } - HarvestCircleShutdownReceipt(receipt.finalRevision, receipt.closed).also { - check(it.closed) { "Native runtime returned an incomplete shutdown receipt" } - shutdownReceipt = it - core.close() - } - } - } - - override fun close() { - shutdown() - } - - private fun requestContext(): RequestContextDto = - RequestContextDto( - requestId = nextRequestId(), - expectedRevision = core.snapshot().revision, - deadlineMillis = 30_000UL, - ) - - private fun nextRequestId(): String = "kotlin:${nextRequest.getAndIncrement()}" -} - -internal class HarvestCircleGatewayException( - val failure: HarvestCircleCommandFailure, -) : Exception(failure.safeMessage) - -internal fun Throwable.toHarvestCircleCommandFailure( - fallbackCorrelationId: String, - fallbackSafeMessage: String = "The application command failed.", -): HarvestCircleCommandFailure { - val native = this as? HarvestCircleException.Failure - return HarvestCircleCommandFailure( - code = native?.code ?: WireErrorCode.INTERNAL, - category = native?.category ?: WireErrorCategory.INTERNAL, - retryable = native?.retryable ?: false, - recoveryAction = native?.recoveryAction ?: WireRecoveryAction.NONE, - correlationId = native?.correlationId ?: fallbackCorrelationId, - safeMessage = native?.safeMessage ?: fallbackSafeMessage, - ) -} - -private class NativeSubscription( - private val subscription: ObserverSubscription, -) : AutoCloseable { - override fun close() { - try { - runBlocking { subscription.unsubscribe() } - } finally { - subscription.close() - } - } -} - -private class NativeRemovalTicket( - val request: RemovalRequest, -) : RemovalTicket { - override val publicKeyHex: String = request.publicKeyHex() - override val deletesLocalCredential: Boolean = request.deletesLocalCredential() - override val signsOut: Boolean = request.signsOut() - override val expiresAtSeconds: Long = request.expiresAtSeconds() - - override fun close() { - request.close() - } -} - -private class NativeGeneratedRecoveryTicket( - private val core: HarvestCircleAppCore, - private val request: GeneratedRecoveryRequest, - private val requestContext: () -> RequestContextDto, - override val requestId: String, - override val identity: IdentityDto, -) : GeneratedRecoveryTicket { - override fun takeRecoveryNsec(): String = - try { - request.takeRecoveryNsec() - } catch (error: Exception) { - throw HarvestCircleGatewayException( - error.toHarvestCircleCommandFailure( - requestId, - "The generated recovery key could not be read.", - ), - ) - } - - override suspend fun acknowledge(): AppSnapshotDto { - val context = requestContext() - return call("The generated identity could not be saved.", context.requestId) { - core.acknowledgeGeneratedIdentity(context, request) - } - } - - override suspend fun cancel(): Boolean = - call("The generated key could not be cancelled safely.") { - core.cancelGeneratedIdentity(request) - } - - override fun close() { - request.close() - } - - private suspend fun <T> call( - fallbackSafeMessage: String, - correlationId: String = requestId, - operation: suspend () -> T, - ): T = - try { - operation() - } catch (error: CancellationException) { - throw error - } catch (error: Exception) { - throw HarvestCircleGatewayException( - error.toHarvestCircleCommandFailure(correlationId, fallbackSafeMessage), - ) - } -} diff --git a/app/desktop/src/main/kotlin/org/harvestcircle/desktop/Main.kt b/app/desktop/src/main/kotlin/org/harvestcircle/desktop/Main.kt @@ -12,10 +12,10 @@ import java.awt.Taskbar import javax.imageio.ImageIO private const val APPLICATION_NAME = "HarvestCircle" -private const val INITIAL_WINDOW_WIDTH = 1284 -private const val INITIAL_WINDOW_HEIGHT = 795 -private const val MINIMUM_WINDOW_WIDTH = 1080 -private const val MINIMUM_WINDOW_HEIGHT = 720 +internal const val INITIAL_WINDOW_WIDTH = 1280 +internal const val INITIAL_WINDOW_HEIGHT = 800 +internal const val MINIMUM_WINDOW_WIDTH = 1100 +internal const val MINIMUM_WINDOW_HEIGHT = 720 private val isMacOs: Boolean = System diff --git a/app/desktop/src/test/kotlin/org/harvestcircle/application/HarvestCircleCoreGatewayTest.kt b/app/desktop/src/test/kotlin/org/harvestcircle/application/HarvestCircleCoreGatewayTest.kt @@ -1,24 +0,0 @@ -package org.harvestcircle.application - -import org.harvestcircle.ffi.WireErrorCategory -import org.harvestcircle.ffi.WireErrorCode -import org.harvestcircle.ffi.WireRecoveryAction -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse - -class HarvestCircleCoreGatewayTest { - @Test - fun unknownFailuresBecomeSanitizedTypedRejections() { - val failure = - IllegalStateException("sensitive detail") - .toHarvestCircleCommandFailure("request-7") - - assertEquals(WireErrorCode.INTERNAL, failure.code) - assertEquals(WireErrorCategory.INTERNAL, failure.category) - assertEquals(WireRecoveryAction.NONE, failure.recoveryAction) - assertEquals("request-7", failure.correlationId) - assertEquals("The application command failed.", failure.safeMessage) - assertFalse(failure.toString().contains("sensitive detail")) - } -} diff --git a/app/desktop/src/test/kotlin/org/harvestcircle/application/NativeGeneratedRecoveryTest.kt b/app/desktop/src/test/kotlin/org/harvestcircle/application/NativeGeneratedRecoveryTest.kt @@ -1,8 +1,6 @@ package org.harvestcircle.application import kotlinx.coroutines.test.runTest -import org.harvestcircle.ffi.HarvestCircleAppCore -import org.harvestcircle.ffi.compatibilityDescriptor import java.nio.file.Files import java.nio.file.Path import kotlin.test.AfterTest @@ -30,24 +28,19 @@ class NativeGeneratedRecoveryTest { @Test fun generatedRecoveryCrossesTheNativeBoundaryAndCancelsWithoutPersistence() = runTest { - val core = - HarvestCircleAppCore.openCompatible( - expectation = verifyNativeCompatibility(compatibilityDescriptor()), - developmentMode = true, - ) - val gateway = NativeHarvestCircleCoreGateway(core) + val runtime = NativeHarvestCircleRuntime.open(developmentMode = true) try { - gateway.bootstrap() - val recovery = gateway.beginGeneratedIdentity() + runtime.bootstrap() + val recovery = runtime.prepareLocalIdentity() assertTrue(recovery.identity.npub.startsWith("npub1")) - assertTrue(recovery.takeRecoveryNsec().startsWith("nsec1")) - assertTrue(recovery.cancel()) - assertFalse(recovery.cancel()) - assertEquals(0, gateway.snapshot().identities.size) - recovery.close() + assertTrue(recovery.backup.revealNsec().startsWith("nsec1")) + runtime.execute(ApplicationCommand.CancelGeneratedIdentity(recovery.requestId)) + recovery.backup.clear() + assertFalse(runtime.currentSnapshot().identities.isNotEmpty()) + assertEquals(0, runtime.currentSnapshot().identities.size) } finally { - gateway.shutdown() + runtime.shutdown() } } } diff --git a/app/desktop/src/test/kotlin/org/harvestcircle/desktop/MainTest.kt b/app/desktop/src/test/kotlin/org/harvestcircle/desktop/MainTest.kt @@ -2,10 +2,19 @@ package org.harvestcircle.desktop import java.io.ByteArrayInputStream import kotlin.test.Test +import kotlin.test.assertEquals import kotlin.test.assertNull class MainTest { @Test + fun desktopWindowUsesTheFoundationDimensions() { + assertEquals(1280, INITIAL_WINDOW_WIDTH) + assertEquals(800, INITIAL_WINDOW_HEIGHT) + assertEquals(1100, MINIMUM_WINDOW_WIDTH) + assertEquals(720, MINIMUM_WINDOW_HEIGHT) + } + + @Test fun missingOrInvalidRuntimeIconFailsSafely() { assertNull(loadRuntimeIcon { null }) assertNull(loadRuntimeIcon { ByteArrayInputStream("not an image".encodeToByteArray()) })