Skip to content

Fix Settings plugin installs registering unloadable directory paths#1428

Merged
gsxdsm merged 7 commits into
mainfrom
gsxdsm/compound-plugin
Jun 5, 2026
Merged

Fix Settings plugin installs registering unloadable directory paths#1428
gsxdsm merged 7 commits into
mainfrom
gsxdsm/compound-plugin

Conversation

@gsxdsm

@gsxdsm gsxdsm commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #1423. Enabling a plugin installed from Settings → Built-in Plugins failed with "Plugin entry must be a file, got directory: …"; those plugins now load. The install routes registered the manifest directory as the plugin path, but since FN-4128 the loader requires a loadable entry file (Node ESM cannot import directories) — only the CLI startup path had been migrated to entry-file resolution, which is why CLI-auto-installed plugins worked while every Settings install broke on enable.

The dashboard install routes now resolve and register the entry file (bundled.jsdist/index.jssrc/index.ts, helper added to @fusion/core), return a clear 400 when a package ships no loadable entry, and the enable route heals legacy directory-path registrations in place — mirroring the CLI's startup heal — so already-broken rows recover from the UI without a restart or reinstall.

Route tests now assert installs register entry files and cover the enable-route heal; existing install tests were updated to the entry-file contract (their old mocks let the directory contract pass silently).


Compound Engineering
Claude Code

Summary by CodeRabbit

  • New Features

    • Compound Engineering and Roadmaps added as bundled plugins in the dashboard.
  • Bug Fixes

    • Graph editor overlay now opens correctly.
    • Installing Built‑in Plugins from Settings no longer fails.
    • Dashboard now registers a plugin's concrete loadable entry file and heals legacy directory-style registrations; installs return a clear error when no valid entry file is found.
  • Documentation

    • Updated guidance on bundled-plugin registration drift and fixes.
  • Tests

    • Added tests covering entry-file resolution and regressions.

gsxdsm and others added 2 commits June 4, 2026 21:57
Plugins installed from Settings → Built-in Plugins registered the
manifest directory as the plugin path, but since FN-4128 the loader
requires a loadable entry FILE (Node ESM cannot import directories),
so enabling failed with "Plugin entry must be a file, got directory".
Only the CLI startup path had been migrated to entry-file resolution,
which is why CLI-auto-installed plugins worked and Settings installs
never did.

- Add resolvePluginEntryPath (bundled.js → dist/index.js →
  src/index.ts) to @fusion/core; the CLI keeps its local copy (its
  test fs mocks don't reach externalized core) with sync comments
  both ways.
- Register the resolved entry file in both dashboard install routes;
  400 with a clear message when a package has no loadable entry.
- Heal legacy directory-path registrations on enable, mirroring the
  CLI's startup heal, so existing broken rows recover from the UI
  without a restart.
- Route tests: assert installs register entry files, cover the
  enable-route heal, and update existing install tests to the
  entry-file contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Plugin installation and enablement now resolve and register the loadable entry file (bundled.js, dist/index.js, or src/index.ts) instead of the package directory. A new resolvePluginEntryPath helper in @fusion/core determines which file is importable, and the dashboard install/enable routes use it to register correct paths and heal legacy directory-path records.

Changes

Plugin entry file resolution and dashboard integration

Layer / File(s) Summary
Core plugin entry path resolver
packages/core/src/plugin-loader.ts, packages/core/src/index.ts, packages/cli/src/plugins/bundled-plugin-install.ts
New exported resolvePluginEntryPath(pluginDir) function selects the plugin's importable entry file from a prioritized list (bundled.js → dist/index.js → src/index.ts), returning the resolved path or null. The function is exported from @fusion/core and documented with a sync contract note in the CLI.
Dashboard install and enable route updates
packages/dashboard/src/plugin-routes.ts, packages/dashboard/src/routes.ts
Install routes now call resolvePluginEntryPath to register the loadable entry file instead of the manifest directory, throwing a 400 error if no entry is found. The enable route adds a best-effort heal step that detects directory-path registrations and updates them to resolved entry file paths before loading.
Test coverage for entry file resolution and healing
packages/dashboard/src/__tests__/plugin-routes.test.ts
Comprehensive test updates verify install routes register entry file paths, enable healing corrects legacy directory paths to entry files, a negative test ensures 400 when no loadable entry file exists, and bundled plugin detection acknowledges both manifest and bundled.js across suites.
CLI/core parity test
packages/cli/src/plugins/__tests__/resolve-plugin-entry-path-sync.test.ts
New on-disk test ensures the CLI resolver and @fusion/core resolver return identical results for multiple filesystem layouts.
Release notes and integration documentation
.changeset/workflow-graph-editor-and-bundled-plugins.md, docs/solutions/integration-issues/bundled-plugin-registration-drift.md, CONCEPTS.md
Changeset and integration docs document the fix for plugin entry file registration, the vitest fs-mock trap and real-fs drift-guard testing, the tightened prevention guidance, and the backwards-compatible heal for legacy directory-path records.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through builds with whiskers keen,

Found directories where files should've been.
Now bundled.js lights the trail,
Old paths mend when heals prevail,
I hop, I test, I fix — so clean.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main bug fix: Settings plugin installs were registering directory paths instead of loadable entry files, causing failures.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gsxdsm/compound-plugin

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a regression introduced in FN-4128 where both dashboard install routes registered the package directory rather than a concrete loadable entry file, causing every Settings-initiated plugin enable to fail with "Plugin entry must be a file, got directory." The fix resolves and persists the real entry file (bundled.jsdist/index.jssrc/index.ts) at install time, and adds a heal pass on enable that re-points legacy directory registrations in place — mirroring the CLI's startup heal.

  • resolvePluginEntryPath is promoted to @fusion/core so both dashboard install routes share it; the CLI keeps a local copy (due to vitest fs-mock isolation) guarded by a real-fs drift-guard test that asserts both copies produce identical results across all candidate layouts.
  • Both routes.ts and createPluginRouter enable handlers now heal legacy directory-path rows before calling loadPlugin, and install handlers return 400 when a package ships no loadable entry file.
  • Route tests were updated to assert entry-file paths (not directory paths), and new tests cover the dist/index.js/src/index.ts fallbacks, the heal flow on both surfaces, and the 400 no-entry-file branch.

Confidence Score: 5/5

Safe to merge. The core bug fix — registering an entry file instead of a directory at install time and healing legacy directory rows on enable — is correctly implemented and tested on both surfaces.

The implementation is straightforward and consistent: both install routes now call resolvePluginEntryPath before persisting the path, both enable routes heal legacy directory rows in place, and the drift-guard test ensures the CLI and core copies of the helper stay in sync. No incorrect data is persisted, no existing contract is broken, and the heal is fail-soft. The only gap is missing install-handler entry-file tests for createPluginRouter specifically, which does not affect correctness since the same function and code structure is used.

packages/dashboard/src/tests/plugin-routes.test.ts — the createPluginRouter install handler's dist/index.js, src/index.ts, and no-entry-file branches are untested

Important Files Changed

Filename Overview
packages/dashboard/src/routes.ts Install route now calls resolvePluginEntryPath(manifestDir) and throws 400 when null; enable route heals legacy directory rows with statSync inside try/catch; both changes are correct and well-tested.
packages/dashboard/src/plugin-routes.ts createPluginRouter install handler now resolves the entry file path; enable handler heals legacy directory rows using async stat; both surfaces consistent with routes.ts. Install handler entry-file fallback paths lack dedicated unit tests.
packages/core/src/plugin-loader.ts Adds resolvePluginEntryPath (exported) with the same candidate list as the CLI copy; uses existsSync (sync) which is intentional and documented.
packages/cli/src/plugins/tests/resolve-plugin-entry-path-sync.test.ts New drift-guard test runs both CLI and core copies against real temp-dir layouts and asserts identical outputs across all candidate permutations including the no-entry null case.
packages/dashboard/src/tests/plugin-routes.test.ts Comprehensively updated: existing install assertions pinned to entry-file paths, new tests for dist/src fallbacks and heal flow on both surfaces, 400 no-entry branch covered for routes.ts. createPluginRouter install handler missing fallback and no-entry tests.
packages/cli/src/plugins/bundled-plugin-install.ts Comment updated to reference the authoritative core copy; no functional change to the local resolvePluginEntryPath implementation.
packages/core/src/index.ts resolvePluginEntryPath added to public exports alongside PluginLoader; straightforward export addition.

Sequence Diagram

sequenceDiagram
    participant UI as Settings UI
    participant RT as routes.ts / createPluginRouter
    participant RPM as resolvePluginManifest
    participant REP as resolvePluginEntryPath
    participant PS as PluginStore
    participant PL as PluginLoader

    UI->>RT: "POST /api/plugins {mode:install, path}"
    RT->>RPM: resolvePluginManifest(path)
    RPM-->>RT: "{manifestDir, manifest}"
    RT->>REP: resolvePluginEntryPath(manifestDir)
    alt bundled.js / dist/index.js / src/index.ts found
        REP-->>RT: entryFilePath
        RT->>PS: "registerPlugin({path: entryFilePath})"
        PS-->>RT: plugin
        RT-->>UI: "201 {plugin}"
    else no loadable entry
        REP-->>RT: null
        RT-->>UI: 400 no loadable entry file
    end

    UI->>RT: POST /api/plugins/:id/enable
    RT->>PS: enablePlugin(id)
    PS-->>RT: plugin (path may be legacy dir)
    RT->>RT: stat(plugin.path).isDirectory()?
    alt path is directory (legacy registration)
        RT->>REP: resolvePluginEntryPath(plugin.path)
        REP-->>RT: entryFilePath
        RT->>PS: "updatePlugin(id, {path: entryFilePath})"
        PS-->>RT: healed plugin
    end
    RT->>PL: loadPlugin(id)
    PL-->>RT: ok
    RT-->>UI: "200 {plugin}"
Loading

Reviews (5): Last reviewed commit: "Fix quality-backfill suites broken by fa..." | Re-trigger Greptile

Comment thread packages/dashboard/src/__tests__/plugin-routes.test.ts
Comment thread packages/core/src/plugin-loader.ts

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/dashboard/src/__tests__/plugin-routes.test.ts`:
- Around line 283-285: The tests currently only simulate a single existing entry
("bundled.js") via mockExistsSync and assert only that the stored path contains
the plugin id; update the suite to fully exercise the entry-resolution contract
by (1) tightening existing bundled-plugin assertions to require a filename
suffix (e.g., ensure the resolved path endsWith a file like ".js" or ".ts" not
just a directory and still contains the plugin id), (2) extend mockExistsSync
behavior in the relevant cases to return true for the other supported candidates
("dist/index.js" and "src/index.ts") and add separate assertions verifying those
exact file endings are chosen, and (3) add a test case for the "manifest exists
but no loadable entry file" scenario where mockExistsSync returns false for all
candidates and assert the 400/no-entry failure path is triggered; focus your
edits around the mockExistsSync setup and the affected tests in
plugin-routes.test.ts so each surface (bundled.js, dist/index.js, src/index.ts,
and no-entry) is explicitly asserted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a0dd587d-fe09-48c3-93c6-62c86f233e99

📥 Commits

Reviewing files that changed from the base of the PR and between e5bab64 and 742989c.

📒 Files selected for processing (8)
  • .changeset/workflow-graph-editor-and-bundled-plugins.md
  • docs/solutions/integration-issues/bundled-plugin-registration-drift.md
  • packages/cli/src/plugins/bundled-plugin-install.ts
  • packages/core/src/index.ts
  • packages/core/src/plugin-loader.ts
  • packages/dashboard/src/__tests__/plugin-routes.test.ts
  • packages/dashboard/src/plugin-routes.ts
  • packages/dashboard/src/routes.ts

Comment thread packages/dashboard/src/__tests__/plugin-routes.test.ts
- Add heal block to createPluginRouter's enable handler so it matches
  routes.ts (directory-path registrations re-pointed at entry files)
- Test the 400 "no loadable entry file" install branch
- Add a real-fs drift-guard test asserting the CLI and @fusion/core
  copies of resolvePluginEntryPath resolve identically

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gsxdsm

gsxdsm commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

Two gaps are worth tracking: the /:id/enable handler inside createPluginRouter was not given the same heal block, leaving it inconsistent with routes.ts for any direct consumer of that router; and the new 400 'no loadable entry file' branch has no test

Both addressed in 7dde43a: createPluginRouter's /:id/enable now runs the same directory-path heal as routes.ts (with a test covering it), and the 400 no-loadable-entry branch is covered in the negative-paths suite.

…1428)

- Bundled fallback assertions for dependency-graph/reports now require
  the bundled.js entry-file suffix instead of just containing the id
- Add route-level fallback cases for dist/index.js and src/index.ts

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

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.

♻️ Duplicate comments (1)
packages/dashboard/src/__tests__/plugin-routes.test.ts (1)

520-524: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Good progress on entry-resolution coverage, but gaps remain.

The new test at lines 770-788 addresses the no-entry-file case—nice! However, the remaining gaps from the earlier review still need attention:

  1. Lines 520-524 and 552-556: These bundled-plugin assertions use stringContaining(plugin-id) rather than a file-suffix pattern. A directory-path regression would still pass. Tighten to stringMatching(/plugin-id[\\/]bundled\.js$/) or similar.

  2. Missing fallback coverage: The entry-resolution contract checks bundled.js, dist/index.js, and src/index.ts in order, but no tests exercise the latter two. Add narrow cases where mockExistsSync returns false for bundled.js but true for dist/index.js (and similarly for src/index.ts), then assert the registered path ends with the chosen candidate.

As per coding guidelines, "Regression tests must assert the general invariant across ALL known surfaces, not only the single reported reproduction (FN-5893)".

Also applies to: 552-556, 770-788

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dashboard/src/__tests__/plugin-routes.test.ts` around lines 520 -
524, Tighten the bundled-plugin path assertions and add fallback tests: replace
the existing
expect(pluginStore.registerPlugin).toHaveBeenCalledWith(expect.objectContaining({
path: expect.stringContaining("fusion-plugin-dependency-graph") })) checks with
a regex-based assertion such as
expect.stringMatching(/fusion-plugin-dependency-graph[\\/](?:bundled\.js|dist[\\/]index\.js|src[\\/]index\.(ts|js))$/)
to ensure the candidate file suffix is asserted; add two new narrow tests that
stub/mock mockExistsSync to return false for bundled.js but true for
dist/index.js (and a second test false for both bundled.js and dist/index.js but
true for src/index.ts) and assert pluginStore.registerPlugin was called with a
manifest path matching the expected chosen candidate using the same
stringMatching pattern; keep references to pluginStore.registerPlugin and
mockExistsSync so the changes are localized in the existing
plugin-routes.test.ts tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/dashboard/src/__tests__/plugin-routes.test.ts`:
- Around line 520-524: Tighten the bundled-plugin path assertions and add
fallback tests: replace the existing
expect(pluginStore.registerPlugin).toHaveBeenCalledWith(expect.objectContaining({
path: expect.stringContaining("fusion-plugin-dependency-graph") })) checks with
a regex-based assertion such as
expect.stringMatching(/fusion-plugin-dependency-graph[\\/](?:bundled\.js|dist[\\/]index\.js|src[\\/]index\.(ts|js))$/)
to ensure the candidate file suffix is asserted; add two new narrow tests that
stub/mock mockExistsSync to return false for bundled.js but true for
dist/index.js (and a second test false for both bundled.js and dist/index.js but
true for src/index.ts) and assert pluginStore.registerPlugin was called with a
manifest path matching the expected chosen candidate using the same
stringMatching pattern; keep references to pluginStore.registerPlugin and
mockExistsSync so the changes are localized in the existing
plugin-routes.test.ts tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: aed725ee-13b9-407d-a611-a16c0dd8cd39

📥 Commits

Reviewing files that changed from the base of the PR and between 742989c and 7dde43a.

📒 Files selected for processing (3)
  • packages/cli/src/plugins/__tests__/resolve-plugin-entry-path-sync.test.ts
  • packages/dashboard/src/__tests__/plugin-routes.test.ts
  • packages/dashboard/src/plugin-routes.ts

gsxdsm and others added 3 commits June 4, 2026 22:33
…tails

- Expand the directory-path follow-up section: PR #1428, 400 no-entry
  branch, heal-on-enable in both routers
- Document the vitest fs-mock vs externalized workspace-dep trap and
  the real-fs drift-guard test pattern
- Add Plugin Entry to CONCEPTS.md Plugins cluster

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…erge race

Main went red when the fast-tests quality-backfill projects (PR #1385)
landed alongside the workflow-columns stream (PR #1424) — the new test
projects were written against pre-stream code:

- TaskFieldsSection.css toggle knob used background: #fff, violating the
  theme-token assertion in AgentListModal's styling-parity test; use
  var(--card) per the SkillsView toggle convention
- ListView.test.tsx api mock lacked fetchBoardWorkflows (TaskDetailModal
  now calls it on mount)
- chat.test.ts and routes-agent-import.test.ts @fusion/core mocks lacked
  registerTraitHookImpl (engine merge-trait registers hooks at import)
- auto-merge-toggle-blank.mobile and board-mobile-initial-render used
  vi.runAllTimers(), which never terminates now that sse-bus starts a
  keepalive setInterval; use vi.runOnlyPendingTimers()

Both quality-backfill projects now pass fully: 7151/7151 across 414
files.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@gsxdsm
gsxdsm merged commit 7b961d6 into main Jun 5, 2026
10 checks passed
@gsxdsm
gsxdsm deleted the gsxdsm/compound-plugin branch July 24, 2026 06:08
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.

1 participant