Releases: sohonetlabs/testfs
Release list
v0.1.28
Added
- Examples ▾ picker next to the Source JSON chooser. The host
bundles 16 example tree JSON files but onlyFile ▸ Try an example…
exposed them, which most users never noticed. The new inline menu
is data-driven from a directory listing ofContents/Resources/Examples/,
so any JSON dropped intoresearch/test_json_fs/example/shows up
on the next build with no code change.
Internal
BundledExamples.sortedJSONURLs(in:)helper inTestFSCore(pure
Foundation, unit-testable).ContentView+Helpers.swiftwas a few lines over SwiftLint's
400-line file budget after the additions; split into
ContentView+Helpers.swift(statics, the re-registration actor,
ContentView's non-view helpers) andContentView+UI.swift
(banners, Examples menu, panel pickers, System Settings deep link).- 4 new tests in
BundledExamplesTestscovering the filter and sort.
swift test now reports 106 tests (was 102).
v0.1.27
Fixed
-
"Restart required" banner after install / Sparkle update. macOS 26
caches FSKit XPC connections infskit_agentand doesn't flush the
cache when an extension exits, so the first mount after a Sparkle
update fails withextensionKit.errorDomain error 2until the
system reboots. The host now detects the post-install state (bundle
mtime > last boot time) and shows a banner explaining it, instead
of letting mount(8) fail with the cryptic error. Mount click is
short-circuited with the same guidance. Banner clears automatically
after reboot. -
friendlyMountErrorextensionKit branch now points at reboot as the
primary recovery, with toggle off+on as the fallback.
v0.1.26
Fixed
-
Mount no longer fails after a Sparkle update with
extensionKit.errorDomain error 2/ "File system named testfs not
found". Sparkle replaces the app bundle but doesn't terminate the
live appex; extensionkitd then routes mounts to the pre-update PID
whose cdhash no longer matches the new bundle. The host now reaps
any orphan TestFSExtension on the post-update path so extensionkitd
spawns a fresh instance from the updated appex on the next mount.The orphan reaper uses libproc with exact-path equality
(proc_pidpath+ string compare against the resolved appex binary)
rather thanpkill -f, so an unrelated process that mentions the
appex path — a grep in a dev shell, a log viewer's window title —
can't be matched. -
Restored the
extensionKit.errorDomainbranch infriendlyMountError
that 0.1.25 dropped on the (incorrect) assumption pre-flight catches
every disabled-state symptom. If the orphan reap doesn't reach
(e.g., the process is owned by another user), the user now gets
toggle-off-then-on guidance instead of TCC privacy text.
v0.1.25
Fixed
- Bundled
big_list_of_naughty_strings_fs.jsonexample now mounts.
The fixture had a duplicated top-leveldirectoryentry from a
hand-generatedtree -J -srun, so File ▸ Try an example… on this
one fixture failed withunexpectedTrailingEntry. Patched upstream
intest_json_fs@5615573; submodule pointer bumped here. All 329
naughty filenames are preserved (sha256-verified).
Internal
- Test parse coverage for every shipped example. The 9 archive_torture
fixtures + bad_windows_extended weren't being exercised by any test —
addedtestEveryBundledFixtureParsesthat enumerates every JSON
shipped underFixtures/and asserts it parses to a directory root. - Trim dead state from the 0.1.24 watcher revert: drop unused
mountpointparameter onfriendlyMountError, drop write-only
@AppStorage verifiedMountedVersionbinding (replaced with a
directUserDefaults.setat the mount-success site), inline
FSKitEnabledWatcher.plistDiras a local.
TestFS 0.1.22
What's new in 0.1.22
Completes the first-launch self-heal across all three caching
layers that macOS 26's stricter ExtensionKit can resolve into.
0.1.21 cleared the LaunchServices database and killed orphan
appex processes; affected machines still hit extensionKit error 2 / Cocoa 4099 because fskit_agent (the long-running per-user
FSKit broker) held an in-memory XPC connection cache referencing
sibling helpers that runningboard tore down during the orphan
kill. Every subsequent spawn routed through the stale cache and
failed with "connection to service with pid <N> was invalidated".
Fix
- Reregister cleanup now also kills
fskit_agentto flush its
per-user XPC cache.fskit_agentis a launchd user service
(com.apple.fskit.fskit_agent) — auto-respawns on demand, no
harm done. The kill happens between the orphan appex kill and
the pluginkit toggle, so the toggle re-establishes every cached
connection from a clean state. (#72)
Three layers, three killers
For the record, the cleanup chain is now:
sweepStaleRegistrations— LaunchServices database (#68 / #70).killOrphanExtensionProcesses— live appex from prior version (#70).killFSKitAgent— per-user XPC cache (this release).lsregister -f/pluginkit -e ignore→use— re-adjudicate.
Every layer that could reference the previous bundle's UUIDs is
forced to re-init.
Auto-update
Existing 0.1.21 installs (and any 0.1.15+) will pick this up via
Sparkle within ~24h or immediately via App ▸ Check for Updates….
Tracked follow-ups
TestFS 0.1.21
What's new in 0.1.21
Completes the first-launch self-heal that 0.1.20 started. 0.1.20's
sweep used NSWorkspace.urlsForApplications for enumeration, which
filters out registrations whose underlying file is gone — exactly
the dead-file entries (unmounted DMGs, deleted Trash items) most
likely to confuse macOS 26's stricter ExtensionKit. Affected
machines still hit mount exit 69 / extensionKit error 2 after
upgrading to 0.1.20 because the sweep couldn't see those entries.
Fixes
- Sweep now enumerates via
lsregister -dumpparsing instead
ofurlsForApplications. Catches every registration regardless
of whether the underlying bundle still exists on disk. Batched
lsregister -u <p1> <p2> ...removes them in a single
subprocess. (#70) - Orphan extension processes are killed during the reregister
cycle. Sparkle's bundle replace doesn't tear down running
ExtensionKit instances; the pluginkit toggle alone doesn't reap
them either. The leftover process holds a stale UUID and
extensionkitd refuses to spawn a fresh instance withCocoa 4099 / NSXPCConnectionInvalid.pkill -fagainst the resolved
appex binary path forces a clean replacement on the next mount.
(#70) - Cleanup gated behind the verifiedMountedVersion stamp so
stable installs don't pay the multi-MBlsregister -dump+
pkill cost on every launch. The stamp resets on every release,
so post-upgrade always runs the cleanup. (#70)
Auto-update
Existing 0.1.20 installs (and any earlier 0.1.15+) will pick this
up via Sparkle within ~24h or immediately via App ▸ Check for
Updates….
Tracked follow-ups
TestFS 0.1.20
What's new in 0.1.20
A targeted fix for the most common silent-failure mode left over
after 0.1.19's monotonic-build-number fix: macOS 26's stricter
ExtensionKit hard-fails when LaunchServices accumulates stale
registrations for the host bundle, even when pluginkit lists only
the canonical bundle as +-enabled. Symptom is mount exit 69: mount: Loading resource: extensionKit.errorDomain error 2 / File system named testfs not found, with the underlying cause showing
up in fskitd logs as NSCocoaErrorDomain Code=4099
(NSXPCConnectionInvalid).
Fix
- First-launch sweep of stale LaunchServices registrations:
the host enumeratesNSWorkspace.urlsForApplicationsfor its
own bundle ID at first launch, batches a singlelsregister -u
for every path that isn't the running bundle (resolving symlinks
to handle Gatekeeper translocation and/var ↔ /private/var
variants), then forces an extensionkitd toggle if anything was
removed — the toggle drops cached UUIDs that may still point at
the just-unregistered paths. Converts "user runslsregister -u
from the terminal to fix mount" into "user opens the app, sweep
runs, next mount works." (#68)
Auto-update
Existing 0.1.19 installs will pick this up via Sparkle within
~24h or immediately via App ▸ Check for Updates….
Tracked follow-ups
TestFS 0.1.19
What's new in 0.1.19
A re-release of 0.1.18's contents with the build-number monotonic-
ity bug fixed, so Sparkle actually offers the update.
Fix
.build_numberonmainhad been stuck at 89 since 0.1.14 —
every release.sh run bumped it to 90 in the archive but never
committed the new value back, so consecutive releases shared a
CFBundleVersion. Locally-iterated builds (e.g. the in-the-wild
0.1.15 at build 94) ended up with a higher build number than
any subsequently published release. Sparkle's "newer build wins"
logic then refused every "update" as a downgrade. .build_number
bumped to 100 so this 0.1.19 archive lands at 101, above the
affected installs.
Carried from 0.1.18
- Sparkle pre-install hook gates updates on a clean unmount:
active testfs mounts no longer pin the FSKit appex into a half-
applied install. On any unmount failure the install is aborted
loudly via an alert, leaving the user on the previous version
with a clear message instead of a silently-stale bundle (#65).
Auto-update
Existing 0.1.15+ installs should now correctly detect this update
via Sparkle within ~24h or immediately via App ▸ Check for Updates….
Updates into 0.1.19 use the pre-hook flow — unmount any active
testfs volumes before accepting the prompt this one time.
Tracked follow-up
- release.sh will be hardened to commit
.build_numberalong with
appcast.xmlso the baseline on main stays current after every
release, preventing this regression class.
TestFS 0.1.18
What's new in 0.1.18
A targeted release covering one fix that closes a silent-failure
class affecting Sparkle auto-update.
Reliability
- Sparkle pre-install hook gates updates on a clean unmount:
an active testfs mount keeps the FSKit appex pinned in the kernel,
and without intervention Sparkle's bundle replace fails silently —
the user sees the new version in About box but/Applications/TestFS.app
stays at the old version on disk. The host now runs an unmount
sweep before allowing the install to proceed; on any failure the
install is aborted loudly via an alert, leaving the user on the
previous version with a clear message instead of a half-applied
update (#65).
Auto-update
Existing 0.1.17 installs will pick this up via Sparkle within ~24 h
or immediately via App ▸ Check for Updates…. The hook is in 0.1.18,
so the next update from 0.1.18 onward (and every one after) is
gated by it. Updates into 0.1.18 still use the pre-hook flow —
unmount any active testfs volumes before accepting the prompt.
Out of scope (tracked)
- Manual drag-installs (no in-process hook) can still fail silently
if a mount is active. Workaround: unmount first, then drag.
TestFS 0.1.17
What's new in 0.1.17
A focus release covering 5 issues filed in round-4 audit against
0.1.16, plus the host-side pre-flight that #56 added (which was
deferred from 0.1.16 once #54 reverted the original attempt).
Security
- mount.sh privilege-escalation fix: the admin osascript that
chowns the dev node no longer interpolates$USERand$DEV
unquoted — both are routed through AppleScript'squoted form of
viasystem attribute(and$USERis replaced with a kernel-
trusted value), so a tampered environment can't escape into the
privileged shell command (#57).
Reliability
- runSilently SIGKILL escalation: re-registration helpers now
use the same SIGTERM → wait → SIGKILL → wait shapeShellRunner
uses, sharing a singleShellRunner.terminate(_:exitSem:)helper
so the two timeout paths can't drift again. A wedged
lsregister/pluginkitno longer survives the watchdog and
stack against subsequent mount attempts (#58). - Failure-marker channel: per-attempt token: the marker channel
added in 0.1.16 is now scoped to a per-mount UUID, so a slow-
failing priorloadResourceon a reused/dev/diskNcan't write
a marker that triggers false rollback on the next mount. Same
diskN-reuse hardening class as 0.1.15's #31/#41 (#59). - Pre-flight off the MountManager actor: the host-side
TreeBuilder.parseAndBuildpre-flight (re-added via host-target
membership for the parsers — #56) now runs in a detached task so
large-fixture parsing doesn't pin the actor and block concurrent
unmounts/sweep/registry refresh (#60).
Auto-update
Existing 0.1.16 installs will pick this up via Sparkle within ~24 h
or immediately via App ▸ Check for Updates….