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:
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",