app

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

commit 5dfce7dea1e2e111955dbebff1655950aaf68b48
parent fee6dce0c8a11b71b86f4c4cae7120031271724f
Author: triesap <tyson@radroots.org>
Date:   Thu, 13 Aug 2026 05:47:17 +0000

test: qualify automatic removal expiry

- cover both terminal winner orders with virtual time
- quarantine uncertain expiry release without retries
- lock exact active-handle and post-close invariants
- enforce expiry and custody source policy

Diffstat:
Mapp/shared/src/commonTest/kotlin/org/harvestcircle/application/HarvestCirclePresenterTest.kt | 203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/lib.rs | 36+++++++++++++++++++++++++++++++++++-
2 files changed, 238 insertions(+), 1 deletion(-)

diff --git a/app/shared/src/commonTest/kotlin/org/harvestcircle/application/HarvestCirclePresenterTest.kt b/app/shared/src/commonTest/kotlin/org/harvestcircle/application/HarvestCirclePresenterTest.kt @@ -454,6 +454,207 @@ class HarvestCirclePresenterTest { } @Test + fun hcEx002ConfirmBeforeExpiryWinsWithoutCancellation() = + runTest { + val runtime = FakePresenterRuntime() + val presenter = presenter(runtime, clock = schedulerClock()) + runCurrent() + val identityId = IdentityId.fromPublicKeyHex("01".repeat(32)) + presenter.dispatch(HarvestCircleIntent.RequestIdentityRemoval(identityId)) + runCurrent() + val requestId = checkNotNull(presenter.state.value.removalConfirmation).requestId + + advanceTimeBy(49_999L) + presenter.dispatch(HarvestCircleIntent.ConfirmIdentityRemoval(identityId, requestId)) + runCurrent() + advanceTimeBy(1L) + runCurrent() + + assertEquals(listOf(requestId), runtime.confirmedRemovalRequests) + assertEquals(0, runtime.removalCancellationCalls) + assertTrue(runtime.activeRemovalRequests.isEmpty()) + presenter.close() + } + + @Test + fun hcEx002ExpiryBeforeConfirmWinsWithoutConfirmation() = + runTest { + val runtime = FakePresenterRuntime() + val presenter = presenter(runtime, clock = schedulerClock()) + runCurrent() + val identityId = IdentityId.fromPublicKeyHex("01".repeat(32)) + presenter.dispatch(HarvestCircleIntent.RequestIdentityRemoval(identityId)) + runCurrent() + val requestId = checkNotNull(presenter.state.value.removalConfirmation).requestId + + advanceTimeBy(50_000L) + runCurrent() + presenter.dispatch(HarvestCircleIntent.ConfirmIdentityRemoval(identityId, requestId)) + runCurrent() + + assertTrue(runtime.confirmedRemovalRequests.isEmpty()) + assertEquals(1, runtime.removalCancellationCalls) + assertTrue(runtime.activeRemovalRequests.isEmpty()) + presenter.close() + } + + @Test + fun hcEx002CancelAndExpiryExecuteOneCancellation() = + runTest { + val runtime = FakePresenterRuntime() + val presenter = presenter(runtime, clock = schedulerClock()) + runCurrent() + val identityId = IdentityId.fromPublicKeyHex("01".repeat(32)) + presenter.dispatch(HarvestCircleIntent.RequestIdentityRemoval(identityId)) + runCurrent() + val requestId = checkNotNull(presenter.state.value.removalConfirmation).requestId + + advanceTimeBy(49_999L) + presenter.dispatch(HarvestCircleIntent.CancelIdentityRemoval(identityId, requestId)) + runCurrent() + advanceTimeBy(1L) + runCurrent() + + assertEquals(1, runtime.removalCancellationCalls) + assertTrue(runtime.activeRemovalRequests.isEmpty()) + presenter.close() + } + + @Test + fun hcEx002ExpiryBeforeCancelCannotReleaseTwice() = + runTest { + val runtime = FakePresenterRuntime() + val presenter = presenter(runtime, clock = schedulerClock()) + runCurrent() + val identityId = IdentityId.fromPublicKeyHex("01".repeat(32)) + presenter.dispatch(HarvestCircleIntent.RequestIdentityRemoval(identityId)) + runCurrent() + val requestId = checkNotNull(presenter.state.value.removalConfirmation).requestId + + advanceTimeBy(50_000L) + runCurrent() + presenter.dispatch(HarvestCircleIntent.CancelIdentityRemoval(identityId, requestId)) + runCurrent() + + assertEquals(1, runtime.removalCancellationCalls) + assertTrue(runtime.activeRemovalRequests.isEmpty()) + presenter.close() + } + + @Test + fun hcEx002ReplacementAndExpiryReleasePriorLeaseOnce() = + runTest { + val runtime = FakePresenterRuntime() + val presenter = presenter(runtime, clock = schedulerClock()) + runCurrent() + val identityId = IdentityId.fromPublicKeyHex("01".repeat(32)) + presenter.dispatch(HarvestCircleIntent.RequestIdentityRemoval(identityId)) + runCurrent() + + advanceTimeBy(49_999L) + presenter.dispatch(HarvestCircleIntent.RequestIdentityRemoval(identityId)) + runCurrent() + + assertEquals( + listOf("request:removal-1", "cancel:removal-1", "request:removal-2"), + runtime.removalEvents, + ) + assertEquals(setOf(RemovalRequestId.from("removal-2")), runtime.activeRemovalRequests) + presenter.close() + assertTrue(runtime.activeRemovalRequests.isEmpty()) + } + + @Test + fun hcEx002ExpiryBeforeReplacementStartsOnlyTheNewLease() = + runTest { + val runtime = FakePresenterRuntime() + val presenter = presenter(runtime, clock = schedulerClock()) + runCurrent() + val identityId = IdentityId.fromPublicKeyHex("01".repeat(32)) + presenter.dispatch(HarvestCircleIntent.RequestIdentityRemoval(identityId)) + runCurrent() + + advanceTimeBy(50_000L) + runCurrent() + runtime.removalExpiresAt = UnixSeconds(110) + presenter.dispatch(HarvestCircleIntent.RequestIdentityRemoval(identityId)) + runCurrent() + + assertEquals( + listOf("request:removal-1", "cancel:removal-1", "request:removal-2"), + runtime.removalEvents, + ) + assertEquals(setOf(RemovalRequestId.from("removal-2")), runtime.activeRemovalRequests) + presenter.close() + } + + @Test + fun hcEx003FailedAutomaticCancellationIsQuarantinedWithoutRetry() = + runTest { + val runtime = FakePresenterRuntime().also { it.removalCancellationResult = false } + val presenter = presenter(runtime, clock = schedulerClock()) + runCurrent() + presenter.dispatch( + HarvestCircleIntent.RequestIdentityRemoval(IdentityId.fromPublicKeyHex("01".repeat(32))), + ) + runCurrent() + + advanceTimeBy(50_000L) + runCurrent() + advanceTimeBy(100_000L) + runCurrent() + + assertEquals(1, runtime.removalCancellationCalls) + assertEquals("The identity removal request could not be released safely.", presenter.state.value.problem) + assertEquals(setOf(RemovalRequestId.from("removal-1")), runtime.activeRemovalRequests) + presenter.close() + assertTrue(runtime.activeRemovalRequests.isEmpty()) + } + + @Test + fun hcEx003ExceptionalAutomaticCancellationIsQuarantinedWithoutRetry() = + runTest { + val runtime = FakePresenterRuntime().also { it.removalCancellationFailure = problem(retryable = false) } + val presenter = presenter(runtime, clock = schedulerClock()) + runCurrent() + presenter.dispatch( + HarvestCircleIntent.RequestIdentityRemoval(IdentityId.fromPublicKeyHex("01".repeat(32))), + ) + runCurrent() + + advanceTimeBy(50_000L) + runCurrent() + advanceTimeBy(100_000L) + runCurrent() + + assertEquals(1, runtime.removalCancellationCalls) + assertEquals("The identity removal request could not be released safely.", presenter.state.value.problem) + assertEquals(setOf(RemovalRequestId.from("removal-1")), runtime.activeRemovalRequests) + presenter.close() + } + + @Test + fun hcEx003NoPostCloseExpiryMutation() = + runTest { + val runtime = FakePresenterRuntime() + val presenter = presenter(runtime, clock = schedulerClock()) + runCurrent() + presenter.dispatch( + HarvestCircleIntent.RequestIdentityRemoval(IdentityId.fromPublicKeyHex("01".repeat(32))), + ) + runCurrent() + + presenter.close() + val closedState = presenter.state.value + advanceTimeBy(100_000L) + runCurrent() + + assertEquals(closedState, presenter.state.value) + assertEquals(0, runtime.removalCancellationCalls) + assertTrue(runtime.activeRemovalRequests.isEmpty()) + } + + @Test fun hcSc001FailedRemovalRequestExposesNoConfirmation() = runTest { val runtime = FakePresenterRuntime().also { it.removalFailure = problem(retryable = false) } @@ -658,6 +859,8 @@ class HarvestCirclePresenterTest { clock = clock, operationIds = ids, ) + + private fun TestScope.schedulerClock() = ApplicationClock { UnixSeconds(10L + testScheduler.currentTime / 1_000L) } } private class DeterministicOperationIds : OperationIdSource { diff --git a/tools/xtask/src/lib.rs b/tools/xtask/src/lib.rs @@ -421,10 +421,14 @@ fn product_shell_audit(root: &Path, inventory: &Inventory, findings: &mut Vec<St ( "app/shared/src/commonTest/kotlin/org/harvestcircle/application/HarvestCirclePresenterTest.kt", &[ - "hcSl002", "hcSl003", "hcSl004", "hcSl005", "hcEx001", "hcEx002", + "hcSl002", "hcSl003", "hcSl004", "hcSl005", "hcEx001", "hcEx002", "hcEx003", ], ), ( + "app/shared/src/desktopTest/kotlin/org/harvestcircle/ui/shell/BootstrapIdentityEntryTest.kt", + &["hcEx004"], + ), + ( "app/shared/src/commonTest/kotlin/org/harvestcircle/application/HarvestCircleShellPresenterTest.kt", &["hcSl001", "hcSl006"], ), @@ -485,6 +489,13 @@ fn product_shell_audit(root: &Path, inventory: &Inventory, findings: &mut Vec<St "app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/FoundationOverlayHost.kt", &["val overlayBusy = (overlay as? FoundationOverlay.ConfirmAction)?.busy == true"], ), + ( + "app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/BootstrapIdentityEntry.kt", + &[ + "The secret is held only for this import.", + "It is cleared after it is sent to the local native runtime.", + ], + ), ]; for (path, markers) in closure_source_contract { let source = read_text(root, path); @@ -496,6 +507,29 @@ fn product_shell_audit(root: &Path, inventory: &Inventory, findings: &mut Vec<St } } } + let presenter_tests = read_text( + root, + "app/shared/src/commonTest/kotlin/org/harvestcircle/application/HarvestCirclePresenterTest.kt", + ); + for forbidden in ["Thread.sleep", "kotlinx.coroutines.delay("] { + if presenter_tests.contains(forbidden) { + findings.push(format!( + "automatic-expiry tests must use virtual time, not {forbidden}" + )); + } + } + let bootstrap_entry = read_text( + root, + "app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/BootstrapIdentityEntry.kt", + ); + let retired_copy = [ + "The secret is sent directly to the local native runtime ", + "and is not retained in the interface.", + ] + .concat(); + if bootstrap_entry.contains(&retired_copy) { + findings.push("Bootstrap identity entry retains retired secret-custody copy".to_owned()); + } let locked_copy = [ ( "app/shared/src/commonMain/kotlin/org/harvestcircle/ui/shell/HarvestCircleShell.kt",