Skip to content

fix: refresh Claude marketplace cache during install#62

Merged
blimmer merged 4 commits into
mainfrom
fix/refresh-marketplace-cache
May 8, 2026
Merged

fix: refresh Claude marketplace cache during install#62
blimmer merged 4 commits into
mainfrom
fix/refresh-marketplace-cache

Conversation

@blimmer
Copy link
Copy Markdown
Contributor

@blimmer blimmer commented May 8, 2026

Summary

contextbridge install claude failed for users upgrading across the cli@contextbridgeplanbridge@contextbridge rename because Claude's locally cached marketplace catalog still listed the old plugin id. Run claude plugin marketplace update contextbridge after marketplace add so the cached catalog always reflects upstream — closes #61.

Review focus

Recommend reviewing commit-by-commit. The first two commits are pure test-side refactors (no source change); the third is the one-line fix. Splitting them keeps the fix obvious to read and the refactors independently revertable.

  • Commit 1 (assertion refactor): most install/uninstall tests previously asserted the full commandRunner.calls chain via .toEqual even when they only cared about one or two specific calls. That made every shellout addition require fixture updates in 13 unrelated tests. Converted to targeted callsTo(...) checks; kept one canonical full-sequence test per scenario family (clean install, migration, clean uninstall) so ordering invariants stay locked down in one place.
  • Commit 2 (setup refactor): extracted primeClaudeShellouts + promoted stubPluginList / stubMarketplaceList to shared helpers. Each test file now wraps these with a local setupTest. Adjacent infrastructure change: flipped FakeCommandRunner from first-match-wins to last-match-wins so overrides registered after primeClaudeShellouts actually take effect — matches Jest/vitest/sinon override semantics. Added a regression test for the override path; renamed the existing matcher-order test.
  • Commit 3 (the fix): 1 source line in runInstall, 1 default stub in primeClaudeShellouts, 3 fixture line additions. Run unconditionally — on a fresh install the second shellout is a near-no-op git pull; on the bug case it does the actual refresh. The post-update refresh path in runUpdate benefits automatically by re-entering runInstall.

The fix-only commit is what reviewers should weigh-in on most carefully; the refactors are mechanical and have green tests with no source change.

Commits

  • ec22228 — replace over-specified commandRunner.calls.toEqual([...]) chains with targeted callsTo(...) assertions across ClaudeInstaller.test.ts. Net -33 lines, no source change.
  • cf12c48 — centralize Claude test setup with primeClaudeShellouts and per-file setupTest helpers; flip FakeCommandRunner to last-match-wins for clean override semantics. Net -92 lines, no source change.
  • 7abf4f7 — add claude plugin marketplace update contextbridge after marketplace add in ClaudeInstaller.runInstall; strengthen the existing "already on disk" test as the regression test for bug: contextbridge install claude fails on stale marketplace cache after plugin rename #61.

blimmer added 3 commits May 8, 2026 10:40
…s with targeted assertions

Most install/uninstall tests previously asserted the full `commandRunner.calls`
chain via `.toEqual([...])` even when the test only cared about one or two
specific calls (e.g., "did the legacy uninstall fire?"). That made every
shellout addition to runInstall/runUninstall require fixture updates in 13
unrelated tests.

Convert the over-specified tests to targeted `callsTo(...)` checks against the
specific behavior each test is exercising. Keep one canonical full-sequence
`.toEqual` per scenario family — clean install, migration, clean uninstall —
to lock down ordering invariants in one place per family.

No source change; the existing 269 tests still pass.
…llouts + setupTest

Extract three reusable test helpers in `testHelpers/claudeInstallerFakes.ts`:

- `primeClaudeShellouts(commandRunner)` — pre-stubs the canonical happy
  path for every `claude plugin` subcommand the installer can fire (claude
  on PATH, empty plugin/marketplace lists, all write commands resolve
  cleanly).
- `stubPluginList` / `stubMarketplaceList` — promoted out of
  `ClaudeInstaller.test.ts` so `install.test.ts` and `cli.test.ts` can
  reuse them.

Each test file now wraps these with a local `setupTest` that returns a
primed installer + context in one line. Tests then specify only what's
*different* from the happy path, instead of repeating the full setup
boilerplate.

Adjacent infrastructure change: flip `FakeCommandRunner` matcher
resolution from first-match-wins to last-match-wins so that overrides
registered after `primeClaudeShellouts` actually take effect — matches
how Jest/vitest/sinon handle override semantics. Added a regression test
covering the override case; renamed the existing matcher-order test.

No source changes; all 270 tests still pass.
`claude plugin marketplace add` short-circuits when the marketplace is
already configured locally and skips refreshing the cached catalog. For
existing users upgrading across the `cli@contextbridge` →
`planbridge@contextbridge` rename, the cached catalog still listed the old
plugin id, so the migration in `runInstall` failed with `Plugin
"planbridge" not found in marketplace "contextbridge"`.

Run `claude plugin marketplace update contextbridge` after `marketplace
add` on every install. On a fresh install it's a near-no-op `git pull`;
on the bug case it does the actual refresh that unblocks the install.
The post-update refresh path in `runUpdate` benefits automatically by
re-entering `runInstall`.

Strengthen the existing "already on disk" test in
`ClaudeInstaller.test.ts` with an assertion that `marketplace update`
fired — that's the regression test for this issue.

Closes #61.
@blimmer blimmer marked this pull request as ready for review May 8, 2026 17:04
@blimmer blimmer requested a review from jcarver989 as a code owner May 8, 2026 17:04
commandRunner.on(CLAUDE_BINARY, ['plugin', 'marketplace', 'list', '--json']).resolves(marketplaceListResult([]));
commandRunner.on(CLAUDE_BINARY, ['plugin', 'list', '--json']).resolves(pluginListResult([]));
commandRunner.on(CLAUDE_BINARY, ['plugin', 'marketplace', 'add']).resolves();
commandRunner.on(CLAUDE_BINARY, ['plugin', 'install']).resolves();
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The amount of boilerplate / churn on these commands was too much. I split out some common helpers to make it cleaner. What's nice is that you can easily see in each test "what deviates from the boilerplate baseline". As noted in the PR description, I recommend reviewing commit-by-commit.

'--scope',
scope,
]);
await runPluginCommand(ctx, binaryName, 'marketplace update', ['marketplace', 'update', CLAUDE_MARKETPLACE_NAME]);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the real fix to the bug.

},
{ cmd: CLAUDE_BINARY, args: ['plugin', 'update', CLAUDE_PLUGIN_ID, '--scope', 'user'], opts: {} },
]);
const updateCalls = commandRunner.callsTo(CLAUDE_BINARY, ['plugin', 'update']);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice cleanups here.

Comment on lines +51 to +52
stubMarketplaceList(commandRunner, [{ name: CLAUDE_MARKETPLACE_NAME }]);
stubPluginList(commandRunner, [{ id: CLAUDE_PLUGIN_ID, scope: 'user' }]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since plugins conceptually belong in a marketplace, it seems like we'd want a single test helper that allows us to say which plugins live in which marketplace ?

Copy link
Copy Markdown
Contributor Author

@blimmer blimmer May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 526ea4astubPluginList and stubMarketplaceList are gone, replaced by stubClaudeState({ marketplaces: [{ name, plugins: [...] }] }) which encodes which plugins live in which marketplace.

Comment on lines +25 to +34
export function primeClaudeShellouts(commandRunner: FakeCommandRunner): void {
commandRunner.setWhich(CLAUDE_BINARY, '/usr/local/bin/claude');
commandRunner.on(CLAUDE_BINARY, ['plugin', 'list', '--json']).resolves(pluginListResult([]));
commandRunner.on(CLAUDE_BINARY, ['plugin', 'marketplace', 'list', '--json']).resolves(marketplaceListResult([]));
commandRunner.on(CLAUDE_BINARY, ['plugin', 'marketplace', 'add']).resolves();
commandRunner.on(CLAUDE_BINARY, ['plugin', 'install']).resolves();
commandRunner.on(CLAUDE_BINARY, ['plugin', 'update']).resolves();
commandRunner.on(CLAUDE_BINARY, ['plugin', 'uninstall']).resolves();
commandRunner.on(CLAUDE_BINARY, ['plugin', 'marketplace', 'remove']).resolves();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider maybe doing FakeCommandRunner.default() or something similar.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to leave this one. FakeCommandRunner is a generic fake — primeClaudeShellouts is Claude-specific (uses getDescriptor('claude').binaryName and primes the seven claude plugin shellouts).

Folding it into the runner as .default() / .primedForClaude() either couples a generic class to a specific consumer — and we'd want .primedForCodex() etc. as harnesses grow — or it's a no-value rename of new FakeCommandRunner(). The standalone helper next to its Claude-specific siblings in claudeInstallerFakes.ts reads better at the call sites IMO. Happy to revisit if you had a different shape in mind.

Replace stubPluginList + stubMarketplaceList with a single
stubClaudeState helper whose fixture nests plugins under their
marketplace, addressing PR #62 review feedback. Tests that
deliberately exercise plugin-list-only code paths use the
unmanagedPlugins escape hatch since `claude plugin list --json`
returns id+scope only — no marketplace association on the wire.
@blimmer blimmer merged commit 58be09a into main May 8, 2026
12 checks passed
@blimmer blimmer deleted the fix/refresh-marketplace-cache branch May 8, 2026 17:25
blimmer added a commit that referenced this pull request May 8, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.3.0](v0.2.0...v0.3.0)
(2026-05-08)


### ⚠ BREAKING CHANGES

* rename Claude plugin to planbridge@contextbridge; refresh plugins on
update ([#52](#52))

### Features

* add automatic release changelog with release-please
([#21](#21))
([45a1bf1](45a1bf1))
* rename Claude plugin to planbridge@contextbridge; refresh plugins on
update ([#52](#52))
([2794ae6](2794ae6))
* **ui:** add GitHub link to header help menu
([#19](#19))
([c36289b](c36289b))


### Bug Fixes

* emit plan review analytics from shared runner
([#48](#48))
([5c769ff](5c769ff))
* **plan:** pre-scan src for transitive deps to stop vitest reload flake
([3b3cc0c](3b3cc0c)),
closes [#12](#12)
* **plan:** stop vitest reload flake from lazy zod optimization
([#15](#15))
([3b3cc0c](3b3cc0c))
* refresh Claude marketplace cache during install
([#62](#62))
([58be09a](58be09a))
* resolve contextbridge via PATH for post-update refresh
([#64](#64))
([f658af4](f658af4))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: contextbridge-pr-automation[bot] <259134118+contextbridge-pr-automation[bot]@users.noreply.github.com>
Co-authored-by: Ben Limmer <ben@benlimmer.com>
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.

bug: contextbridge install claude fails on stale marketplace cache after plugin rename

2 participants