277 lines
12 KiB
TypeScript
Vendored
277 lines
12 KiB
TypeScript
Vendored
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { useComponentUpdates } from "@/composables/useComponentUpdates";
|
|
import { runPendingComponentUpdatePlan, type PendingComponentUpdates } from "@/lib/updates/componentUpdateOrchestration";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
listInstalledAgents: vi.fn(),
|
|
jdbcPluginStatus: vi.fn(),
|
|
checkMcpServerStatus: vi.fn(),
|
|
listPlugins: vi.fn(),
|
|
fetchPluginMarketplaceCatalogs: vi.fn(),
|
|
checkAgentUpdateBlockers: vi.fn(),
|
|
upgradeAllAgents: vi.fn(),
|
|
installJdbcPlugin: vi.fn(),
|
|
installMcpServer: vi.fn(),
|
|
installMarketplacePlugin: vi.fn(),
|
|
buildMarketplacePluginListings: vi.fn(),
|
|
}));
|
|
|
|
const settings = {
|
|
editorSettings: {
|
|
autoUpdateDrivers: true,
|
|
autoUpdateJdbc: true,
|
|
autoUpdateMcp: true,
|
|
autoUpdatePlugins: true,
|
|
},
|
|
};
|
|
|
|
vi.mock("@/lib/backend/api", () => mocks);
|
|
vi.mock("@/stores/settingsStore", () => ({ useSettingsStore: () => settings }));
|
|
vi.mock("@/i18n", () => ({ currentLocale: () => "zh-CN" }));
|
|
vi.mock("@/lib/plugins/pluginMarketplace", async (importOriginal) => ({ ...(await importOriginal<object>()), buildMarketplacePluginListings: mocks.buildMarketplacePluginListings }));
|
|
|
|
const mcpStatus = {
|
|
installed: true,
|
|
npm_available: true,
|
|
current_version: "1.0.0",
|
|
latest_version: "1.1.0",
|
|
update_available: true,
|
|
};
|
|
|
|
const pluginUpdate = {
|
|
status: "update",
|
|
artifact: { target: "universal", url: "https://example.com/plugin.dbxp", sha256: "hash" },
|
|
repository: { id: "official" },
|
|
plugin: { id: "example", latestVersion: "1.1.0" },
|
|
name: "Example plugin",
|
|
};
|
|
|
|
function deferred<T>() {
|
|
let resolve!: (value: T | PromiseLike<T>) => void;
|
|
const promise = new Promise<T>((resolvePromise) => {
|
|
resolve = resolvePromise;
|
|
});
|
|
return { promise, resolve };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
Object.assign(settings.editorSettings, {
|
|
autoUpdateDrivers: true,
|
|
autoUpdateJdbc: true,
|
|
autoUpdateMcp: true,
|
|
autoUpdatePlugins: true,
|
|
});
|
|
mocks.listInstalledAgents.mockResolvedValue([{ db_type: "mysql", update_available: true }]);
|
|
mocks.jdbcPluginStatus.mockResolvedValue({ installed: true, update_available: true });
|
|
mocks.checkMcpServerStatus.mockResolvedValue(mcpStatus);
|
|
mocks.listPlugins.mockResolvedValue([]);
|
|
mocks.fetchPluginMarketplaceCatalogs.mockResolvedValue([]);
|
|
mocks.buildMarketplacePluginListings.mockReturnValue([pluginUpdate]);
|
|
mocks.checkAgentUpdateBlockers.mockResolvedValue([]);
|
|
mocks.upgradeAllAgents.mockResolvedValue({ upgraded: 1, failed: [], cancelled: 0 });
|
|
mocks.installJdbcPlugin.mockResolvedValue({ installed: true, update_available: false });
|
|
mocks.installMcpServer.mockResolvedValue("updated");
|
|
mocks.installMarketplacePlugin.mockResolvedValue({});
|
|
});
|
|
|
|
describe("useComponentUpdates", () => {
|
|
it("counts agent, JDBC, MCP, and plugin updates", async () => {
|
|
const updates = useComponentUpdates({ isDesktop: true });
|
|
|
|
expect(await updates.refresh()).toBe(true);
|
|
expect(updates.driverUpdateCount.value).toBe(1);
|
|
expect(updates.jdbcUpdateAvailable.value).toBe(true);
|
|
expect(updates.mcpUpdateAvailable.value).toBe(true);
|
|
expect(updates.pluginUpdateCount.value).toBe(1);
|
|
expect(updates.totalUpdateCount.value).toBe(4);
|
|
});
|
|
|
|
it("forces a fresh snapshot when a management surface changes during an in-flight refresh", async () => {
|
|
const firstAgents = deferred<Array<{ db_type: string; update_available: boolean }>>();
|
|
const secondAgents = deferred<Array<{ db_type: string; update_available: boolean }>>();
|
|
mocks.listInstalledAgents.mockReturnValueOnce(firstAgents.promise).mockReturnValueOnce(secondAgents.promise);
|
|
const updates = useComponentUpdates({ isDesktop: true });
|
|
|
|
const initialRefresh = updates.refresh();
|
|
await Promise.resolve();
|
|
const forcedRefresh = updates.refresh({ force: true });
|
|
await Promise.resolve();
|
|
|
|
secondAgents.resolve([{ db_type: "mysql", update_available: false }]);
|
|
expect(await forcedRefresh).toBe(true);
|
|
firstAgents.resolve([{ db_type: "mysql", update_available: true }]);
|
|
expect(await initialRefresh).toBe(false);
|
|
|
|
expect(mocks.listInstalledAgents).toHaveBeenCalledTimes(2);
|
|
expect(updates.driverUpdateCount.value).toBe(0);
|
|
});
|
|
|
|
it("clears driver and MCP update state after a successful update and refresh", async () => {
|
|
mocks.listInstalledAgents.mockResolvedValueOnce([{ db_type: "mysql", update_available: true }]).mockResolvedValueOnce([{ db_type: "mysql", update_available: false }]);
|
|
mocks.checkMcpServerStatus.mockResolvedValueOnce(mcpStatus).mockResolvedValueOnce({ ...mcpStatus, latest_version: "1.0.0", update_available: false });
|
|
const updates = useComponentUpdates({ isDesktop: true });
|
|
|
|
await updates.installCategories(["drivers", "mcp"]);
|
|
|
|
expect(mocks.upgradeAllAgents).toHaveBeenCalledOnce();
|
|
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
|
|
expect(updates.driverUpdateCount.value).toBe(0);
|
|
expect(updates.mcpUpdateAvailable.value).toBe(false);
|
|
});
|
|
|
|
it("installs every enabled component category after an app update", async () => {
|
|
const updates = useComponentUpdates({ isDesktop: true });
|
|
|
|
const result = await updates.autoUpdateEnabledComponents();
|
|
|
|
expect(mocks.checkAgentUpdateBlockers).toHaveBeenCalledWith(["mysql"]);
|
|
expect(mocks.upgradeAllAgents).toHaveBeenCalledOnce();
|
|
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
|
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
|
|
expect(mocks.installMarketplacePlugin).toHaveBeenCalledWith({ repositoryId: "official", pluginId: "example", version: "1.1.0" });
|
|
expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [] });
|
|
});
|
|
|
|
it("shares one update operation between automatic and manual callers", async () => {
|
|
const jdbcInstall = deferred<{ installed: boolean; update_available: boolean }>();
|
|
mocks.installJdbcPlugin.mockReturnValue(jdbcInstall.promise);
|
|
const updates = useComponentUpdates({ isDesktop: true });
|
|
|
|
const automatic = updates.autoUpdateEnabledComponents();
|
|
await vi.waitFor(() => expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce());
|
|
const manual = updates.installCategory("jdbc");
|
|
|
|
expect(updates.updating.value).toBe(true);
|
|
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
|
|
|
jdbcInstall.resolve({ installed: true, update_available: false });
|
|
const [automaticResult, manualResult] = await Promise.all([automatic, manual]);
|
|
|
|
expect(manualResult).toEqual(automaticResult);
|
|
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
|
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
|
|
expect(mocks.installMarketplacePlugin).toHaveBeenCalledOnce();
|
|
expect(updates.updating.value).toBe(false);
|
|
});
|
|
|
|
it("still detects updates but does not install disabled categories", async () => {
|
|
Object.assign(settings.editorSettings, {
|
|
autoUpdateDrivers: false,
|
|
autoUpdateJdbc: false,
|
|
autoUpdateMcp: false,
|
|
autoUpdatePlugins: false,
|
|
});
|
|
const updates = useComponentUpdates({ isDesktop: true });
|
|
|
|
const result = await updates.autoUpdateEnabledComponents();
|
|
|
|
expect(updates.totalUpdateCount.value).toBe(4);
|
|
expect(mocks.upgradeAllAgents).not.toHaveBeenCalled();
|
|
expect(mocks.installJdbcPlugin).not.toHaveBeenCalled();
|
|
expect(mocks.installMcpServer).not.toHaveBeenCalled();
|
|
expect(mocks.installMarketplacePlugin).not.toHaveBeenCalled();
|
|
expect(result).toEqual({ drivers: 0, jdbc: false, mcp: false, plugins: 0, skippedDrivers: 0, failed: [] });
|
|
});
|
|
|
|
it("installs every manually selected category when no DBX update exists and automatic updates are disabled", async () => {
|
|
Object.assign(settings.editorSettings, {
|
|
autoUpdateDrivers: false,
|
|
autoUpdateJdbc: false,
|
|
autoUpdateMcp: false,
|
|
autoUpdatePlugins: false,
|
|
});
|
|
const updates = useComponentUpdates({ isDesktop: true });
|
|
|
|
const result = await updates.installCategories(["drivers", "jdbc", "mcp", "plugins"]);
|
|
|
|
expect(mocks.upgradeAllAgents).toHaveBeenCalledOnce();
|
|
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
|
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
|
|
expect(mocks.installMarketplacePlugin).toHaveBeenCalledOnce();
|
|
expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [] });
|
|
});
|
|
|
|
it("resumes a persisted manual update-all plan after restart while automatic updates are disabled", async () => {
|
|
Object.assign(settings.editorSettings, {
|
|
autoUpdateDrivers: false,
|
|
autoUpdateJdbc: false,
|
|
autoUpdateMcp: false,
|
|
autoUpdatePlugins: false,
|
|
});
|
|
const updates = useComponentUpdates({ isDesktop: true });
|
|
const pending: PendingComponentUpdates = {
|
|
fromVersion: "0.6.16",
|
|
targetVersion: "0.6.17",
|
|
plan: { kind: "manual", categories: ["drivers", "jdbc", "mcp", "plugins"] },
|
|
};
|
|
|
|
const result = await runPendingComponentUpdatePlan(pending, updates);
|
|
|
|
expect(mocks.upgradeAllAgents).toHaveBeenCalledOnce();
|
|
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
|
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
|
|
expect(mocks.installMarketplacePlugin).toHaveBeenCalledOnce();
|
|
expect(result).toEqual({ drivers: 1, jdbc: true, mcp: true, plugins: 1, skippedDrivers: 0, failed: [] });
|
|
});
|
|
|
|
it("skips blocked agent updates while allowing other component updates", async () => {
|
|
mocks.checkAgentUpdateBlockers.mockResolvedValue([{ db_type: "mysql", label: "MySQL" }]);
|
|
const updates = useComponentUpdates({ isDesktop: true });
|
|
|
|
const result = await updates.autoUpdateEnabledComponents();
|
|
|
|
expect(mocks.upgradeAllAgents).not.toHaveBeenCalled();
|
|
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
|
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
|
|
expect(mocks.installMarketplacePlugin).toHaveBeenCalledOnce();
|
|
expect(result.skippedDrivers).toBe(1);
|
|
});
|
|
|
|
it("does not install from stale state when update detection fails", async () => {
|
|
mocks.listInstalledAgents.mockRejectedValue(new Error("registry unavailable"));
|
|
mocks.jdbcPluginStatus.mockRejectedValue(new Error("JDBC registry unavailable"));
|
|
mocks.checkMcpServerStatus.mockRejectedValue(new Error("MCP registry unavailable"));
|
|
mocks.listPlugins.mockRejectedValue(new Error("plugin store unavailable"));
|
|
mocks.fetchPluginMarketplaceCatalogs.mockRejectedValue(new Error("catalog unavailable"));
|
|
const updates = useComponentUpdates({ isDesktop: true });
|
|
|
|
const result = await updates.autoUpdateEnabledComponents();
|
|
|
|
expect(result.failed).toEqual(["drivers: registry unavailable; JDBC: JDBC registry unavailable; MCP: MCP registry unavailable; plugins: plugin store unavailable; plugin catalog: catalog unavailable"]);
|
|
expect(mocks.upgradeAllAgents).not.toHaveBeenCalled();
|
|
expect(mocks.installJdbcPlugin).not.toHaveBeenCalled();
|
|
expect(mocks.installMcpServer).not.toHaveBeenCalled();
|
|
expect(mocks.installMarketplacePlugin).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("continues independent updates when the plugin catalog is unavailable", async () => {
|
|
mocks.fetchPluginMarketplaceCatalogs.mockRejectedValue(new Error("catalog unavailable"));
|
|
const updates = useComponentUpdates({ isDesktop: true });
|
|
|
|
const result = await updates.autoUpdateEnabledComponents();
|
|
|
|
expect(mocks.upgradeAllAgents).toHaveBeenCalledOnce();
|
|
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
|
expect(mocks.installMcpServer).toHaveBeenCalledOnce();
|
|
expect(mocks.installMarketplacePlugin).not.toHaveBeenCalled();
|
|
expect(result.failed).toEqual(["plugin catalog: catalog unavailable"]);
|
|
});
|
|
|
|
it("allows a category to be installed manually even when automatic updates are disabled", async () => {
|
|
Object.assign(settings.editorSettings, {
|
|
autoUpdateDrivers: false,
|
|
autoUpdateJdbc: false,
|
|
autoUpdateMcp: false,
|
|
autoUpdatePlugins: false,
|
|
});
|
|
const updates = useComponentUpdates({ isDesktop: true });
|
|
await updates.refresh();
|
|
|
|
const result = await updates.installCategory("jdbc");
|
|
|
|
expect(mocks.installJdbcPlugin).toHaveBeenCalledOnce();
|
|
expect(result).toEqual({ drivers: 0, jdbc: true, mcp: false, plugins: 0, skippedDrivers: 0, failed: [] });
|
|
});
|
|
});
|