Shedtest mac
Run and debug the shed desktop app's macOS end-to-end tests — the hermetic pytest harness that drives the real app over its IPC socket and captures in-process screenshots, plus the Swift unit tests and the Rust-core parity leg. Use when asked to run the Mac desktop tests, verify a UI/behavior change by driving the app, debug an e2e failure, add a harness test, or check screenshot/IPC ops. The app lives under `desktop/`; its shared Rust core is the sibling `crates/` workspace. The Linux client is the Tauri app — see the shedtest-linux skill.From its SKILL.md
npx -y skills add charliek/shed --skill shedtest-macAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
11.7 KB, as published. Nobody here has run it
macOS desktop app end-to-end + unit tests
The shed desktop app's north star is that the app is drivable and observable by an
agent: every change is verified by launching the real app and driving it over the
IPC socket (and reading in-process screenshots), not by asking a human to click. This
skill is that loop. The app lives under desktop/; run its targets with
make -C desktop <target> (or the root make desktop-<target> passthrough).
The fast path
make -C desktop build # Rust core (xcframework) + swift build — run before a bare swift build/test
make -C desktop test # Swift unit tests (ShedKit) + the Rust FFI canary
make -C desktop e2e-ci # hermetic e2e: bundles the app, TEST_MODE + in-process mock, fresh
make -C desktop e2e-ciis CI parity and the one to trust: it setsSHED_DESKTOP_TEST_MODE=1and points every HTTP client at an in-process mock shed-server (desktop/tools/shedtest/mockserver.py), so no real shed-server is touched.identifyis checked up front to confirm hermeticity.make -C desktop e2edrives a running/auto-launched app for quick local iteration.- The harness lives in
desktop/tools/shedtest/(pytest); it speaks the same JSON IPC protocol asshedctl.
Driving the app by hand (shedctl)
The bundle ships the CLI driver at
desktop/build/ShedDesktop.app/Contents/Resources/bin/shedctl:
make -C desktop run # build + launch the bundle
desktop/build/ShedDesktop.app/Contents/Resources/bin/shedctl ui show-window
desktop/build/ShedDesktop.app/Contents/Resources/bin/shedctl screenshot --surface window --out /tmp/s.png
desktop/build/ShedDesktop.app/Contents/Resources/bin/shedctl sheds list
The screenshot op renders in-process (no screen-recording TCC grant needed), so it works headless in CI.
Conventions that keep the harness reliable
- Condition-waits, never sleeps. Use
wait_until/wait_alive(readiness is gated on the app answering, not on a timer). - Hermetic. The harness launches the app with
SHED_DESKTOP_TEST_MODE=1+SHED_DESKTOP_MOCK_BASE_URLso all HTTP hits the in-process mock. Never point a test at a real server. Fixtures live indesktop/tools/shedtest/fixtures/. - New UI ⇒ new IPC op + harness coverage. When you add UI or behavior, add the IPC op that lets an agent observe/drive it, and a pytest that exercises it. That is the definition of done here, not a manual click-through.
A module that needs a DIFFERENT launch (config / env overrides)
Only one mac app can exist at a time — the control socket + single-instance lock are
at fixed paths under ~/Library/Caches/ShedDesktop/ and are NOT moved by
SHED_DESKTOP_STATE_DIR — so the Tauri trick of spawning a second, self-managed instance
(test_tauri_downhost.py) has no mac analog. A mac module that needs a different config
or launch env owns the app for its duration: quit the session instance, launch its
own, and relaunch the session one (shared mock fixture + fixtures/config.yaml) in the
fixture's finally. test_agent_upgrade.py is the worked example. Modules run
sequentially, so the session fake host-agent can be reused — there is no second
connection to steal.
Launch-time overrides the harness can pass (test mode only, ui.launch(...)):
| override | effect |
|---|---|
unreachable_hosts=(...) | those server names are pointed at a closed port (deterministic ECONNREFUSED) |
credential_hosts=(...) | those server names keep their REAL credential wiring against the mock — the fake host-agent + the config's auth_mode — instead of the tokenless open-mode shortcut |
MockShedServer(require_auth=True) | a private mock instance that 401s an unauthenticated /api request (the auth-shaped refusal a secure shed-server gives) |
The last two together are what make a mint failure observable end to end: an open-mode 200 would swallow it (a mint error only surfaces on an auth-shaped or transport-level outcome), so without the 401 the assertion would be vacuous.
Never hold an IPC client open across tests (the CI-only wedge)
The Swift IPCServer serves each accepted connection from a blocking read on a
cooperative-pool thread, so every idle connection permanently consumes one — and the
pool is only as wide as the machine's CPU count. Measured on a 10-core Mac: with 7
idle connections held, the next connection is accepted by the kernel but never served —
connect() and send() succeed and the reply never comes, surfacing as
client.Timeout: no IPC response within socket timeout. The ceiling is roughly
ncpu − 3 (the accept loop and the host-agent read loop each pin one too), so on the
3-vCPU GitHub runner it is ~0: ONE long-lived client is enough to wedge the app.
Consequences for a fixture author:
- Take the function-scoped
shed/clientfixture (open per test, closed after), like every existing module. A module- or session-scoped fixture that yields a live client is the trap: it is created BEFORE the autouse_reset_policy, whosepolicy.setthen starves — the failure lands on a conftest fixture, pointing away from the module that actually caused it, and it passes locally on a many-core dev Mac. - A fixture that needs IPC during setup should open a client, use it, and close it before yielding.
- The symptom to recognize:
client.Timeouton a shared autouse fixture, CI-only, affecting a contiguous run of tests that then "heals" for later modules. - Reproduce it locally without CI: hold N clients open in a loop and probe with an extra one until it times out.
Also: prefer polling read ops (host.list, ui.state) over hammering sheds.refresh in
a wait_until — a refresh chains behind any in-flight poll, so calling it every 100 ms
queues work rather than hastening it. The app polls every 5 s on its own.
Rust-core parity leg
The shed-server protocol path can run through the shared Rust core in crates/
(SHED_DESKTOP_RUST_CORE, the macOS default). identify.core reports
rust|swift, and wait_alive asserts it, so a silent fallback fails the run rather
than passing falsely:
# the same hermetic suite through the Rust core (the default):
SHED_DESKTOP_RUST_CORE=1 make -C desktop e2e-ci
# force the legacy Swift URLSession path (rollback escape hatch):
make -C desktop e2e-swift # sets SHED_DESKTOP_RUST_CORE=0
If a run reports identify.core=rust but a host silently fell back to Swift,
that's a bug to fix (adapter construction must fail loudly, not try? away).
Real-launch smokes (the paths the hermetic suite gates off)
make -C desktop smoke-real-launch # non-test launch survival (real notification path)
make -C desktop smoke-launch-window # user launch opens the dashboard; reopen reaches it
make -C desktop smoke # drive the app + capture labeled screenshots
Gotchas
- Run
make -C desktop build(ormake -C desktop core) before a bareswift build/swift test— the Swift package links a static xcframework generated from the Rust core incrates/, output todesktop/artifacts/, so that path must exist first.bundle.sh/make -C desktop e2e-cibuild it themselves. - Do not run
swift format -ion the whole tree reflexively — match the existing 4-space style; formatting churn muddies review. - The control socket + lock live under
~/Library/Caches/ShedDesktop/and are NOT moved bySHED_DESKTOP_STATE_DIR, so the harness + a dev session agree on them. - Tauri screenshots on macOS are Screen-Recording-TCC-gated. The mac app's
app.screenshotrenders the content view in-process (no permission needed), but the Tauri client (--target taurion a Mac) has no in-process WebKitGTK capture — it shells out toscreencapture, which exits 1 without a Screen- Recording grant in an agent/headless session. Those Tauri screenshot assertions skip on Darwin by design (seetest_tauri.py); for real visual capture of the Tauri client run theshedtest-linuxrender gate under Xvfb (ui.set_appearancemakes the dark shot deterministic there).
Tauri mac packaging + Sparkle updater (rough edges)
The Tauri client now ships a macOS DMG with an embedded real Sparkle updater
(make -C desktop tauri-dmg-mac; the updater is the tray popover's "Check for Updates…"
row, drivable over IPC). Mac-specific gotchas:
- Every mac Tauri build needs
Sparkle.frameworkstaged first. The overlaytauri.macos.conf.jsonembeds it and thetauri-plugin-sparkle-updatercrate'sbuild.rspanics at build time if it is absent.make -C desktop sparkle-frameworkstages it (scripts/fetch-sparkle.sh, pinned Sparkle 2.8.1, checksum-verified, idempotent); on Darwin it is an automatic prerequisite oftauri-build/tauri-lint/tauri-test/tauri-run. The framework +.sparkle-dist/bin tools are gitignored and--exclude'd from the Docker tar legs. - Unbundled binaries load Sparkle via a debug-only rpath injected by
build.rs, socargo test/make tauri-run/ the raw harness binary can link the staged framework. Release DMGs stay clean — they use the bundled@executable_path/../Frameworkscopy. - Verifying the Sparkle dialog on a dev Mac is TCC-bound (no in-process WebKitGTK
capture, and
screencaptureneeds a Screen-Recording grant). The reliable signal is the log, not a screenshot:log show --predicate 'subsystem == "org.sparkle-project.Sparkle"'(look for the.sessionInProgressline) confirms the check ran and fronted a session. npm --prefix ui exec taurikeeps the shell's cwd (load-bearing — the Tauri CLI walks up from cwd to findsrc-tauri/tauri.conf.json;bundle-tauri-mac.shruns it fromdesktop/tauri). A wrong cwd fails to locate the config.- Sparkle ≥ 2.6's
Downloader.xpchas EMPTY entitlements (its sandbox was removed, Sparkle #2511). Never assertapp-sandboxon it; the re-sign preserves the dist's empty entitlements and must not inject the app's. - The updater is drivable over IPC:
updater.status→{os, enabled, reason, instantiated}andupdater.check. Under the harness the plugin is never registered (Swift parity), so status isenabled=false, instantiated=false,reason="test_mode"on mac ("linux_apt"on the Linux render gate);updater.checkreturns the deterministicupdater_disabled:<reason>error and never crashes.test_tauri.pypins both. - The mac dev config dir moved to
~/Library/Application Support/ai.stridelabs.ShedDesktop(identity alignment for the Swift→Tauri hop). The harness is unaffected — it runs under a throwaway HOME — but a hand-run dev bundle reads/writes there now. hdiutil detachcan report a different disk number thanattachprinted — verify the actual device withhdiutil inforather than trusting the attach output when scripting DMG mount/unmount.cp -Rpreserves read-only directory bits — after copying a signed/notarized.apparound,chmod -R u+wit beforerm -rf, or the delete fails on the read-only dirs.
When you hit a NEW rough edge
Update this skill whenever you hit a new rough edge it doesn't cover.