rastrillo / native Public

Retain known sync status while protected storage is unavailable

Paul Campbell pushed by paul@keymail.dev 4d40c6f145bbfd6d82f4c87a6226e2833f63704e parent 3e93ef9
3 files changed, +57 −9
  • Sources/RastrilloApple/OutboxDrainer.swift +9 −2
  • Tests/RastrilloAppleTests/OutboxDrainerTests.swift +21 −0
  • docs/testing/k3-evidence.md +27 −7

CI failed — run details

diff --git a/Sources/RastrilloApple/OutboxDrainer.swift b/Sources/RastrilloApple/OutboxDrainer.swift
index aab2fc0..d3fc1cf 100644
--- a/Sources/RastrilloApple/OutboxDrainer.swift
+++ b/Sources/RastrilloApple/OutboxDrainer.swift
@@ -66,6 +66,7 @@ import RastrilloNative
private let send: Send
private let maxBatchBytes: Int?
private var backoff: Backoff
+ @ObservationIgnored private var knownStatuses: [String: SyncStatus] = [:]
private var draining = false
private var drainWaiters: [CheckedContinuation<Outcome, Never>] = []
private var worker: Task<Void, Never>?
@@ -218,7 +219,13 @@ import RastrilloNative
count: (OutboxSummary) -> Int = { $0.pendingLanes }) -> SyncStatus {
let state = lane.map { summary.only($0) } ?? summary
let offline = transport().map { connectivity.isOffline($0.origin) } ?? !connectivity.isPathSatisfied
- return SyncStatus(summary: state, sending: state.inFlight > 0, sessionEnded: sessionEnded,
- offline: offline, backingOff: phase == .backingOff, localFailure: localFailure, count: count)
+ let key = lane.map { "lane:" + $0 } ?? "account"
+ if state.unavailable, localFailure == nil, let known = knownStatuses[key] { return known }
+ let status = SyncStatus(summary: state, sending: state.inFlight > 0, sessionEnded: sessionEnded,
+ offline: offline, backingOff: phase == .backingOff, localFailure: localFailure, count: count)
+ // A locked store retains the last observable status. An app's explicit
+ // enqueue failure still reports input that has not reached durable storage.
+ if !state.unavailable, localFailure == nil { knownStatuses[key] = status }
+ return status
}
}
diff --git a/Tests/RastrilloAppleTests/OutboxDrainerTests.swift b/Tests/RastrilloAppleTests/OutboxDrainerTests.swift
index 163de59..b27b4f9 100644
--- a/Tests/RastrilloAppleTests/OutboxDrainerTests.swift
+++ b/Tests/RastrilloAppleTests/OutboxDrainerTests.swift
@@ -60,6 +60,27 @@ import XCTest
XCTAssertTrue(summary.isEmpty)
}
+ func testUnavailableStoreKeepsTheLastKnownStatusUntilUnlock() async throws {
+ let (connectivity, _) = await connectivity(.unsatisfied)
+ let disk = FaultingBlobStore()
+ let outbox = Outbox(store: disk, account: "a")
+ try await outbox.enqueue(lane: "doc:a", kind: "update", payload: Data([1]))
+ let drainer = OutboxDrainer(outbox: outbox, transport: { ScriptedTransport([]) }, connectivity: connectivity, send: poster())
+ await drainer.drain()
+ XCTAssertEqual(drainer.status(), .offline(pending: 1))
+ disk.unavailable = true
+ // Force an I/O operation even after a successful initial load.
+ do { try await outbox.enqueue(lane: "doc:a", kind: "update", payload: Data([2])); XCTFail("Locked store must refuse enqueue") }
+ catch BlobStoreError.unavailable { }
+ await drainer.drain()
+ XCTAssertEqual(drainer.status(), .offline(pending: 1), "Unavailable keeps the known status; it is not lost")
+ XCTAssertEqual(drainer.status(localFailure: .cannotSaveOnDevice), .failed(pending: 1, reason: .cannotSaveOnDevice), "The app still reports undurable input explicitly")
+ disk.unavailable = false
+ await drainer.drain()
+ XCTAssertEqual(drainer.status(), .offline(pending: 1))
+ XCTAssertEqual(disk.committed.contents.count, 1)
+ }
+
func testDrainsEachLaneInOrderAndAcknowledges() async throws {
let (connectivity, _) = await connectivity()
let transport = ScriptedTransport([.respond(status: 204), .respond(status: 204)])
diff --git a/docs/testing/k3-evidence.md b/docs/testing/k3-evidence.md
index c463970..5de288c 100644
--- a/docs/testing/k3-evidence.md
+++ b/docs/testing/k3-evidence.md
@@ -1,4 +1,4 @@
-# K3 validation (in progress)
+# K3 validation
Continuing WIP 68a51d7 against offline-kit spec 6049830 (discussion #5).
The inherited implementations were explicitly no-op stubs, not working code.
@@ -16,8 +16,8 @@ drainer/catalog suite passed 18. `make ci` passed, including macOS and iOS
companion builds, Go race tests, and Python bundle tests; transcript:
`/tmp/k3-kit-ci-1.log`. This is intermediate evidence, not final K3 approval.
-Remaining: complete Docs adoption and crash/convergence evidence, seal-count
-rotation, final audit and full gates. Nothing has been merged or deployed.
+The continuation evidence below supersedes the intermediate status. Nothing
+has been merged or deployed.
## Disk stop, 2026-09-24
@@ -38,10 +38,8 @@ Additional focused evidence before the stop:
- Fixed an inherited test's unchecked empty-array indexing so stub failures
produce an assertion instead of terminating the test process.
-The latest changes have NOT had another full `make ci`. The previous full
-pass predates rotation and lane-retry fixes. Hub CI on 05bf466 failed while
-unpacking Sparkle because `unzip` is unavailable; no macOS runner exists.
-This branch is WIP and must not merge.
+At that disk stop, the latest changes had not had another full make ci.
+The resumed local gate and later focused checks are recorded below.
Docs adoption is saved on oficina/docs `feat/k3-outbox-lane`, discussion #69.
Its evidence file lists unresolved title/lifecycle issues and remaining gates.
@@ -60,3 +58,25 @@ empty queue; entries includes held entries for offline replay; beginSend makes
selection and reservation one actor operation so compaction cannot delete a
batch in flight; endSend releases a retry/cancelled reservation without ack.
No update or retarget API was added.
+
+## Final continuation evidence
+
+- Full local `make ci` passed at 3e93ef916f2bb5469fd09375e0f6253938da8650,
+ including Swift, macOS/iOS companion builds, Go race and Python bundle checks
+ (`/tmp/k3-kit-final-ci.log`).
+- A locked store must preserve its last known status. The new regression failed
+ against the previous behavior (`/tmp/k3-unavailable-status-red.log`); all 21
+ drainer/status tests passed after the fix (`/tmp/k3-unavailable-status-green.log`).
+ Explicit app enqueue failures still show cannotSaveOnDevice; unknown storage
+ never masquerades as a known empty queue.
+- The final-head `make ci` rerun is logged at `/tmp/k3-kit-release-ci.log` and
+ its result/head is recorded on [discussion #9](https://amadan.net/rastrillo/native/discuss/9).
+- Hub CI at 3e93ef9 failed before compilation: Sparkle archive extraction could
+ not find unzip. There is no macOS runner; the local macOS gate is authoritative
+ for this task, as directed by the user.
+- Docs now has real SIGKILL red/green proof and the parked-update port. Its
+ replica/metadata/Home adoption and crash tests are on oficina/docs #69.
+
+The only deliberate ordering difference is app-side discard removing the replica
+before its lane; exhaustive crash tests prove this prevents stranded unsent text.
+The prior empty-session status exception is removed. No merge or deployment.