feat: add onboarding plugin install flow

This commit is contained in:
Peter Steinberger
2026-01-15 05:03:56 +00:00
parent 5abe3c2145
commit 568cc368ae
7 changed files with 478 additions and 12 deletions

View File

@@ -0,0 +1,126 @@
import path from "node:path";
import { beforeEach, describe, expect, it, vi } from "vitest";
vi.mock("node:fs", () => ({
default: {
existsSync: vi.fn(),
},
}));
const installPluginFromNpmSpec = vi.fn();
vi.mock("../../plugins/install.js", () => ({
installPluginFromNpmSpec: (...args: unknown[]) => installPluginFromNpmSpec(...args),
}));
vi.mock("../../plugins/loader.js", () => ({
loadClawdbotPlugins: vi.fn(),
}));
import fs from "node:fs";
import type { ChannelPluginCatalogEntry } from "../../channels/plugins/catalog.js";
import type { ClawdbotConfig } from "../../config/config.js";
import type { WizardPrompter } from "../../wizard/prompts.js";
import { makePrompter, makeRuntime } from "./__tests__/test-utils.js";
import { ensureOnboardingPluginInstalled } from "./plugin-install.js";
const baseEntry: ChannelPluginCatalogEntry = {
id: "zalo",
meta: {
id: "zalo",
label: "Zalo",
selectionLabel: "Zalo (Bot API)",
docsPath: "/channels/zalo",
docsLabel: "zalo",
blurb: "Test",
},
install: {
npmSpec: "@clawdbot/zalo",
localPath: "extensions/zalo",
},
};
beforeEach(() => {
vi.clearAllMocks();
});
describe("ensureOnboardingPluginInstalled", () => {
it("installs from npm and enables the plugin", async () => {
const runtime = makeRuntime();
const prompter = makePrompter({
select: vi.fn(async () => "npm") as WizardPrompter["select"],
});
const cfg: ClawdbotConfig = { plugins: { allow: ["other"] } };
vi.mocked(fs.existsSync).mockReturnValue(false);
installPluginFromNpmSpec.mockResolvedValue({
ok: true,
pluginId: "zalo",
targetDir: "/tmp/zalo",
extensions: [],
});
const result = await ensureOnboardingPluginInstalled({
cfg,
entry: baseEntry,
prompter,
runtime,
});
expect(result.installed).toBe(true);
expect(result.cfg.plugins?.entries?.zalo?.enabled).toBe(true);
expect(result.cfg.plugins?.allow).toContain("zalo");
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
expect.objectContaining({ spec: "@clawdbot/zalo" }),
);
});
it("uses local path when selected", async () => {
const runtime = makeRuntime();
const prompter = makePrompter({
select: vi.fn(async () => "local") as WizardPrompter["select"],
});
const cfg: ClawdbotConfig = {};
vi.mocked(fs.existsSync).mockReturnValue(true);
const result = await ensureOnboardingPluginInstalled({
cfg,
entry: baseEntry,
prompter,
runtime,
});
const expectedPath = path.resolve(process.cwd(), "extensions/zalo");
expect(result.installed).toBe(true);
expect(result.cfg.plugins?.load?.paths).toContain(expectedPath);
expect(result.cfg.plugins?.entries?.zalo?.enabled).toBe(true);
});
it("falls back to local path after npm install failure", async () => {
const runtime = makeRuntime();
const note = vi.fn(async () => {});
const confirm = vi.fn(async () => true);
const prompter = makePrompter({
select: vi.fn(async () => "npm") as WizardPrompter["select"],
note,
confirm,
});
const cfg: ClawdbotConfig = {};
vi.mocked(fs.existsSync).mockReturnValue(true);
installPluginFromNpmSpec.mockResolvedValue({
ok: false,
error: "nope",
});
const result = await ensureOnboardingPluginInstalled({
cfg,
entry: baseEntry,
prompter,
runtime,
});
const expectedPath = path.resolve(process.cwd(), "extensions/zalo");
expect(result.installed).toBe(true);
expect(result.cfg.plugins?.load?.paths).toContain(expectedPath);
expect(note).toHaveBeenCalled();
expect(runtime.error).not.toHaveBeenCalled();
});
});