07038018ac
* fix(plugins): unload previous version before reloading on plugin update The install/update path reloaded a plugin without first unloading the previous version's registrations, unlike disable/uninstall. Because the slot registry only appends, this ran initialize() twice and left two main-top-bar slot entries, duplicating the plugin's widget icon until a page refresh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(plugins): cache-bust plugin bundle import so updates re-execute unloadPlugin(id, {evictCache: true}) drops the cached bundle registration on install/update, but a same-tab re-import() of the identical bundle URL returns the browser's already-evaluated ES module without re-running its top-level registerKandevPlugin() call — leaving an updated plugin active but missing from the UI. Key the bundle URL on the plugin's installed version (?v=<version>) on both the frontend (toActivePlugin) and the backend boot payload (bootActivePlugins) so a version bump forces a real re-import while an unchanged version stays cache-friendly across boots. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(plugins): update bundleUrl assertions for cache-bust query param toActivePlugin now appends a ?v=<version> cache-busting query param to bundleUrl; these two assertions still expected the bare path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(plugins): only evict bundle cache on install/update, not on enable loadIfActive unconditionally evicted the cached bundle registration, which broke the disable->enable cycle: since bundleUrl doesn't change on plain enable, the browser's ES module cache returned the already-evaluated module without re-running registerKandevPlugin, leaving the plugin's slots/routes unregistered until a full page refresh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
72 lines
2.6 KiB
TypeScript
72 lines
2.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { toActivePlugin } from "./active-plugin";
|
|
import type { PluginRecord } from "@/lib/types/plugins";
|
|
|
|
function record(overrides: Partial<PluginRecord> = {}): PluginRecord {
|
|
return {
|
|
id: "acme-tools",
|
|
api_version: 1,
|
|
version: "1.0.0",
|
|
display_name: "Acme Tools",
|
|
description: "",
|
|
author: "",
|
|
categories: [],
|
|
capabilities: {},
|
|
status: "active",
|
|
install_path: "/home/user/.kandev/plugins/acme-tools/1.0.0",
|
|
signed: true,
|
|
installed_at: "2026-01-01T00:00:00Z",
|
|
restart_count: 0,
|
|
ui: { bundle: "/ui/bundle.js" },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("toActivePlugin", () => {
|
|
it("builds the bundle URL from the plugin id and version, mirroring the backend's boot payload mapping", () => {
|
|
const result = toActivePlugin(record());
|
|
expect(result).not.toBeNull();
|
|
expect(result?.bundleUrl).toBe("/api/plugins/acme-tools/bundle?v=1.0.0");
|
|
expect(result?.id).toBe("acme-tools");
|
|
expect(result?.name).toBe("Acme Tools");
|
|
});
|
|
|
|
it("appends the plugin's version as a cache-busting query param, so an updated version resolves to a new module specifier", () => {
|
|
const before = toActivePlugin(record({ version: "1.0.0" }));
|
|
const after = toActivePlugin(record({ version: "1.0.1" }));
|
|
expect(before?.bundleUrl).not.toBe(after?.bundleUrl);
|
|
expect(after?.bundleUrl).toBe("/api/plugins/acme-tools/bundle?v=1.0.1");
|
|
});
|
|
|
|
it("resolves to the identical bundleUrl on a plain unchanged reload (no needless cache-busting)", () => {
|
|
const first = toActivePlugin(record({ version: "1.0.0" }));
|
|
const second = toActivePlugin(record({ version: "1.0.0" }));
|
|
expect(first?.bundleUrl).toBe(second?.bundleUrl);
|
|
});
|
|
|
|
it("maps ui.styles to /api/plugins/:id/ui/:style, mirroring pluginStyleURLs on the backend", () => {
|
|
const result = toActivePlugin(
|
|
record({ ui: { bundle: "/ui/bundle.js", styles: ["/ui/a.css", "/ui/b.css"] } }),
|
|
);
|
|
expect(result?.styleUrls).toEqual([
|
|
"/api/plugins/acme-tools/ui/ui/a.css",
|
|
"/api/plugins/acme-tools/ui/ui/b.css",
|
|
]);
|
|
});
|
|
|
|
it("omits styleUrls when the plugin declares no styles", () => {
|
|
const result = toActivePlugin(record({ ui: { bundle: "/ui/bundle.js" } }));
|
|
expect(result?.styleUrls).toBeUndefined();
|
|
});
|
|
|
|
it("returns null when the plugin declares no UI bundle", () => {
|
|
const result = toActivePlugin(record({ ui: undefined }));
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it("returns null when ui.bundle is an empty string", () => {
|
|
const result = toActivePlugin(record({ ui: { bundle: "" } }));
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|