fix: report keychain, toast and IPC failures honestly; pin ruff to 0.16.x - #29
Merged
Merged
Conversation
…16.x Three silent failures found by reading ruff 0.16's expanded default rule set site by site, plus the config change that made those rules visible. Real defects fixed: * `keychain.clear_ingest_token` promised "removed (or already absent)" and its own inline comment said an absent entry was a no-op success -- but the code returned False. keyring raises `PasswordDeleteError` when there is nothing to delete, which the blanket `except Exception` caught. Signing out on a machine that had never signed in reported a failed sign-out. Absent now returns True; a genuine backend error still returns False and is logged (was silent). * `NotificationManager._show_windows` returned True unconditionally after shelling out to PowerShell, ignoring the exit status. On a machine where the WinRT toast API is unavailable, every notification -- including "N automations need attention" -- was reported to the caller as delivered. It now checks returncode and logs stderr, matching the macOS and Linux paths which have always done so. * `IPCServer._accept_loop` caught bare `Exception` and `break`, so the server could stop accepting connections with no message anywhere. Narrowed to `OSError` (all `accept()` can raise) and it now logs when the loop ends for any reason other than `stop()` closing the socket. Also: a corrupt icon PNG and a broken cairosvg install were both indistinguish- able from "file missing" -- now logged; a pystray balloon failure silently fell through to the toast path -- now logged; and `WindowsHandler._find_executable` had a try/except/pass around `os.path.exists`, which never raises, so the handler was unreachable and only hid a failed `import os`. Lint configuration made explicit and bounded: * `ruff>=0.1.0` -> `ruff>=0.16,<0.17`. ruff 0.16.0 grew its default rule set from 59 rules to 413, this project selects no rules, and CI runs a blocking `ruff check src tests scripts`. Only the 0.14.x pin in `uv.lock` was keeping `main` green; the next dependabot bump would have turned it red with 216 findings and no code change. `uv.lock` now resolves 0.16.0, so CI genuinely exercises the new default set. * The FULL default set is kept -- no `select`. The residual after safe autofix was 91 findings in a 3.9k-line tree, small enough to read and judge one by one, so the coverage is worth more than a narrow `["E","F","I","W"]`. * `ignore = ["BLE001"]`, documented inline. The tray is almost nothing but boundaries to optional, platform-owned subsystems (pystray, pynput, Pillow, keyring, desktop-notifier, tkinter, zenity/kdialog, osascript, PowerShell, winreg, ctypes.windll) that share no exception hierarchy; every handler logs and degrades on purpose. Where a blind catch was hiding a defect rather than guarding a boundary, the code was fixed instead. The rest is mechanical and behaviour-preserving: PEP 604/585 annotations, import sorting, explicit `check=False` on `subprocess.run` calls whose returncode is already inspected, `ClassVar` on mutable class attributes, tz-aware datetimes that render the same local time as before, and `chmod +x` on the two scripts that carry a shebang. Regression tests added for both user-visible fixes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
abrichr
added a commit
that referenced
this pull request
Jul 28, 2026
Every defect here is one failure class: a failure rendered as a successful
empty result. `return []` when we could not look. A `count` of 0 that means
"the body was unreadable". A `False` that means "we never asked the user". A
bool that means "we did not check". Each fix makes the distinction
representable -- a raised exception, a returned reason, a different rendering
-- rather than adding a log line above the same wrong value.
Every fix below is mutation-checked: the production change was reverted, the
new test was confirmed to FAIL, and the fix restored.
hosted.py
* `CountResult.from_payload` used `payload.get("count", 0)`, so a response body
that lost or renamed `count` produced a confident all-clear on the one number
the tray exists to report. It now raises `InvalidCountPayload`, `poll_once`
reports a failed poll, and the badge keeps its last known value instead of
clearing to zero. The display-only subfields stay tolerant of absence.
* `_fire_notification` discarded the notifier's return value and `_handle_result`
advanced `_last_count` regardless, so the honest `False` that #29 added to
`_show_windows` went nowhere: a user was recorded as informed about breaks
they were never shown, and the count had to RISE again before a second
attempt. Delivery is now propagated and `_last_count` only advances when the
notification actually landed, so the next poll retries.
* `route_break_click` discarded `webbrowser.open`'s result. A click that opened
nothing now returns False, and `TrayApplication.open_needs_attention` tells
the user instead of leaving them in front of an unchanged screen.
config.py
* `TrayConfig.load` printed a warning and returned defaults when tray.json was
unreadable. `deployment_lane` defaults to "cloud", so a byoc install -- where
the fix must stay local -- was silently moved onto the hosted route by a
corrupt settings file. A missing file still means defaults; an unreadable one
raises `ConfigLoadError`. `load_or_defaults` hands back both so the caller
cannot lose it, and the tray now says so on startup.
menu.py
* `_get_recent_captures` swallowed every exception and returned `[]`, so an
unreadable captures directory rendered the same "No captures" as an empty
one. It now raises, and the menu says "Could not read captures".
* `_view_capture` never read the exit status of `openadapt visualize`, so a CLI
that ran and failed did nothing at all and said nothing. A non-zero exit now
falls back to the file browser, like a missing CLI always did.
platform/
* `prompt_input` returned None and `confirm_dialog` returned False when no
dialog could be shown -- identical to "the user cancelled" and "the user said
no". Clicking Start Recording or Delete could therefore do nothing, forever,
silently. Both now raise `DialogUnavailableError` when nothing could ask, and
the answers they do return are answers the user actually gave.
On Linux, zenity/kdialog exit 1 means the user declined; any other non-zero
exit is the tool failing, which used to be read as the user's answer AND
skipped the remaining fallbacks. On macOS, an osascript execution error is
now separated from a user cancel by stderr.
Callers: a recording proceeds with a default name (and says why there was no
prompt); a delete is not performed and the user is told it was not performed.
CI
* `ruff check src tests scripts` left examples/ and the four root-level helper
scripts outside every gate: 15 findings sat there while CI reported a green
lint. The gate now covers the repository and those findings are fixed --
including a screenshot helper that printed a success tick regardless of
whether the notification was delivered.
* The gitleaks install piped curl into tar without pipefail, so the step
reported tar's status rather than the download's.
Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
Why now
ruff>=0.1.0was unbounded,[tool.ruff.lint]declared noselect, and CI runs a blockinguv run ruff check src tests scripts. ruff 0.16.0 grew its DEFAULT rule set from 59 rules to 413, so this repo silently inherits 354 new rules the moment a runner resolves a new ruff. The only thing keepingmaingreen was the 0.14.13 pin inuv.lock— and.github/dependabot.ymlschedules weeklypipupdates. The next bump would have turnedmainred with 216 findings and no code change. Same failure mode already hitopenadapt-consilium(0 → 54) andOmniMCP(0 → 375) today.Rule-set decision: keep the FULL ruff 0.16 default set
No
select. After safe autofix the residual was 91 findings in a 3.9k-line tree — small enough to read and judge one by one, so the real coverage is worth more than trading it away for a narrow["E","F","I","W"]. The reasoning is written inline inpyproject.toml, and the ruff version is now bounded (ruff>=0.16,<0.17) so the default set can only change when a person raises the ceiling.uv.lockresolves 0.16.0, so CI genuinely exercises the new set.One rule disabled, documented inline:
BLE001. The tray is almost nothing but boundaries to optional, platform-owned subsystems — pystray, pynput, Pillow,keyring, desktop-notifier/Cocoa, tkinter, zenity/kdialog,osascript, PowerShell,winreg,ctypes.windll. They share no exception hierarchy, and every handler logs and degrades on purpose because a menu-bar app that dies when a notification daemon is missing is a worse product. Enumerating vendor exception types would reintroduce crash-on-first-surprise the next time a dependency added one — a behaviour change, not a style change.Real defects this exposed
Where a blind catch was hiding a defect rather than guarding a boundary, the code was fixed, not the rule silenced.
src/openadapt_tray/keychain.pyclear_ingest_tokendocumented "removed or already absent" and its own inline comment said absent = no-op success — but returnedFalse.keyringraisesPasswordDeleteErrorwhen there is nothing to delete, and the blanketexcept Exceptioncaught it.src/openadapt_tray/notifications.py_show_windowsreturnedTrueunconditionally after shelling out to PowerShell, ignoring exit status (PLW1510). macOS and Linux paths have always checkedreturncode.src/openadapt_tray/ipc.pyIPCServer._accept_loopcaught bareExceptionandbreak.OSError(allaccept()can raise) and it logs unlessstop()closed the socket.src/openadapt_tray/icons.pytry/except/pass(S110).scripts/generate_icons.pysrc/openadapt_tray/notifications.pysrc/openadapt_tray/platform/windows.py_find_executablewrappedos.path.existsintry/except/pass.os.path.existsswallows its ownOSErrors and returnsFalse, so the handler was unreachable and only hid a failedimport os.Regression tests added for the two user-visible fixes (
tests/test_keychain.py::TestClearIngestToken, newtests/test_notifications.py). Both fail againstmain.Mechanical, behaviour-preserving
PEP 604/585 annotations (
UP045/UP006/UP035), import sorting (I001),passafter docstrings (PIE790),OSError/TimeoutErroraliases (UP024/UP041), sorted__all__(RUF022), unused# noqaremoved (RUF100—E402/N802are not in the 0.16 default set, so both were inert),ClassVaron mutable class attributes (RUF012), unused unpacked variables prefixed (RUF059), collapsed nestedif(SIM102), explicitcheck=Falseon everysubprocess.runwhosereturncodeis already inspected (PLW1510), tz-aware datetimes that render the same local time as before (DTZ005/DTZ006), andchmod +xon the two scripts carrying a shebang (EXE001).Verified locally
uv sync --locked --extra dev→ ruff 0.16.0uv run ruff check src tests scripts→ All checks passeduv run pytest tests -q→ 153 passed, 2 skippeduv build→ wheel + sdist OK;scripts/check_release_consistency.pyOKCommit type
fix:—patch_tags = ["fix", "perf"], so this cuts a patch release. Warranted: three real behavioural defects ship in the package, not just lint config.Not in scope
Four ad-hoc root-level scripts (
generate_screenshots.py,rename_screenshots.py,test_notification*.py,examples/) still report 15 findings. They are outside the CI gate (src tests scripts) and outside this change.🤖 Generated with Claude Code
https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM