Summary
UpdateChecker.saveCache(_:) catches every error from the cache-write pipeline (directory creation, JSON encoding, file write) and drops them on the floor:
// Sources/mcs/Core/UpdateChecker.swift (in saveCache)
} catch {
// Cache write failure is non-fatal — next check will just redo network calls
}
This was surfaced during #334 review. Filing separately since it's pre-existing and outside the #334 fix scope.
Why this matters
The inline justification ("non-fatal — next check will just redo network calls") is only partially true:
- Permission/filesystem errors repeat forever. If
createDirectory fails because ~/.mcs/ is read-only (recovery mode, disk full, bad ACL, sudo mcs aftermath), every session silently re-runs full network probes. No feedback to the user, no degraded-mode signal.
- Encoding errors are programmer bugs, not transient.
JSONEncoder failing on CachedResult means the type is broken — will never self-correct.
- Silent-failure compounds with other silent paths. When
invalidateCache later can't delete a file saveCache silently failed to write, the two failures chain with zero diagnostic trail. The PR author had to add a fileExists post-check in seedUpdateCheckCache test helper specifically to guard against this defense-in-depth hole.
CLAUDE.md's convention is explicit: "Never use try? to silently discard errors — use do/catch and surface the error (via output.warn(), logging, or propagation)." The same reasoning applies to empty catch {} blocks.
Proposed fix
Match the pattern used by invalidateCache after #334: return a Bool (or throw), and have the single caller (performCheck) decide how to surface the failure. Or simpler: accept a CLIOutput? parameter and warn at the source, preserving the underlying error in the message.
@discardableResult // ← only if truly safe to ignore, which it isn't
func saveCache(_ result: CheckResult) -> Bool {
// ... existing encode + write logic, with catch returning false
}
Callers that can't reasonably react (e.g., the SessionStart hook path) can _ = checker.saveCache(result) with an explicit comment — forces the decision to be auditable rather than implicit.
Scope
Sources/mcs/Core/UpdateChecker.swift — saveCache(_:)
- Call sites:
performCheck, --hook path
- Tests:
UpdateCheckerCacheTests — add a permission-error case (chmod'd tmp dir)
Related
Summary
UpdateChecker.saveCache(_:)catches every error from the cache-write pipeline (directory creation, JSON encoding, file write) and drops them on the floor:This was surfaced during #334 review. Filing separately since it's pre-existing and outside the #334 fix scope.
Why this matters
The inline justification ("non-fatal — next check will just redo network calls") is only partially true:
createDirectoryfails because~/.mcs/is read-only (recovery mode, disk full, bad ACL,sudo mcsaftermath), every session silently re-runs full network probes. No feedback to the user, no degraded-mode signal.JSONEncoderfailing onCachedResultmeans the type is broken — will never self-correct.invalidateCachelater can't delete a filesaveCachesilently failed to write, the two failures chain with zero diagnostic trail. The PR author had to add afileExistspost-check inseedUpdateCheckCachetest helper specifically to guard against this defense-in-depth hole.CLAUDE.md's convention is explicit: "Never use
try?to silently discard errors — usedo/catchand surface the error (viaoutput.warn(), logging, or propagation)." The same reasoning applies to emptycatch {}blocks.Proposed fix
Match the pattern used by
invalidateCacheafter #334: return aBool(or throw), and have the single caller (performCheck) decide how to surface the failure. Or simpler: accept aCLIOutput?parameter and warn at the source, preserving the underlying error in the message.Callers that can't reasonably react (e.g., the SessionStart hook path) can
_ = checker.saveCache(result)with an explicit comment — forces the decision to be auditable rather than implicit.Scope
Sources/mcs/Core/UpdateChecker.swift—saveCache(_:)performCheck,--hookpathUpdateCheckerCacheTests— add a permission-error case (chmod'd tmp dir)Related
@discardableResultremoval oninvalidateCacheaddresses the parallel issue in the delete path but the write path still silently failsTests/MCSTests/PackUpdaterTests.swiftseedUpdateCheckCachehelper — usesfileExistsaftersaveCachespecifically to detect the silent failure