docs/v10-release-test-plan.md
Branch: shared-network (both repos: FlyingCarpet, FlyingCarpetApple).
Goal: validate everything v10 introduces before release, efficiently, by gating on
builds/KATs first and exploiting the symmetry of the wire protocol.
Noise_NNpsk0_25519_ChaChaPoly_SHA256, PBKDF2
PSK, plaintext version/mode preamble bound into the Noise prologue. Replaces the old
inner AES-GCM. Now encrypts all metadata (filenames, sizes, count), not just contents.docs/send-folder-behavior.md.Five platforms: Windows (10+), Linux (AppImage + .deb), macOS, iOS, Android (API 29+). Apple devices cannot host a hotspot, so Apple↔Apple must use shared network. Ideally have two Android and/or two desktops available for same-OS runs.
Cheapest, and catches the biggest current risk: the Apple lifecycle changes have never been compiled.
./gradlew assembleDebug (and lintDebug — only the pre-existing
MainActivity:94 MissingPermission finding is expected)cargo build for Windows and Linux; cargo clippy clean — "clean" means
no errors. cargo clippy --workspace --all-targets on Windows emits 23 style lints in
core and 4 in the app (is_none() over == None, Ok(…)?, expect with a format
arg, arg counts); all pre-existing, none in v10 code paths.cargo tauri build)NoiseUnitTest, DiscoveryUnitTestcargo test (incl. official_noise_test_vector,
wrong_password_fails_handshake, tampering_is_detected, round_trip_small_and_large,
and the utils::selection_tests folder-naming cases)MAJOR_VERSION: u64 = 10 (core/src/lib.rs:83), Kotlin
MAJOR_VERSION: Long = 10 (MainViewModel.kt:56), Swift VERSION: UInt8 = 10
(shared/Transfer.swift:17). The Swift type is narrower but goes out as
Data([0,0,0,0,0,0,0,VERSION]), so all three write the same 8 big-endian bytes.The wire protocol (discovery → preamble → Noise → files) is symmetric, so a directed cycle covers each platform as both sender and receiver without testing every pair. W=Windows, L=Linux, M=macOS, I=iOS, A=Android.
Apple always guests; the peer hosts. Confirm the 6-digit pairing code and the transfer.
If any single hop fails, expand only that pair to localize it. Passing the cycle + the hotspot pairs means every platform's discovery, Noise, BLE, and hotspot code has run in both roles.
NoiseUnitTest 10, DiscoveryUnitTest 4) re-run; Apple's suite was run on
a Mac in Tier 0 and its sources are unchanged since. All nine shared vectors — PSK,
discovery key, app handshake msg1/msg2/record, prologue + its msg1/msg2/record — are
byte-identical in core/src/noise.rs, NoiseUnitTest.kt, and
macOS/FlyingCarpetTests/FlyingCarpetTests.swift, as is the discovery announcement
vector between Rust and Kotlin.
- Swift had the discovery key KAT but no discovery announcement vector — the
93-byte layout + HMAC was pinned only between Rust and Kotlin. Closed 2026-07-25:
DiscoveryTests in macOS/FlyingCarpetTests/FlyingCarpetTests.swift now asserts the
same vector on serialize, on deserialize (field by field, plus HMAC verify), and on a
flipped bit in the signed prefix. Written on Windows and never compiled — it needs
one run on a Mac before Tier 0's Apple row can be re-checked.
- The iOS test target still holds only the Xcode template tests; the Noise and
discovery KATs live in the macOS target. Both projects compile the same
shared/Noise.swift and shared/Discovery.swift, so iOS's implementations are
covered as long as the macOS suite is the one that gets run.prologue_mismatch_fails_handshake (Rust),
prologueMismatchFailsHandshake (Kotlin), testPrologueMismatchFails (Swift). Each
flips one bit of the responder's transcript and asserts the handshake fails even
though the passwords match. tampered_handshake_is_detected covers a corrupted
handshake message on all three as well.TransferStream::Encrypted handle — file count (lib.rs), then filename length,
filename bytes, size, per-chunk lengths and hashes (sending.rs:87-117). The only
plaintext writes are the version/mode preamble, which the prologue binds, and the
TransferStream::Plain fallback used solely to report a version/mode mismatch.Each row has a specific repro that previously failed — test the repro, not just "works."
Pairing result: Failed and keeps failing on subsequent attempts. Capture
bluetoothctl paired-devices and btmon on the Linux side when testing this.bluetoothctl paired-devices on Linux still lists the Windows box after leg 1.DeviceAdded, which never
fires twice for a bonded peer. Expect Found peer … by re-reading known devices
in the Linux stdout.
- [ ] Then a third leg in each case, to confirm repeat transfers keep workingTRANSPORT_AUTO → TRANSPORT_LE, fixed 2026-07-25):
Android ↔ macOS and Android ↔ Linux/Windows, from fully unpaired, so the post-bond
connectGatt runs against a dual-mode peer right after cross-transport key derivation.
This was the direct analogue of the Windows↔Linux br-connection-canceled bug and had
never been exercised. Failure looks like a connect that succeeds with no Flying Carpet
service.onServiceChanged now re-discovers, gated on
exchangeComplete, and every onServicesDiscovered exit reports and calls
bluetoothFailed()). Expected now: leg 2 logs "Services changed" followed by a
successful re-discovery and a normal transfer. A "Did not find the Flying Carpet
service" abort or any hang after "Discovered services" is a regression in that fix,
not the previously predicted stale-cache hang. See docs/ble-bond-asymmetries.md.remove_device on characteristic-discovery failure was kept; confirm a genuinely bad
bond still recovers via the "retrying with a fresh pairing" path.Bundle, so this also covers the
TransactionTooLargeException that a long transfer's log previously risked on rotation.
Rotate a second time after the transfer completes to confirm the finished log persists.flyingCarpet_* Wi-Fi config; force-
quit mid-transfer then relaunch → stale config removed on startupflyingCarpet_* NetworkManager
connection removed on startup (#51)Every platform now sends a chosen folder so the receiver recreates that folder inside the
destination they picked, with the contents inside. Before this change only macOS did
that; Windows, Linux, Android, and iOS dumped the contents loose into the destination, and
Android→desktop failed outright for any folder with sub-folders. Rationale, the old
per-platform behavior, and the fixes: docs/send-folder-behavior.md.
Use one test folder throughout, so results are comparable. It must exercise every case that used to break:
TestFolder/
top.txt <- file directly inside the selection
Nested/inner.txt <- one level down
Nested/Deeper/deep.txt <- two levels down
OnlyDirs/x/1.txt <- selection level with NO loose files (used to flatten or fail)
OnlyDirs/y/2.txt <- sibling of the above (used to abort: "Strip prefix error")
Pass = destination contains TestFolder/ with all five files at the paths above, and
nothing loose in the destination root.
TestFolder (Send Folder checkbox) → receiver gets TestFolder/…TestFolder by drag-and-drop onto the window → same resultTestFolder (checkbox and drag-and-drop)TestFolder (Send Folder checkbox) → to a Windows or Linux receiver;
this is the combination that used to fail with "Received invalid filename path"TestFolder ("Send Folder" in the "Send from:" prompt)TestFolder (choose the folder in the picker) → confirm no regression;
this is the one platform whose behavior did not changeTestFolder from at least one other platform, and the
folder is recreated rather than flattenedPost-fix state. ✅ = correct; ⚠️ = fragile/by-design; see caveats.
| Platform | Hotspot torn down | Central: scan stopped | Central: connection closed | Peripheral: advertising stopped | Peripheral: service removed |
|---|---|---|---|---|---|
| Android | ✅ fixed | ✅ | ✅ fixed | ✅ | ✅ fixed |
| Windows | ✅ | ✅ | ⚠️ persists (by design) | ✅ fixed (explicit StopAdvertising) | ✅ (registration released on drop) |
| Linux | ✅ | ✅ (RAII) | ✅ (RAII + remove_device) | ✅ (explicit drop) | ✅ (explicit drop) |
| iOS | ✅ | ✅ | ✅ fixed | ✅ | ✅ fixed |
| macOS | ✅ | ✅ | ✅ fixed | ✅ | ✅ fixed |
Verification status of the fixes:
StopAdvertising() +
RemoveAdvertisementStatusChanged); the one remaining ⚠️ is the device
connection/pairing persisting after a transfer, which is by design (Windows has
trouble re-enumerating already-paired devices, so "unpair after every transfer" is
intentionally disabled) and is pre-existing, not a v10 regression.
Hardware-tested 2026-07-24: Windows→iPhone hotspot (fresh pairing) passed, but the
reversed second leg (iPhone→Windows, reused bond) failed GATT service enumeration with
0x8000FFFF; a manual rerun with fresh pairing succeeded. The Windows central now
recovers from this automatically: on enumeration failure it retries enumeration (up to
3×, ~1 s apart), and only if the bond was reused and all retries fail does it unpair,
rescan, and re-pair (new PIN confirmation) within the same transfer instead of
aborting. Retest 2026-07-24: passed — reversed second leg reused the bond and
enumerated services on attempt 1 (leg-1 BLE link was still up, so no reconnect was
needed); no recovery rung fired, so the ladder itself remains field-unexercised. The
original failure is intermittent — if it recurs, the UI log's attempt/timing
diagnostics will show which rung fixed it. Root-cause investigation, sources, and the
recovery-ladder design: docs/windows-ble-gatt-0x8000ffff.md.Release read on the lifecycle front: the code gaps found in the audit are closed on Android and Apple, so on paper every platform is ✅ except Windows' pre-existing ⚠️. But "buttoned up" requires the verification: Tier 0 build of Apple + Tier 4 regressions on Android and Apple must pass. Until then the fixes are unverified. Windows' remaining items are not new in v10 and should not block release.
core/Cargo.toml, src-tauri/Cargo.toml, and
tauri.conf.json at 10.0.0; Android versionName "10.0.0" (versionCode 22); iOS and
macOS app targets MARKETING_VERSION = 10.0.0 (the 1.0 entries in both pbxprojs belong
to the test bundles, which don't ship). README covers Shared Network + Noise. Left
unchecked for the changelog only — the repo has no CHANGELOG.md, so if release notes
live on the GitHub Releases page, that's the remaining item.