Extend plugin registration-drift learning with entry-file contract details
- 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>
This commit is contained in:
@@ -141,6 +141,10 @@ A plugin that ships inside the Fusion distribution itself rather than being inst
|
||||
*Avoid:* built-in plugin (as a distinct concept; the Settings label uses "Built-in" for the same thing)
|
||||
|
||||
A Bundled Plugin must be registered in several independently maintained surfaces — the Settings catalog, the dashboard server's bundled-id fallback set, the CLI's startup auto-install list, and the build step that stages a loadable copy into the distribution. The surfaces do not cross-check each other: a plugin registered in some but not all appears installable yet fails to install or load, so adding one means mirroring an existing bundled plugin across every surface.
|
||||
|
||||
### Plugin Entry
|
||||
The single loadable file persisted as a plugin's path and dynamically imported by the loader. The contract is strict: a package directory is never a valid entry (ESM cannot import directories), so every install surface must resolve a concrete file before persisting, preferring the shipped bundle, then a prebuilt output, then raw workspace source. Legacy registrations that stored a directory are healed in place — re-pointed at a resolved entry — the next time the plugin is enabled or auto-installed.
|
||||
|
||||
## Workflow columns & traits
|
||||
|
||||
*Behind the `experimentalFeatures.workflowColumns` flag. With the flag off, the legacy fixed pipeline (the closed column enum + `VALID_TRANSITIONS`) is authoritative and unchanged.*
|
||||
|
||||
@@ -12,7 +12,8 @@ symptoms:
|
||||
root_cause: incomplete_setup
|
||||
resolution_type: code_fix
|
||||
severity: medium
|
||||
tags: [plugins, bundled-plugins, settings, install, tsup, registration-drift]
|
||||
last_updated: 2026-06-05
|
||||
tags: [plugins, bundled-plugins, settings, install, tsup, registration-drift, entry-file, fs-mock]
|
||||
---
|
||||
|
||||
# Bundled plugins must be registered in 4 independent places — they drift
|
||||
@@ -65,7 +66,11 @@ await bundlePluginEntry({
|
||||
|
||||
## Follow-up failure: directory registered as plugin path
|
||||
|
||||
Fixing the fallback surfaced a second, independent bug: both dashboard 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) — enable then failed with `Plugin entry must be a file, got directory: <dir>`. Only the CLI startup path had been migrated to `resolvePluginEntryPath` (`bundled.js` → `dist/index.js` → `src/index.ts`), which is why CLI-auto-installed plugins worked and Settings-installed ones never did. Fix: the install routes now resolve and register the entry file (helper added to `@fusion/core`), and the enable route heals legacy directory-path rows in place — mirroring the CLI's startup heal.
|
||||
Fixing the fallback surfaced a second, independent bug (fixed in PR #1428): both dashboard 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) — enable then failed with `Plugin entry must be a file, got directory: <dir>`. Only the CLI startup path had been migrated to `resolvePluginEntryPath` (`bundled.js` → `dist/index.js` → `src/index.ts`), which is why CLI-auto-installed plugins worked and Settings-installed ones never did. Fix: both install routes now resolve and register the entry file (helper added to `@fusion/core`; 400 with "no loadable entry file" when none exists), and **both** enable routes heal legacy directory-path rows in place before `loadPlugin` — mirroring the CLI's startup heal — so pre-fix broken registrations self-repair on first enable without a migration.
|
||||
|
||||
### Trap: vitest fs mocks don't reach externalized workspace deps
|
||||
|
||||
Moving `resolvePluginEntryPath` to `@fusion/core` and re-exporting from the CLI broke the CLI's tests: `vi.mock("node:fs")` in the CLI package does **not** intercept fs calls made inside the externalized `@fusion/core` import (vitest only inlines/mocks modules in the test package's transform graph — the dashboard package inlines core, the CLI doesn't). Resolution: the CLI keeps an intentionally duplicated local copy (its fs mocks work against it), both copies carry keep-in-sync comments, and a **real-fs drift-guard test** (`packages/cli/src/plugins/__tests__/resolve-plugin-entry-path-sync.test.ts`) imports both copies and asserts identical resolution across real temp-dir layouts — each candidate alone, precedence pairs, all three, and the no-entry → `null` case. Real directories are the only seam that exercises both implementations equally; a candidate-list change applied to one copy but not the other now fails CI.
|
||||
|
||||
## Why This Works
|
||||
|
||||
@@ -75,9 +80,13 @@ The Settings card sends a relative `./plugins/<id>` path. The server resolves it
|
||||
|
||||
- **When adding a bundled plugin, grep for an existing one** (e.g. `rg -l "fusion-plugin-roadmap" packages/` ) and mirror every hit — that surfaces all four lists plus view registration.
|
||||
- Route tests must force the fallback: mock fs so the cwd-relative path **misses** and only `dist/plugins/<id>` exists (see "installs bundled compound engineering plugin when relative path misses cwd" in `packages/dashboard/src/__tests__/plugin-routes.test.ts`). A mock that matches any path containing the plugin id tests nothing.
|
||||
- **Pin assertions to the exact contract, not substring containment.** `stringContaining(pluginId)` passed for both the correct entry-file path and the buggy directory path — when a mock or matcher can satisfy both the correct and the buggy value, the test proves nothing. Route tests now assert the registered path ends in an entry-file suffix, cover the `dist/index.js` and `src/index.ts` fallbacks, and the 400 no-entry branch.
|
||||
- When duplicating a helper is forced by test infrastructure (fs mocks vs externalized deps), add a real-fs drift-guard test that runs every copy against the same on-disk fixtures and asserts identical output.
|
||||
- Consider a future consistency test asserting every `BUILTIN_PLUGINS` UI entry with a `path` is present in both server-side `BUNDLED_PLUGIN_IDS` sets.
|
||||
|
||||
## Related Issues
|
||||
|
||||
- PR #1423 — the fix
|
||||
- PR #1423 — the registration-drift fix
|
||||
- PR #1428 — the entry-file/heal follow-up fix
|
||||
- Issue #1096 — same Settings-install bundled-plugin failure family (missing-bundle symptom for the Paperclip runtime in global npm installs); different root cause
|
||||
- Commit `ff0750cd1` — added CE/Roadmap to the UI list (2 of 4 registrations)
|
||||
|
||||
Reference in New Issue
Block a user