logs.gokuls.in

3 pull requests merged across 1 repo

Offline-Protocol/offline-protocol-sdk

Clears every residual that a review of a PR merged since v0.27.0 left on main, before the 0.28.0 cut. Each residual was a claim someone verified by hand once and nothing in CI verified afterwards: two release-workflow steps no run has exercised, sentences that described a different order or parser than the code, and branches whose tests passed with the branch deleted.

Twelve commits, one per residual family, each independent of the others:

CommitCame fromWhat changes
ci(release): tell a failing npm view from a staging publish#464The provenance verify poll keeps npm's stderr. Nothing, a warning or an E404 is staging. Any other npm error is printed, and the timeout names it instead of blaming the registry.
ci: key the iOS Rust caches on the podspec#475cargo does not rebuild Rust crates when IPHONEOS_DEPLOYMENT_TARGET changes. Both iOS caches put the podspec's hash in shared-key. It cannot go in key, which rust-cache ignores when shared-key is set.
fix(bindings): check the deployment target inside package_xcframework#474The gate moved into the pod's packaging function, which now takes the target. The Swift package's packager still relies on the CI job's own gate step, and the docs say so. The test drives the function with stand-ins for otool, lipo and xcodebuild, replacing line-order greps that missed 4 of 6 breaking edits.
fix(bindings): say where the Kotlin timestamp parser parts from Instant#475Four sentences claimed parity with Instant. The parser matches Instant for every timestamp a relay sends, and refuses hour 24, an empty fraction, lowercase t/z and a leap second, which Instant accepted. \d became [0-9], pinned by a Rust guard.
test(bindings): pin a mesh wake whose service does not start#475Robolectric's startService never returns null, so the null branch goes through an internal seam and is now tested.
ci(bindings): lint the Android library for calls newer than minSdk#475Lint's NewApi check runs alone on the library and fails the build. Generated bindings are excluded: their one newer call sits behind a Class.forName.
test(leaf): pin the pointer guard's tokenizer#476Four tokenizer behaviours get pins. The module doc names the three routes a one-file scan cannot follow.
fix(protocol): tell an unreadable sealed record from one that does not open#468A read error was reported as "does not open under this store key" and counted towards WrongStoreKey. The probe skips an unreadable record, so a readable one can still prove the key, and reports the read failure, naming the record, only when nothing opens. The listing warns with the real cause and still skips the record. The warn-once memo forgets a path when it is written or removed.
test(protocol): open the file stores over a relative root#468A new test binary pins flush_target at its call site. Before this, bypassing it passed every test.
fix(protocol): a sealed record that opens outranks a pairing-id mismatch#470A mismatch now asks the records, and only one that opens under this MLS store's record key admits the state root. Two tests pin branches that survived mutation. Three sentences said the MLS store is written first; the state store is.
fix(protocol,mls): check key-type isolation before the state suite deletes#466The FFI-exposed protocol-state suite deleted what it listed before any check ran, so a merging backend lost real records to it. It now checks isolation first and stops on a failure. Both suites gain a probe type related by suffix. Check count and names are unchanged.
fix(bindings): two orderings in the peer-stream managers#465iOS stop() clears the session before ending links. Android resets the redial delay on a new connection after a disconnect. Both are pinned by a source guard.

Already closed on main, so not in this PR: the second podspec reader and the gate's error text from #474 (fixed by #475), the rustix comment from #468, and the React Native guide's appId gap (#462). The Android library's missing INTERNET permission is by design and documented in bindings/kotlin/README.md.

Verification

  • Every behavioural fix was mutation-checked: each mutant of the fix fails a test. 31 mutants across the gate, the Kotlin parser, the wake seam, the tokenizer, the sealed store, the pairing and both suites; all caught.
  • npm verify step: extracted from the YAML and run under a fake npm for four scenarios (staging then live, persistent DNS failure, a transient failure then staging, never visible). Each ends with the right annotation.
  • Android lint: green on the library. A planted java.time call fails it, and without lint.xml the only hit is the generated Cleaner.
  • Android: 551 unit tests pass in a clean copy. The other 10 are the vector-file lookup that works only inside the repository.
  • iOS: the CI bridge typecheck step, run verbatim, is clean, and a planted type error fails it.
  • A fresh-context reviewer read the whole diff before the push. It found two blocking issues, both fixed: the cache key first went in key, which rust-cache ignores, and the docs claimed the gate covered the Swift package's packager. Its five non-blocking findings were fixed too.
  • Workspace: 3,033 unit tests pass, plus the checklist below.

Not in this PR

The release cut itself, an rc tag, and the two-phone smoke test. The public API of the native packages. The ARM32 UniFFI checksum defect. Dependabot.

Type of change

  • fix: bug fix
  • test / ci

Checklist

  • Commits follow Conventional Commits
  • cargo fmt --all -- --check
  • cargo clippy --workspace --locked -- -D warnings
  • cargo test --workspace --lib
  • RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
  • Leaf no_std clippy on all four embedded targets
  • CHANGELOG updated under [Unreleased]

Follow-up to #473. Its review left three residuals, and they went to main unfixed.

What changes

The source guard matches the class, not a list of shapes. It keeps alloc::sync out of every leaf file but src/shared.rs, because a host always has atomics and a stray import only breaks the targets without compare-and-swap. The old line matcher missed use alloc::{.., sync as s}; and use alloc::{.., sync::*};. It false-alarmed on use std::sync::{Mutex, MutexGuard}; and on a comment. It also skipped any file it could not read.

The guard now tokenizes the source, dropping comments, literals and lifetimes. It flattens every use tree and follows renames of alloc. It reports any path through alloc::sync, including one relative to crate, self or super. It also reports any glob over alloc and any rename of alloc, since a rename made in one file is visible in every other. An unreadable directory or file fails the test, and only src/shared.rs itself is exempt.

The footprint harness builds its store with shared_store. It was the only firmware-shaped code in the repo, and it still used alloc::sync::Arc::new, which the guard cannot see.

Three sentences are corrected.

  • The leaf README now says CI covers the first four rows of its target table, not the two Xtensa rows.
  • The CLAUDE.md rule now says the leaf reaches alloc::sync in one file. Four files name Arc.
  • The shared.rs module doc no longer claims host tests exercise the fallback pointer.

Verification

CheckResult
Guard pinned cases27 caught, 14 not reported
Mutants of the scanner11 of 11 killed
Real-file controls: an offending file, an unreadable fileBoth fail the test
Leaf unit, integration and doc testsPass
cargo clippy --workspace --locked -- -D warningsPass
Leaf clippy with --all-targetsPass
Leaf clippy and build, four bare-metal targets8 of 8 pass
Harness clippy, three feature setsPass
Leaf rustdoc with -D warningsPass

Footprint on rustc 1.97.1, flash bytes:

ImageBeforeAfter
Never-committing leaf, shipping profile460,260460,260
rfc_compliant with X.509472,720472,728

Both round to the published KiB figures, so no document changes.

A fresh-context review of the first push found three more missed routes: crate-relative paths, cross-file renames of alloc, and two literal forms that threw the tokenizer out of step. The last commit fixes and pins all three.

Not in this PR

These stay open from #473's plan:

  • A host run of the leaf tests against the replacement pointer. It needs the integration tests moved to shared_store first.
  • The upstream mls-rs issue about the portable-atomic backend conflict with esp-hal.
  • A CI gate for the esp-hal firmware graph, with a negative control.

What

A native iOS or Android application had no package to install. This adds both, built from the bridge sources the React Native module already compiles, where they are.

SwiftAndroid
NameOfflineProtocolSDKcom.offlineprotocol:offline-protocol-android
Built byscripts/assemble-swift-package.shthe Gradle build in bindings/kotlin
Containsgenerated bindings, every transport manager, both storage providers, the Rust librarythe same
Leaves outOfflineProtocolModule.swift, OfflineProtocolModule.mOfflineProtocolModule.kt, OfflineProtocolPackage.kt, MeshHeadlessWakeService.kt

Nothing is published by this PR. There is no release wiring in it. CI builds and tests both packages on every pull request.

Why assembled, and not moved

About fifty Rust guards read the Swift and Kotlin bridge sources by path. Most panic when a file is missing. Four skip it on purpose, and three list a directory and take what is there. A moved source would break the first kind and quietly stop being checked by the rest. ADR 0025 records the decision and what would undo it.

What this changes for the React Native package

Four small things. Everything else is new files.

  • The iOS harness shims have their own flag. They were switched on SWIFT_PACKAGE, which SwiftPM defines for every package.
  • The consumer rules gain one -dontwarn. See the first finding below.
  • Two iOS test files and one Kotlin test file are repaired.
  • Two stale comments are corrected.

Found along the way

FindingState
No Android application that minifies could build a release against the SDK. Tink refers to annotation classes it does not ship, and R8 stops on them.Fixed here
Two iOS mesh controller tests were failing. The suite ran nowhere, so nobody saw. The Kotlin twins were repaired in #120.Fixed here
java.time.Instant.parse is called in RelayTimestamps.kt and InternetManager.kt. It needs API 26 and minSdk is 24. The surrounding catch (e: Exception) does not catch the NoClassDefFoundError.Fixed here: parsed by hand, and a Rust guard refuses java.time in the bridge. Not run on an Android 7 device
The release build and the Swift package read the podspec's deployment target with two parsers that disagreed on "15", "13." and a second declaration.Fixed here: one reader. The Swift Package job now runs the release's deployment-target gate
An opted-in mesh wake with no wake service declared, which is every native Android application, held "Mesh Active" until the watchdog fired.Fixed here: the service must resolve, or it stops at once
The AAR carried no license or notices, while the Swift package carries four.Fixed here: packed at the AAR root, and required by the AAR check
The library's manifest does not declare INTERNET, because the module's does not. A native application must declare it.Documented, not changed
162 Swift declarations and every non-internal Kotlin one become public API. Nobody chose that surface.Not decided. Must be before a first release

Verification

All run locally. The three new CI jobs were extracted from the workflow and run as written.

CheckResult
Swift package on an iOS 26.5 simulator, every suite415 tests, 0 failures
Swift consumer check, plain import, call into the librarypass
Android library unit suite, Gradle 8.9, AGP 8.5.2, Kotlin 1.9.24552 tests, 0 failures
Android consumer check, minified release buildpass
AAR with all four ABIs, --require-nativespass
Existing SwiftPM harness403 tests, 0 failures
Existing React Native Android harness552 tests, 0 failures
Rust guards in offline-protocol-uniffi173 pass
Assemble script fixture test, under BSD awk, gawk and mawk120 assertions
AAR check unit tests38 tests

Negative controls, each confirmed red and then restored:

  • A shared Swift source given import React: the assemble script refuses it by name.
  • A shared Kotlin source given a React import: the library does not compile.
  • The wake service renamed in the module's manifest: the manifest task fails.
  • A React-only file renamed without updating the list: both builds refuse.
  • The harness manifest without its flag: the harness does not compile.
  • The library without the new R8 rule: the consumer check fails.

Mutation testing, in scratch copies: 24 mutants of the assemble script and 18 of the AAR check. All killed but one of the first, which is equivalent.

Not covered

  • Nothing ran on a device. BLE does not exist on a simulator, and no relay was involved.
  • The device slice was never linked. CI builds one simulator slice. The Swift sources typecheck for a device.
  • Xcode 27 was not available. Everything ran on Xcode 26.5.
  • No job on a GitHub runner has run yet. This PR is its first run.

Review

Two fresh-context reviews, one per platform. One blocker and sixteen other findings, all addressed in this version. The ones that changed the design:

  • The Swift job would have passed once and failed forever after. It assembled under target/, and the Rust cache restores that directory as an empty tree. It now writes under build/.
  • The Android library was built by a newer Kotlin than its tests. No application on Kotlin 1.9 or 2.0 could have used it. It is now built by the toolchain the module's tests run on, read from the harness, and published by Gradle's own plugin.
  • Versions were read from the first textual match, comments included. They are now read from a line that declares them, exactly once.
  • JNA was published at runtime scope while generated public types extend JNA's. It is now api.