























@@ -59,6 +59,7 @@ vi.mock("./clawhub.js", () => ({
5959CLAWHUB_INSTALL_ERROR_CODE: {
6060PACKAGE_NOT_FOUND: "package_not_found",
6161VERSION_NOT_FOUND: "version_not_found",
62+ARTIFACT_UNAVAILABLE: "artifact_unavailable",
6263ARCHIVE_INTEGRITY_MISMATCH: "archive_integrity_mismatch",
6364ARTIFACT_DOWNLOAD_UNAVAILABLE: "artifact_download_unavailable",
6465},
@@ -2292,6 +2293,226 @@ describe("updateNpmInstalledPlugins", () => {
22922293);
22932294});
229422952296+it("falls back to npm for trusted official ClawHub artifact blocks", async () => {
2297+const warnMessages: string[] = [];
2298+const installPath = createInstalledPackageDir({
2299+name: "@openclaw/discord",
2300+version: "2026.5.12",
2301+});
2302+installPluginFromClawHubMock.mockResolvedValueOnce({
2303+ok: false,
2304+code: "artifact_unavailable",
2305+error:
2306+'ClawHub artifact download for "@openclaw/discord@2026.5.16-beta.5" is not available yet (ClawHub /api/v1/packages/%40openclaw%2Fdiscord/versions/2026.5.16-beta.5/artifact/download failed (403): Blocked: this package release has been flagged as malicious and cannot be downloaded.). Use "npm:@openclaw/discord@2026.5.16-beta.5" for launch installs while ClawHub artifact routing is being rolled out.',
2307+});
2308+installPluginFromNpmSpecMock.mockResolvedValueOnce(
2309+createSuccessfulNpmUpdateResult({
2310+pluginId: "discord",
2311+targetDir: "/tmp/openclaw-plugins/discord",
2312+version: "2026.5.16-beta.5",
2313+npmResolution: {
2314+name: "@openclaw/discord",
2315+version: "2026.5.16-beta.5",
2316+resolvedSpec: "@openclaw/discord@2026.5.16-beta.5",
2317+},
2318+}),
2319+);
2320+2321+const result = await updateNpmInstalledPlugins({
2322+config: createClawHubInstallConfig({
2323+pluginId: "discord",
2324+ installPath,
2325+clawhubUrl: "https://clawhub.ai",
2326+clawhubPackage: "@openclaw/discord",
2327+clawhubFamily: "code-plugin",
2328+clawhubChannel: "official",
2329+spec: "clawhub:@openclaw/discord",
2330+}),
2331+pluginIds: ["discord"],
2332+updateChannel: "beta",
2333+disableOnFailure: true,
2334+logger: { warn: (msg) => warnMessages.push(msg) },
2335+});
2336+2337+expect(clawHubInstallCall()?.spec).toBe("clawhub:@openclaw/discord@beta");
2338+expect(npmInstallCall()?.spec).toBe("@openclaw/discord@beta");
2339+expect(npmInstallCall()?.expectedPluginId).toBe("discord");
2340+expect(npmInstallCall()?.trustedSourceLinkedOfficialInstall).toBe(true);
2341+expect(result.config.plugins?.entries?.discord?.enabled).toBeUndefined();
2342+expectRecordFields(result.config.plugins?.installs?.discord, {
2343+source: "npm",
2344+spec: "@openclaw/discord",
2345+installPath: "/tmp/openclaw-plugins/discord",
2346+version: "2026.5.16-beta.5",
2347+});
2348+expect(result.config.plugins?.installs?.discord?.clawhubPackage).toBeUndefined();
2349+expect(result.config.plugins?.installs?.discord?.clawhubUrl).toBeUndefined();
2350+expect(result.config.plugins?.installs?.discord?.artifactKind).toBeUndefined();
2351+expect(result.outcomes).toEqual([
2352+{
2353+pluginId: "discord",
2354+status: "updated",
2355+currentVersion: "2026.5.12",
2356+nextVersion: "2026.5.16-beta.5",
2357+message:
2358+"Updated discord: 2026.5.12 -> 2026.5.16-beta.5. (warning: official ClawHub artifact fallback used @openclaw/discord@beta).",
2359+},
2360+]);
2361+expect(warnMessages).toEqual([
2362+'Plugin "discord" could not download official ClawHub artifact for clawhub:@openclaw/discord@beta; using npm @openclaw/discord@beta instead. Core update can still complete.',
2363+]);
2364+});
2365+2366+it("uses the default npm spec when beta ClawHub falls back before an artifact block", async () => {
2367+const warnMessages: string[] = [];
2368+const installPath = createInstalledPackageDir({
2369+name: "@openclaw/discord",
2370+version: "2026.5.12",
2371+});
2372+installPluginFromClawHubMock
2373+.mockResolvedValueOnce({
2374+ok: false,
2375+code: "version_not_found",
2376+error: "version not found: beta",
2377+})
2378+.mockResolvedValueOnce({
2379+ok: false,
2380+code: "artifact_unavailable",
2381+error: "artifact unavailable",
2382+});
2383+installPluginFromNpmSpecMock.mockResolvedValueOnce(
2384+createSuccessfulNpmUpdateResult({
2385+pluginId: "discord",
2386+targetDir: "/tmp/openclaw-plugins/discord",
2387+version: "2026.5.16",
2388+npmResolution: {
2389+name: "@openclaw/discord",
2390+version: "2026.5.16",
2391+resolvedSpec: "@openclaw/discord@2026.5.16",
2392+},
2393+}),
2394+);
2395+2396+const result = await updateNpmInstalledPlugins({
2397+config: createClawHubInstallConfig({
2398+pluginId: "discord",
2399+ installPath,
2400+clawhubUrl: "https://clawhub.ai",
2401+clawhubPackage: "@openclaw/discord",
2402+clawhubFamily: "code-plugin",
2403+clawhubChannel: "official",
2404+spec: "clawhub:@openclaw/discord",
2405+}),
2406+pluginIds: ["discord"],
2407+updateChannel: "beta",
2408+logger: { warn: (msg) => warnMessages.push(msg) },
2409+});
2410+2411+expect(clawHubInstallCall(0)?.spec).toBe("clawhub:@openclaw/discord@beta");
2412+expect(clawHubInstallCall(1)?.spec).toBe("clawhub:@openclaw/discord");
2413+expect(npmInstallCall()?.spec).toBe("@openclaw/discord");
2414+expectRecordFields(result.config.plugins?.installs?.discord, {
2415+source: "npm",
2416+spec: "@openclaw/discord",
2417+installPath: "/tmp/openclaw-plugins/discord",
2418+version: "2026.5.16",
2419+});
2420+expect(result.outcomes[0]?.message).toBe(
2421+"Updated discord: 2026.5.12 -> 2026.5.16. (warning: official ClawHub artifact fallback used @openclaw/discord).",
2422+);
2423+expect(warnMessages).toEqual([
2424+'Plugin "discord" has no beta ClawHub release for clawhub:@openclaw/discord@beta; using clawhub:@openclaw/discord instead. Core update can still complete.',
2425+'Plugin "discord" could not download official ClawHub artifact for clawhub:@openclaw/discord; using npm @openclaw/discord instead. Core update can still complete.',
2426+]);
2427+});
2428+2429+it("reports npm dry-run versions for trusted official ClawHub artifact fallback", async () => {
2430+const installPath = createInstalledPackageDir({
2431+name: "@openclaw/discord",
2432+version: "2026.5.16-beta.5",
2433+});
2434+installPluginFromClawHubMock.mockResolvedValueOnce({
2435+ok: false,
2436+code: "artifact_unavailable",
2437+error: "artifact unavailable",
2438+});
2439+installPluginFromNpmSpecMock.mockResolvedValueOnce({
2440+ok: true,
2441+pluginId: "discord",
2442+targetDir: "/tmp/openclaw-plugins/discord",
2443+extensions: [],
2444+npmResolution: {
2445+name: "@openclaw/discord",
2446+version: "2026.5.16-beta.5",
2447+resolvedSpec: "@openclaw/discord@2026.5.16-beta.5",
2448+},
2449+});
2450+2451+const result = await updateNpmInstalledPlugins({
2452+config: createClawHubInstallConfig({
2453+pluginId: "discord",
2454+ installPath,
2455+clawhubUrl: "https://clawhub.ai",
2456+clawhubPackage: "@openclaw/discord",
2457+clawhubFamily: "code-plugin",
2458+clawhubChannel: "official",
2459+spec: "clawhub:@openclaw/discord",
2460+}),
2461+pluginIds: ["discord"],
2462+updateChannel: "beta",
2463+dryRun: true,
2464+});
2465+2466+expect(npmInstallCall()?.spec).toBe("@openclaw/discord@beta");
2467+expect(npmInstallCall()?.dryRun).toBe(true);
2468+expect(result.outcomes).toEqual([
2469+{
2470+pluginId: "discord",
2471+status: "unchanged",
2472+currentVersion: "2026.5.16-beta.5",
2473+nextVersion: "2026.5.16-beta.5",
2474+message:
2475+"discord is up to date (2026.5.16-beta.5). (warning: official ClawHub artifact fallback would use @openclaw/discord@beta).",
2476+},
2477+]);
2478+});
2479+2480+it("does not fall back to trusted npm from custom ClawHub provenance", async () => {
2481+const installPath = createInstalledPackageDir({
2482+name: "@openclaw/discord",
2483+version: "2026.5.12",
2484+});
2485+installPluginFromClawHubMock.mockResolvedValueOnce({
2486+ok: false,
2487+code: "artifact_unavailable",
2488+error: "artifact unavailable",
2489+});
2490+2491+const result = await updateNpmInstalledPlugins({
2492+config: createClawHubInstallConfig({
2493+pluginId: "discord",
2494+ installPath,
2495+clawhubUrl: "https://custom-clawhub.example",
2496+clawhubPackage: "@openclaw/discord",
2497+clawhubFamily: "code-plugin",
2498+clawhubChannel: "official",
2499+spec: "clawhub:@openclaw/discord",
2500+}),
2501+pluginIds: ["discord"],
2502+updateChannel: "beta",
2503+});
2504+2505+expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
2506+expect(result.outcomes).toEqual([
2507+{
2508+pluginId: "discord",
2509+status: "error",
2510+message:
2511+"Failed to update discord: artifact unavailable (ClawHub clawhub:@openclaw/discord@beta).",
2512+},
2513+]);
2514+});
2515+22952516it("preserves explicit ClawHub tags when updating on the beta channel", async () => {
22962517installPluginFromClawHubMock.mockResolvedValue(
22972518createSuccessfulClawHubUpdateResult({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。