Skip to content

fix(cache): survive an unusable cache, and add asimov doctor - #123

Merged
django23 merged 3 commits into
mainfrom
fix/cache-permissions-and-doctor
Jul 29, 2026
Merged

fix(cache): survive an unusable cache, and add asimov doctor#123
django23 merged 3 commits into
mainfrom
fix/cache-permissions-and-doctor

Conversation

@django23

Copy link
Copy Markdown
Collaborator

Closes #122.

@ajoslin103 migrated from 0.3.0 to 0.10.0 and hit three bumps. Two of them are our bugs; the third is a documentation gap.

The crash

asimov aborted with nothing but cat: ~/.cache/asimov/excluded: Permission denied. Reproduced exactly:

chmod 000 ~/.cache/asimov/excluded && asimov    # exit 1, one cryptic line
chmod 500 ~/.cache/asimov && asimov             # exit 1, mid-scan

The cache is an optimisation, but nothing treated it as optional: a bare cat on an unreadable state file fails under set -Eeu -o pipefail and takes the run with it.

The cause was Asimov itself. ensure_cache_dir chowned the cache directory to the console user when running as root, but the state files were created after that chown — so every sudo asimov left root-owned files behind and broke the next run as the user.

Both halves are fixed. Reads and writes go through cache_readable/cache_writable, which warn once, name the reset command, and continue without the cache. The state files are created before the chown, and appending never changes an existing file's owner.

One thing this caught: the full-scan branch pipes find | tee "$ASIMOV_PATH_CACHE" | exclude_paths_from_stdin, so a failing tee takes the pipeline down under pipefail. It now falls back to the untee'd pipeline.

asimov doctor

Checks the install rather than the projects, and exits 1 if it finds anything:

Install        which asimov the shell actually runs; older ones shadowing it
Schedule       plists present, loaded, and pointing at a program that exists
Cache          readable and writable by you; entry count; last scan
Config         parses, and every section and key is one we understand
Time Machine   tmutil can read exclusions at all (it can't without Full Disk Access)

Two rules keep it safe to run part-way through a migration:

  • It never writes. Fixes are printed, not applied.
  • It never executes another asimov binary it finds. v0.3.0 parses no arguments at all, so running it to ask its version would start a real scan. Versions are read out of the file with grep. A test asserts the stub binary is never executed.

Docs

UPGRADING.md gains a v0.3.0 section — the version most people have. Because v0.3.0 ignores --version and --help too, it opens with a version check that reads the file rather than running it, and puts asimov doctor last, after the new binary is in place. It also covers the three leftovers that outlive a v0.3.0 install (the LaunchAgent, the old cellar, the root-owned cache) and notes that Asimov is a one-shot scan, not a daemon — an empty ps aux | grep asimov after brew services start is expected, not a failure.

Tests

219 passing, up from 188; shellcheck clean. Written test-first — every behaviour here was red against the pre-fix script before it was green.

  • tests/cache.bats — 7 tests for unreadable/unwritable state, warn-once, and the --full-scan tee path
  • tests/doctor.bats — 31 tests, including one asserting doctor never executes a binary it finds
  • tests/bin/launchctl — new mock, driven by ASIMOV_TEST_LAUNCHCTL_LOADED
  • tests/test_helper.bash — now strips any real asimov install from PATH, since doctor inspects PATH and would otherwise report a different result on every machine
  • scripts/test.shdoctor.bats added to the file list

Tests that depend on chmod skip when running as root, where they'd be meaningless.

django23 added 3 commits July 29, 2026 14:03
The cache under ~/.cache/asimov is an optimisation, but nothing treated it
as optional. A bare `cat` on an unreadable state file failed under
`set -Eeu -o pipefail` and aborted the run immediately, printing nothing
but "Permission denied" — no context, no fix, exit 1.

The cause was Asimov itself. ensure_cache_dir chowned the cache directory
to the console user when running as root, but the state files were created
*after* that chown, so every `sudo asimov` left root-owned files behind and
broke the next run as the user.

Guard every cache read and write behind cache_readable/cache_writable, warn
once naming the reset command, and carry on without the cache. Create the
state files before the chown so appends — which never change an existing
file's owner — keep them owned by the user who has to read them next.

Closes #122
Checks the install rather than the projects: which asimov the shell
actually runs, whether a schedule is installed and loaded, whether the
cache is readable and writable, whether the config parses, and whether
tmutil can read exclusions at all. Exits 1 if it finds anything.

Two rules keep it safe to run part-way through a migration. It never
writes — fixes are printed, not applied. And it never executes another
asimov binary it finds: v0.3.0 parses no arguments at all, so running it
to ask its version would start a real scan, so versions are read out of
the file instead.

The test helper now strips any real asimov install from PATH, since doctor
inspects PATH and would otherwise report a different result per machine.

Refs #122
v0.3.0 is the version most people have, and it parses no arguments at all:
`asimov --version` and `asimov doctor` are both ignored and it goes
straight to scanning. So the upgrade notes lead with a version check that
reads the file instead of running it, and put `asimov doctor` last, after
the new binary is in place.

Also documents the three leftovers that outlive a v0.3.0 install — the
LaunchAgent, the old cellar, and a root-owned cache — and that Asimov is a
one-shot scan, not a daemon, so an empty `ps aux | grep asimov` after
`brew services start` is expected.

Refs #122
@django23
django23 merged commit a35abbe into main Jul 29, 2026
5 checks passed
@django23
django23 deleted the fix/cache-permissions-and-doctor branch July 29, 2026 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migrating from 0.3.0 - in the trenches

1 participant