

























@@ -72,6 +72,23 @@ vi.mock("../utils/with-timeout.js", () => ({
72727373import { ensureOnboardingPluginInstalled } from "./onboarding-plugin-install.js";
747475+function createDeferred<T>() {
76+let resolve!: (value: T) => void;
77+const promise = new Promise<T>((next) => {
78+resolve = next;
79+});
80+return { promise, resolve };
81+}
82+83+async function waitForMockCall(mock: { mock: { calls: unknown[][] } }) {
84+for (let i = 0; i < 20; i += 1) {
85+if (mock.mock.calls.length > 0) {
86+return;
87+}
88+await new Promise((resolve) => setTimeout(resolve, 0));
89+}
90+}
91+7592describe("ensureOnboardingPluginInstalled", () => {
7693beforeEach(() => {
7794vi.clearAllMocks();
@@ -241,6 +258,114 @@ describe("ensureOnboardingPluginInstalled", () => {
241258expect(refreshPluginRegistryAfterConfigMutation).not.toHaveBeenCalled();
242259});
243260261+it("animates ClawHub install progress while the remote install is running", async () => {
262+const deferred = createDeferred<Awaited<ReturnType<typeof installPluginFromClawHub>>>();
263+installPluginFromClawHub.mockImplementation(async (params) => {
264+params.logger?.info?.("Downloading demo-plugin from ClawHub…");
265+return await deferred.promise;
266+});
267+const stop = vi.fn();
268+const update = vi.fn();
269+270+const install = ensureOnboardingPluginInstalled({
271+cfg: {},
272+entry: {
273+pluginId: "demo-plugin",
274+label: "Demo Provider",
275+install: {
276+clawhubSpec: "clawhub:demo-plugin@2026.5.2",
277+defaultChoice: "clawhub",
278+},
279+},
280+prompter: {
281+select: vi.fn(async () => "clawhub"),
282+progress: vi.fn(() => ({ update, stop })),
283+} as never,
284+runtime: {} as never,
285+});
286+287+await waitForMockCall(installPluginFromClawHub);
288+expect(installPluginFromClawHub).toHaveBeenCalled();
289+290+await new Promise((resolve) => setTimeout(resolve, 250));
291+expect(update).toHaveBeenCalledWith("Downloading");
292+expect(
293+update.mock.calls.some(
294+([message]) =>
295+typeof message === "string" && /^Downloading {2}\[[█░]{16}\] \d+%$/u.test(message),
296+),
297+).toBe(true);
298+299+deferred.resolve({
300+ok: true,
301+pluginId: "demo-plugin",
302+targetDir: "/tmp/demo-plugin",
303+version: "2026.5.2",
304+packageName: "demo-plugin",
305+clawhub: {
306+source: "clawhub",
307+clawhubUrl: "https://clawhub.ai",
308+clawhubPackage: "demo-plugin",
309+clawhubFamily: "code-plugin",
310+clawhubChannel: "official",
311+version: "2026.5.2",
312+integrity: "sha256-clawpack",
313+resolvedAt: "2026-05-02T00:00:00.000Z",
314+clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
315+clawpackSpecVersion: 1,
316+clawpackManifestSha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
317+clawpackSize: 4096,
318+},
319+});
320+await install;
321+});
322+323+it("animates npm install progress while the remote install is running", async () => {
324+const deferred = createDeferred<Awaited<ReturnType<typeof installPluginFromNpmSpec>>>();
325+installPluginFromNpmSpec.mockImplementation(async (params) => {
326+params.logger?.info?.("Resolving npm package…");
327+return await deferred.promise;
328+});
329+const stop = vi.fn();
330+const update = vi.fn();
331+332+const install = ensureOnboardingPluginInstalled({
333+cfg: {},
334+entry: {
335+pluginId: "demo-plugin",
336+label: "Demo Plugin",
337+install: {
338+npmSpec: "@demo/plugin@1.2.3",
339+},
340+},
341+prompter: {
342+select: vi.fn(async () => "npm"),
343+progress: vi.fn(() => ({ update, stop })),
344+} as never,
345+runtime: {} as never,
346+});
347+348+await waitForMockCall(installPluginFromNpmSpec);
349+expect(installPluginFromNpmSpec).toHaveBeenCalled();
350+351+await new Promise((resolve) => setTimeout(resolve, 250));
352+expect(update).toHaveBeenCalledWith("Resolving");
353+expect(
354+update.mock.calls.some(
355+([message]) =>
356+typeof message === "string" && /^Resolving {2}\[[█░]{16}\] \d+%$/u.test(message),
357+),
358+).toBe(true);
359+360+deferred.resolve({
361+ok: true,
362+pluginId: "demo-plugin",
363+targetDir: "/tmp/demo-plugin",
364+version: "1.2.3",
365+});
366+await install;
367+});
368+244369it("returns a timed out status and notes the retry path when npm install hangs", async () => {
245370const note = vi.fn(async () => {});
246371const stop = vi.fn();
@@ -310,7 +435,7 @@ describe("ensureOnboardingPluginInstalled", () => {
310435});
311436312437expect(captured?.options).toEqual([
313-{ value: "npm", label: "Download from npm (@demo/plugin)" },
438+{ value: "npm", label: "Remote install from npm (@demo/plugin)" },
314439{ value: "skip", label: "Skip for now" },
315440]);
316441expect(captured?.initialValue).toBe("npm");
@@ -349,8 +474,11 @@ describe("ensureOnboardingPluginInstalled", () => {
349474});
350475351476expect(captured?.options).toEqual([
352-{ value: "clawhub", label: "Download from ClawHub (clawhub:demo-plugin@2026.5.2)" },
353-{ value: "npm", label: "Download from npm (@openclaw/demo-plugin@2026.5.2)" },
477+{
478+value: "clawhub",
479+label: "Remote install from ClawHub (clawhub:demo-plugin@2026.5.2)",
480+},
481+{ value: "npm", label: "Remote install from npm (@openclaw/demo-plugin@2026.5.2)" },
354482{ value: "skip", label: "Skip for now" },
355483]);
356484expect(captured?.initialValue).toBe("clawhub");
@@ -460,7 +588,7 @@ describe("ensureOnboardingPluginInstalled", () => {
460588expect(captured).toBeDefined();
461589expect(captured?.message).toBe("Install Demo Plugin\\n plugin?");
462590expect(captured?.options).toEqual([
463-{ value: "npm", label: "Download from npm (@demo/plugin@1.2.3)" },
591+{ value: "npm", label: "Remote install from npm (@demo/plugin@1.2.3)" },
464592{
465593value: "local",
466594label: "Use local plugin path",
@@ -674,7 +802,7 @@ describe("ensureOnboardingPluginInstalled", () => {
674802});
675803676804expect(captured).toBeDefined();
677-// "Download from npm (@openclaw/tlon)" must NOT appear: the bundled
805+// "Remote install from npm (@openclaw/tlon)" must NOT appear: the bundled
678806// copy is what gets enabled, so the npm hint would only confuse
679807// users into thinking the plugin is missing.
680808expect(captured?.options).toEqual([
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。