Fix false-success, a crash, and a dead storage watch found in review - #74
Merged
Conversation
Adversarial review of #60-#73 (six parallel reviewers, findings then reproduced by hand before fixing). Four defects that made the app either crash or state something untrue: login_items.sh reported removal SUCCESS when the check itself failed. login_item_exists returned the same status for "not a login item" and "the System Events query failed", so a failed read at execute time emitted already_gone/ok with exit 0. Reproduced end to end: preview issues a token normally, System Events then fails, execute reports "ok" -- while the login item is still installed. The app maps that to success and announces removal, so a security tool affirmatively told the owner a persistence mechanism was gone while it was still there. Now tri-state: only a successful read may assert absence; an unreadable list is blocked, and a failed post-delete recheck is failed, never ok. SpaceGoalView hard-crashed the 목표 tab. Slider(in: 1...max(x, 1)) collapses to 1...1 whenever the cleanable total is above zero but at or below 1GB -- one small npm cache -- and SwiftUI fatals with "max stride must be positive". Reproduced as a real crash in a hosted view, not inferred. The range is now provably wider than its lower bound, and below 1GB the slider is replaced by the achievable total. The storage watch could never be turned on, from two independent causes, both predating this range but only diagnosable together: PCH_STORAGE_WATCH_APP_BUNDLE was never in allowedEnvironmentOverrides, so sanitizedEnvironment threw and the spawn was refused; and once that was fixed, StorageWatchService's expectedArguments still omitted the entry schedule.sh actually writes, so the installed plist failed the exact-array comparison and every install was judged stale. The Swift unit test could not catch that because its fixture was built from the same wrong list the implementation used. Verified on a live machine: the toggle now turns on and the app's reported state matches launchd for the first time. A cross-language guard now compares schedule.sh's and Swift's argument lists so they cannot drift apart again. network_watch.sh reported a false all-clear when the closing lsof sample failed: `|| true` cannot distinguish failure from zero matches, so an unread sample looked like a quiet window. The closing LISTEN set is the liveness probe (every real Mac has listening sockets); an empty one now reports an error instead of "0 new connections". Every new test was proven against the pre-fix code first.
5 tasks
heznpc
added a commit
that referenced
this pull request
Aug 13, 2026
* Fix accuracy defects from the review follow-up batch The lower-severity half of the same adversarial review as #74. None of these crashes or claims a false success, but each reports something that is not quite true. Login item names are now read one per line instead of relying on osascript's ", " list serialization. That default is ambiguous: an item named "Backup, Inc." serialized exactly like two items "Backup" and "Inc.", so it could never be matched and was permanently unremovable through this tool; and a surviving "Foo, Bar" kept producing a "Foo" fragment, so removing a genuinely separate "Foo" re-matched on the post-delete recheck and reported failed after a successful removal. lsof escapes every byte it deems unprintable, not just the space that motivated the earlier fix -- a tab arrives as \x09 and a non-ASCII name as a run of \xNN. Both the awk and the JXA decoders now handle whole runs, so a multi-byte UTF-8 name reassembles instead of leaking literal escapes into the UI and into anything matching on the name; control bytes become a space because they would break the TSV. Pinned Homebrew packages were dropped outright by the devtoolUpdates regex, which anchored the latest version to end-of-line while brew appends " [pinned at X]". The row vanished while the collector's line count still included it, so the displayed total disagreed with the list and a pinned package's update was invisible. Rows now survive and say they are pinned. SpaceGoalSelection's documented "same set regardless of scan order" promise did not hold when size and label both tie -- label falls back to kind, so two same-size rows of one kind tied completely and the result depended on emit order. Path breaks the remaining tie. The goal comparison also gained a tolerance: 2.4 + 0.3 + 0.3 is exactly 3.0 in decimal but accumulates to 2.9999999999999996, so the greedy walked past the set that met the goal, took an extra item, and then called the result short. A non-finite sizeGB is clamped at the parse boundary, where it used to poison every sum and trip a range precondition. Login item removal and the observation window now respect isBusy in both directions: removal could previously start a second concurrent ScanPipeline over the same output files during a scan (untracked by scanTask, so 검사 취소 could not stop it), and a scan started under an observation landed in that observation's own results. Mothball used scan() rather than scanReport(), discarding inspection failures -- MothballCore's own API comment warns that this makes "found but could not inspect" indistinguishable from "nothing found". The page now says which it is. * Skip the JXA-driven pinned-line test on non-macOS CI Same class of miss as the network_watch skipif: the probe runs the real regex through /usr/bin/osascript, which Linux runners lack. --------- Co-authored-by: Heznpc <[email protected]>
4 tasks
heznpc
added a commit
that referenced
this pull request
Aug 13, 2026
Six items the #74/#75 reviewers classified low/informational -- nothing here is reachable in a shipped build today; each is a seam a future change could silently reopen. privacy.sh's PCH_TCC_DB_PATH test seam now opens only under PCH_TEST_MODE=1, matching PCH_TEST_BREW_BIN and PCH_TEST_OSASCRIPT_BIN. It was the one injection point in the new modules without the gate, and ungated it doubles as an arbitrary-sqlite-read redirect for any future caller that forwards the variable. login_items.sh now refuses a manifest whose protocol version differs from its own -- fields this version reads could mean something else under another one. cleanup.sh cross-checks its whole manifest; this is the minimum equivalent. The bundled-runtime completeness guard now also covers non-.sh runtime dependencies (rules/*.json, data/*.json, report_i18n, *.jxa.js, and shipped .py), with an explicit build/reference-only exclusion list mirroring the .sh guard's. Proven the same way: removing rules/network.json from RUNTIME_FILES trips it. RuntimeWorkspace's bundled branch now demands the development opt-in whenever the injected resourceURL is not the running app's own signed bundle. That combination skips the code-signature payload comparison and is constructible only through parameter injection; without the gate, a future refactor decoupling the parameters would run unsigned code with no test noticing. The revalidation tests declare the opt-in they were implicitly relying on. Raw pinned-file values now refuse on key collision instead of silently clobbering whichever module payload merged first -- the exact asymmetry behind the approval_token/approval_token_module near-miss fixed in #67. Observation timeout slack grows from 20s to 60s past the window (the snapshots run exactly when the machine is busiest), and a timeout now says it timed out instead of "status 124". Co-authored-by: Heznpc <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adversarial review of this session's work (#60–#73) using six parallel reviewers with different scopes. Every finding below was reproduced by hand before being fixed — none is taken on the reviewers' word.
1.
login_items.shreported removal SUCCESS when the verification itself failedlogin_item_existsreturned the same exit status for "this is not a login item" and "the System Events query failed", so a failed read was silently read as absence. Two consumers turned that into a success report:already_gone, exit 0ok, exit 0 (the delete's own exit code is discarded)Reproduced end to end with a stateful osascript stand-in: preview issues a token normally, System Events then fails, execute reports
status ok/ exit 0 — while the login item is still installed.LoginItemServicemaps both to success and the app announces removal. A security tool affirmatively telling the owner a persistence mechanism is gone while it is still there is the worst version of this bug class.login_item_existsis now tri-state (present / confirmed absent / could not determine). Only a successful read may assert absence; an unreadable list isblocked, and a failed post-delete recheck isfailed, neverok.2.
SpaceGoalViewhard-crashed the 목표 tabSlider(value:in: 1...max(achievableGB, 1), step: 1)collapses to1...1whenever the cleanable total is above zero but at or below 1GB — a single small npm cache is enough. SwiftUI divides the range bystepand callsfatalError:Reproduced as a genuine crash (signal 5) in a hosted view, not inferred from reading. The
eligibleCandidates.isEmptyguard doesn't cover it, because candidates do exist. The bound is now provably wider than its lower bound, and below 1GB the slider is replaced by the achievable total (a whole-GB goal picker is meaningless there anyway). Five rendering regression tests cover 0 / sub-1GB / exactly-1GB / multi-GB / no-candidates; they crash against the old view.3. The storage watch could never be turned on — two independent causes
Both predate this review range (introduced with the watch-notification work), but neither was diagnosable without the other:
PCH_STORAGE_WATCH_APP_BUNDLEwas passed bysetStorageWatchEnabledbut never added toallowedEnvironmentOverrides.sanitizedEnvironmentthrows on any non-allowlisted key, so the spawn was refused beforeschedule.shever ran.schedule.shruns and installs a real plist — butStorageWatchService.runtimeState'sexpectedArgumentsstill omitted thePCH_STORAGE_WATCH_APP_BUNDLE=entry thatschedule.shwrites unconditionally. The exact-array comparison therefore always failed, every fresh install was judged.stale, and the UI reported failure and showed the watch as off while launchd had actually loaded the job.The Swift unit test could not catch (2) because its fixture built the plist from the same wrong list the implementation used — the test encoded the bug.
Verified on a live machine: the toggle now turns on, and the app's reported state matches
launchctlfor the first time. (The LaunchAgent installed during verification was removed afterward.) A new cross-language contract test comparesschedule.sh's and Swift's argument lists so they cannot drift apart again — it fails against the pre-fix Swift.4.
network_watch.shreported a false all-clear on an unread closing samplelsofexits non-zero with no output both on failure and on zero matches, and|| trueerases the difference — so a closing sample that was never read emittednewEstablished 0/newListen 0, indistinguishable from a genuinely quiet window. The closing LISTEN set is the usable liveness probe (every real Mac has listening sockets); an empty one now reports an error instead of a count. Existing tests were passing unrealistically empty listen samples and are corrected to a real baseline.Test plan
pytest tests/— 366 passedswift test— 162 passedrelease_smoke.py --check-only— okNot included
Lower-severity findings from the same review (pinned Homebrew packages dropped by the
brew outdatedregex; comma-in-name login items; non-\x20lsof escapes; goal-selection tie-break determinism and float-epsilon; missingisBusyguards) are real but none produces a crash or a false success. They're deliberately left for a follow-up rather than mixed into this one.🤖 Generated with Claude Code