Releases: sohonetlabs/testfs
Release list
TestFS 0.1.16
What's new in 0.1.16
Closes the audit's #46: deterministic mount failures no longer wait the full 15-second confirmMountedOrRollback timeout before surfacing.
Fast-fail rollback (#46)
Previously, mounts that failed inside the FSKit extension's loadResource (malformed tree JSON, BuildError.duplicateName, BuildError.totalSizeOverflow, etc.) still took the full 15s before the host rolled back — mount(8) returns success synchronously and only the extension's async loadResource saw the error.
Cross-process failure marker channel:
- Extension writes
~/Library/Containers/com.sohonet.testfsmount.appex/Data/Library/Application Support/TestFS/failed-<bsd>.json(aLoadFailureMarkerCodable with{"error": "<localized description>"}) before callingreplyHandler(nil, error). - Host's
confirmMountedOrRollbackpolls statfs (success) AND the marker (failure) on each 100 ms tick. Whichever fires first wins. - Stage-time delete in
prepareMountensures a recycled/dev/diskNcan't false-trigger off a stale marker from a prior failed mount. - Marker is removed in
detachas part of normal rollback.
MountConfirmResult enum (.mounted / .failed(reason: String?)) carries the underlying error up to the UI, replacing the previous generic "the kernel accepted the mount but the FSKit extension didn't load the volume" with the actual decode/build error.
Auto-update
Existing 0.1.15 installs will pick this up via Sparkle within ~24 h or immediately via App ▸ Check for Updates….
Known follow-ups
- #55 — re-add host-side pre-flight:
TreeBuilder.parseAndBuildfromMountManager.swiftrequiresTreeBuilder/JSONTree/TreeIndexto be members of the host target (currently extension-only). Tracked separately, no user-facing impact (the marker channel covers the same failure modes via the extension round-trip).
TestFS 0.1.15
What's new in 0.1.15
A focus release covering 18 issues filed across three rounds of audits against the 0.1.14 codebase.
Reliability
- Re-registration race: the FSKit toggle no longer races the user's first Mount click after an update (#21, #32).
- Mount confirmation uses
ContinuousClockwith a 15s budget — sleep/wake mid-mount no longer skews the deadline (#20). - One wedged
hdiutil/mountno longer hangs the wholeMountManageractor — bounded subprocess timeout with SIGTERM→SIGKILL escalation (#42).
Correctness
- Volume capability flipped to
.sensitiveso the runtime contract matches the personality plist andTreeIndexbyte-keyed lookup. Closes the kernel-name-cache aliasing risk for case-distinct siblings (#17). TreeBuilderrejects file sizes that would overflow the volume's block-count math instead of trapping at firststatfs(#18).parseSizereturnsnilonIntmultiply overflow instead of crashing the host when an oversized value lands in the block size field (#19).- JSON loader rejects malformed top-level entries instead of silently truncating (#22).
- Invalid
mtimein the sidecar is rejected at load time, not silently rewritten to1970-01-01(#24). - Directory
st_nlinkis now2 + immediate-subdirectory-count, matching the Unix convention (#25). MountRegistrypreserves app metadata across canonical-path variants (/tmpvs/private/tmp) and across/dev/diskNreuse (#31, #41).lookupItem/enumerateDirectoryreturnENOTDIR(notENOENT) when the target is a file (#43).
Tooling
scripts/install.shalways builds the current checkout intobuild/derived(was: could install another clone's app on a multi-clone machine) (#33).scripts/mount.shallocates a per-UUID image instead of the singletondummy.img, so concurrent CLI mounts no longer stomp each other's sidecar state (#45).scripts/mount.shencodes the sidecar JSON viapython3, surviving filenames containing",\, or newlines (#34).scripts/unmount.shparseshdiutil info -plistinstead of the brittle text format, mirroring the Swift sweep (#44).scripts/update_appcast.rbis idempotent — re-running for the same version replaces in place rather than duplicating items (#35).- LaunchServices now gets pruned of stale
TestFS.appregistrations during install/release soextensionkitdcan't resolve a phantom UUID.
Known issue (deferred to 0.1.16)
- Mounts that fail at
loadResource(malformed sidecar, missing config) still take the full 15s before the error surfaces. The fix needs a cross-process failure-marker channel whose lifecycle deserves more design than this release has time for. Tracked in #46.
Auto-update
Existing 0.1.14 installs will pick this up via Sparkle within ~24 h or immediately via App ▸ Check for Updates….
TestFS 0.1.14
What's new
Verify FSKit volume came up before recording the mount (#15)
mount(8) returning 0 only confirms the kernel queued the mount; the FSKit extension's loadResource runs asynchronously after that and could still fail (JSON parse error, missing config, the case-fold collision in 0.1.12 and earlier). The host app was recording the mount as successful and then the user would see the volume in the Live mounts list but Finder wouldn't show any files.
After mount(8), poll statfs(2) for f_fstypename == "testfs" with a 3-second timeout. On failure, unmount + detach so we don't leave an orphaned dev node. Status message now points at Show log… for the extension's actual error.
statfs over notification-watching: kernel VFS state is the authoritative source — log delivery is asynchronous and notifications routed through diskarbitrationd are opaque.
Auto-update
Existing 0.1.13 installs will pick this up via Sparkle within ~24 h or immediately via App ▸ Check for Updates….
TestFS 0.1.13
What's new
Match Python upstream: case-sensitive lookup, drop fold (#14)
The Swift port had been case-folding via TreeIndex.fold() → String.lowercased(), deviating from the Python jsonfs.py upstream which is fully case-sensitive. Two compounding bugs:
- Case-fold collapsed
Foo.txtandfoo.txtas siblings. String.lowercased()is canonical-equivalence-aware, so evenunicode_normalization=nonecouldn't keep NFCé(U+00E9) and NFDé(U+0065+U+0301) distinct.archive_torture_evil_filenames.jsonwas unmountable as a result.
Drop the fold entirely. Switch the lookup map from [String: …] to [Data: …] keyed by raw UTF-8 bytes (Swift String Dictionary keys are also canonical-equivalence-hashed, so the byte-keyed form is required for upstream parity). Flip FSPersonalities.TestFSPersonality.FSfileObjectsAreCaseSensitive to true to tell the kernel the volume is now case-sensitive.
Renamed BuildError.caseFoldCollision → .duplicateName since the only collisions left are literal duplicates after normalization.
89/89 SPM tests pass including 4 new tests in Tests/TestFSCoreTests/CaseSensitivityTests.swift that pin the contract.
Auto-update
Existing 0.1.12 installs will pick this up via Sparkle within ~24 h or immediately via App ▸ Check for Updates….
TestFS 0.1.12
What's new
Mount post-Sparkle-update — actually working this time
0.1.11 still failed live on the test machine. Diagnosis: when pluginkit is already at +, calling pluginkit -e use again is a no-op. There's no state transition, so extensionkitd doesn't drop its cached UUID and re-resolve against the post-update bundle.
The user's manual toggle off then on fix works because the transition is what kicks extensionkitd. Reproduced programmatically:
pluginkit -e ignore -i com.sohonet.testfsmount.appex
pluginkit -e use -i com.sohonet.testfsmount.appex
AppEnvironment.reregisterExtensionIfNeeded now does the four-step sequence (lsregister + pluginkit -a + pluginkit -e ignore + pluginkit -e use) on every version change. Same toggle cycle in scripts/install.sh. Confirmed live on the M4 Max test machine where 0.1.11 was still failing.
TestFS 0.1.11
What's new
Mount actually works after auto-update now
The 0.1.9 / 0.1.10 attempts at this fix were under-medicating. Diagnosed live: pluginkit -a registers the extension but leaves it in the default adjudication state (whitespace prefix in pluginkit -m -A). mount refuses non-+ extensions, hence the recurring extensionKit.errorDomain error 2 / File system named testfs not found failures.
pluginkit -e use is the programmatic equivalent of the System Settings toggle — flips + directly, runs without admin. 0.1.11 calls it as the third step of the auto-re-register sequence (after lsregister -f and pluginkit -a). Confirmed working on a fresh M4 Max / macOS 26.4.1 install where every prior version failed.
scripts/install.sh now also runs pluginkit -e use so manual install converges on the same end state as the Sparkle update path.
Internal hygiene
runSilently()now returns Bool.reregisterExtensionIfNeededonly stampslastRegisteredVersionin UserDefaults if all three commands succeeded — a transient failure now retries on the next launch instead of locking us out until the next version bump.
TestFS 0.1.10
What's new
Bundle-version hygiene
The embedded TestFSExtension.appex was stuck at CFBundleVersion = 1 while the host kept incrementing — Apple requires the match (warning at build, hard ITMS-90473 error on App Store), and lsd's bookkeeping is more reliable when the two are synced. The 'Bump build number' Xcode phase now patches both plists with the same VERSION + build, then re-signs the appex in place preserving its identifier / entitlements / flags.
This complements 0.1.9's auto-re-register fix by removing one more variable from the extensionkitd adjudication path.
Auto-update
Existing 0.1.9 installs will pick this up via Sparkle within ~24 h or immediately via App ▸ Check for Updates….
TestFS 0.1.9
What's new
Auto-fix the post-update mount failures
The headline fix. After every Sparkle update, extensionkitd was holding a stale UUID for the just-replaced FSKit extension — System Settings would show the toggle still 'on' but mount would fail with com.apple.extensionKit.errorDomain error 2 / File system named testfs not found until the user toggled the switch off and back on.
0.1.9 detects a version change on launch and runs lsregister -f + pluginkit -a automatically (the same commands scripts/install.sh runs by hand). extensionkitd re-resolves the extension before you click Mount, breaking the staleness window.
This is the same class of issue macFUSE 5.2.0 worked around at the FSKit/PluginKit layer.
Fallback for users on older macOS / when the auto-fix fails
The targeted in-app status text from 0.1.8 still fires if extensionkitd somehow stays stale despite the auto-re-register — pointing you at the manual toggle-off-then-on as a backup.
Auto-update
Existing 0.1.8 installs will pick this up via Sparkle within ~24 h or immediately via App ▸ Check for Updates….
TestFS 0.1.8
What's new
Mount
- Better diagnosis when
mountfails with ExtensionKit error 2. macOS sometimes refuses to invoke the FSKit extension after an app update — the System Settings toggle still reads "on" but the adjudication is stale. The status line now identifies that signature specifically and tells you to toggle TestFS off and back on under General → Login Items & Extensions → File System Extensions. (Captured from a real failing mount thanks to 0.1.7's diagnostic logging.) - Fixes mount of
archive_torture_format_sentinels.jsonand any other tree that ships its own.metadata_never_indexfile. The mount used to fail with a case-fold collision because we re-added our own copy on top; now we dedupe against the tree's contents.
Visual
- Release builds: ladybug overlay renders solid black as the brand mark. The previous attempt at multicolor wasn't engaging via NSImage's symbol path. Debug builds still get a solid red ladybug, distinct at a glance.
Internal
Package.swiftexcludesbuild/soswift testdoesn't trip over Sparkle's localized resources staged there byscripts/release.sh. 85 SPM tests now run cleanly again, including a new regression test for the cache-files dedup.
TestFS 0.1.7
What's new
Mount error reporting
- The status line now shows the actual
mount(8)output verbatim, then a list of common causes — no more confidently mis-classifying everyOperation not permittedas a TCC denial. The previous wording sent users hunting for privacy fences when the real cause was often something else (stale extension registration, fskitd state). - Drop the misleading "try under your home folder root" suggestion — macOS 26 appears to extend privacy fences into more of $HOME than just the named subdirs.
/tmp/<name>is now the only path the message confidently calls safe. - Full
mount(8)stdout / stderr / exit code is now logged to OSLog from MountManager so the in-app Show log… window captures it for post-hoc diagnosis. Until now, every failure dropped the raw bytes and only kept Foundation'slocalizedDescription.