Files
thedotmack__claude-mem/tests
Alex Newman d959572bde fix(setup): guard plugin deps on completeness, not node_modules existence (#3972)
* fix(setup): guard plugin deps on completeness, not node_modules existence

`ensurePluginDependencies()` decided whether to run `bun install` by asking
whether `node_modules/` existed. A tree that is merely present — but short
of the declared closure — satisfied that check and permanently skipped
repair on every subsequent Setup run.

Two trigger paths, and the second needs no corruption at all:

1. An install interrupted mid-fetch (network timeout, OOM, registry 5xx)
   leaves `node_modules/` behind incomplete.
2. A tree that was complete *for the version that created it*. `zod` was
   added to plugin deps after some users had already installed; their
   node_modules has been incomplete ever since, and no upgrade heals it
   because the stale tree is gitignored and gets re-seeded into each new
   cache version.

The worker then dies at boot on `Cannot find module 'zod/v3'` while memory
search keeps working — `mcp-server.cjs` bundles zod (build-hooks.js:519
hard-fails if it ever externalizes it) while `worker-service.cjs` has 19
external zod requires. So the plugin looks alive while capture is dead.
One reporter lost ~4 months of capture with no visible symptom.

Guard on completeness instead: every key of `package.json` `dependencies`
must resolve, with a `<dep>/package.json` fallback for bin-only packages
like tree-sitter-cli (gh #2730), plus the zod subpaths the worker requires.
This mirrors `verifyCriticalModules` (src/npx-cli/install/setup-runtime.ts:245),
which already applies exactly this contract on the npx install path but was
never reachable from the Setup hook. It cannot be imported here — this
script is standalone and dependency-free — so the probe is inlined and the
two are cross-referenced.

Two consequences fall out:

- The post-failure `rmSync` of node_modules is removed. It existed only
  because the existence guard would otherwise block retry forever; the
  completeness guard re-detects the gap on the next run, so deleting bought
  nothing while actively destroying a partial tree that still powers search.
- A zero exit from `bun install` is no longer trusted. It can exit 0 with a
  failed integrity check, so the closure is re-probed afterwards and the
  diagnostic reports what actually resolves.

The install diagnostic now names the unresolvable modules, which is what
stops this failure mode from being silent.

Fixes #3755. Refs #3604 (plan-16), #2730.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NT5K64VU4a7Kbc36oTVjyc

* fix(setup): keep the completeness probe inside the plugin's own node_modules

Greptile P1 on #3872, and it was right.

`require.resolve(dep, { paths: [nodeModulesPath] })` reads as tree-scoped
but is not. `paths` only seeds Node's lookup; resolution then walks every
ancestor directory and always consults the global folders
($HOME/.node_modules, $PREFIX/lib/node). Plugin roots live at
~/.claude/plugins/cache/thedotmack/claude-mem/<version>/, so a copy of a
dependency anywhere above them — or installed globally — answered for the
plugin's own.

Verified against the committed probe: a plugin whose node_modules is
completely EMPTY, with a valid zod one directory up, exits 0 with no output
and never runs the install. That is the #3755 bug reintroduced by the fix
for it, and it would have shipped silently.

Presence is now checked by statting `<node_modules>/<dep>/package.json`
directly, which cannot escape the tree. This is also the signal
scripts/check-postinstall-allowlist.js:75-78 already uses, and it handles
scoped names (split into path segments) and bin-only packages like
tree-sitter-cli (package.json present, no entry point — gh #2730) without
the bare-name/fallback dance.

zod's subpaths still need real resolution, since they are `exports` entries
that a present directory does not guarantee. Those are now accepted only
when the resolved file lands inside the plugin's own zod directory. Both
sides are realpath'd before comparison: bun can materialise node_modules
entries as links into a shared store, and Node returns the real path of
what it resolved, so a literal comparison would report a healthy linked
install as missing and loop the install forever. Containment uses
path.relative rather than string prefixing so a sibling like zod-extra is
not mistaken for being inside zod.

Regression test added; it fails against the previous commit's probe.

Note for follow-up: verifyCriticalModules (setup-runtime.ts:245) has the
same ancestor/global escape. It is far less dangerous there — a post-install
assertion that fails loud, not the gate deciding whether repair runs at all
— but worth tightening.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NT5K64VU4a7Kbc36oTVjyc

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 18:06:09 -07:00
..
2026-07-22 23:49:48 -07:00