




















@@ -38,6 +38,11 @@ vi.mock("./marketplace.js", () => ({
3838}));
39394040vi.mock("./clawhub.js", () => ({
41+CLAWHUB_INSTALL_ERROR_CODE: {
42+PACKAGE_NOT_FOUND: "package_not_found",
43+VERSION_NOT_FOUND: "version_not_found",
44+ARCHIVE_INTEGRITY_MISMATCH: "archive_integrity_mismatch",
45+},
4146installPluginFromClawHub: (...args: unknown[]) => installPluginFromClawHubMock(...args),
4247}));
4348@@ -73,6 +78,36 @@ function createSuccessfulNpmUpdateResult(params?: {
7378};
7479}
758081+function createSuccessfulClawHubUpdateResult(params?: {
82+pluginId?: string;
83+targetDir?: string;
84+version?: string;
85+clawhubPackage?: string;
86+}) {
87+return {
88+ok: true,
89+pluginId: params?.pluginId ?? "legacy-chat",
90+targetDir: params?.targetDir ?? "/tmp/openclaw-plugins/legacy-chat",
91+version: params?.version ?? "2026.5.1-beta.2",
92+extensions: ["index.ts"],
93+packageName: params?.clawhubPackage ?? "legacy-chat",
94+clawhub: {
95+source: "clawhub" as const,
96+clawhubUrl: "https://clawhub.ai",
97+clawhubPackage: params?.clawhubPackage ?? "legacy-chat",
98+clawhubFamily: "code-plugin" as const,
99+clawhubChannel: "official" as const,
100+version: params?.version ?? "2026.5.1-beta.2",
101+integrity: "sha256-clawpack",
102+resolvedAt: "2026-05-01T00:00:00.000Z",
103+clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
104+clawpackSpecVersion: 1,
105+clawpackManifestSha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
106+clawpackSize: 4096,
107+},
108+};
109+}
110+76111function createNpmInstallConfig(params: {
77112pluginId: string;
78113spec: string;
@@ -1481,6 +1516,7 @@ describe("updateNpmInstalledPlugins", () => {
14811516describe("syncPluginsForUpdateChannel", () => {
14821517beforeEach(() => {
14831518installPluginFromNpmSpecMock.mockReset();
1519+installPluginFromClawHubMock.mockReset();
14841520installPluginFromGitSpecMock.mockReset();
14851521resolveBundledPluginSourcesMock.mockReset();
14861522});
@@ -1662,6 +1698,193 @@ describe("syncPluginsForUpdateChannel", () => {
16621698});
16631699});
166417001701+it("installs a ClawHub-preferred externalized bundled plugin", async () => {
1702+resolveBundledPluginSourcesMock.mockReturnValue(new Map());
1703+installPluginFromClawHubMock.mockResolvedValue(
1704+createSuccessfulClawHubUpdateResult({
1705+pluginId: "legacy-chat",
1706+targetDir: "/tmp/openclaw-plugins/legacy-chat",
1707+version: "2026.5.1-beta.2",
1708+clawhubPackage: "legacy-chat",
1709+}),
1710+);
1711+1712+const result = await syncPluginsForUpdateChannel({
1713+channel: "stable",
1714+externalizedBundledPluginBridges: [
1715+{
1716+bundledPluginId: "legacy-chat",
1717+preferredSource: "clawhub",
1718+clawhubSpec: "clawhub:legacy-chat@2026.5.1-beta.2",
1719+clawhubUrl: "https://clawhub.ai",
1720+npmSpec: "@openclaw/legacy-chat",
1721+channelIds: ["legacy-chat"],
1722+},
1723+],
1724+config: {
1725+channels: {
1726+"legacy-chat": {
1727+enabled: true,
1728+},
1729+},
1730+plugins: {
1731+load: { paths: [appBundledPluginRoot("legacy-chat")] },
1732+installs: {
1733+"legacy-chat": {
1734+source: "path",
1735+sourcePath: appBundledPluginRoot("legacy-chat"),
1736+installPath: appBundledPluginRoot("legacy-chat"),
1737+},
1738+},
1739+},
1740+},
1741+});
1742+1743+expect(installPluginFromClawHubMock).toHaveBeenCalledWith(
1744+expect.objectContaining({
1745+spec: "clawhub:legacy-chat@2026.5.1-beta.2",
1746+baseUrl: "https://clawhub.ai",
1747+mode: "update",
1748+expectedPluginId: "legacy-chat",
1749+}),
1750+);
1751+expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
1752+expect(result.changed).toBe(true);
1753+expect(result.summary.switchedToClawHub).toEqual(["legacy-chat"]);
1754+expect(result.summary.switchedToNpm).toEqual([]);
1755+expect(result.summary.errors).toEqual([]);
1756+expect(result.config.plugins?.load?.paths).toEqual([]);
1757+expect(result.config.plugins?.installs?.["legacy-chat"]).toMatchObject({
1758+source: "clawhub",
1759+spec: "clawhub:legacy-chat@2026.5.1-beta.2",
1760+installPath: "/tmp/openclaw-plugins/legacy-chat",
1761+version: "2026.5.1-beta.2",
1762+integrity: "sha256-clawpack",
1763+clawhubUrl: "https://clawhub.ai",
1764+clawhubPackage: "legacy-chat",
1765+clawhubFamily: "code-plugin",
1766+clawhubChannel: "official",
1767+clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
1768+clawpackSpecVersion: 1,
1769+clawpackManifestSha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
1770+clawpackSize: 4096,
1771+});
1772+});
1773+1774+it("falls back from ClawHub to npm only when the ClawHub package is absent", async () => {
1775+resolveBundledPluginSourcesMock.mockReturnValue(new Map());
1776+installPluginFromClawHubMock.mockResolvedValue({
1777+ok: false,
1778+code: "package_not_found",
1779+error: "Package not found on ClawHub.",
1780+});
1781+installPluginFromNpmSpecMock.mockResolvedValue(
1782+createSuccessfulNpmUpdateResult({
1783+pluginId: "legacy-chat",
1784+targetDir: "/tmp/openclaw-plugins/legacy-chat",
1785+version: "2.0.0",
1786+}),
1787+);
1788+1789+const result = await syncPluginsForUpdateChannel({
1790+channel: "stable",
1791+externalizedBundledPluginBridges: [
1792+{
1793+bundledPluginId: "legacy-chat",
1794+preferredSource: "clawhub",
1795+clawhubSpec: "clawhub:legacy-chat@2026.5.1-beta.2",
1796+npmSpec: "@openclaw/legacy-chat",
1797+channelIds: ["legacy-chat"],
1798+},
1799+],
1800+config: {
1801+channels: {
1802+"legacy-chat": {
1803+enabled: true,
1804+},
1805+},
1806+plugins: {
1807+load: { paths: [appBundledPluginRoot("legacy-chat")] },
1808+installs: {
1809+"legacy-chat": {
1810+source: "path",
1811+sourcePath: appBundledPluginRoot("legacy-chat"),
1812+installPath: appBundledPluginRoot("legacy-chat"),
1813+},
1814+},
1815+},
1816+},
1817+});
1818+1819+expect(installPluginFromNpmSpecMock).toHaveBeenCalledWith(
1820+expect.objectContaining({
1821+spec: "@openclaw/legacy-chat",
1822+mode: "update",
1823+expectedPluginId: "legacy-chat",
1824+}),
1825+);
1826+expect(result.changed).toBe(true);
1827+expect(result.summary.switchedToClawHub).toEqual([]);
1828+expect(result.summary.switchedToNpm).toEqual(["legacy-chat"]);
1829+expect(result.summary.warnings).toEqual([
1830+"ClawHub clawhub:legacy-chat@2026.5.1-beta.2 unavailable for legacy-chat; falling back to npm @openclaw/legacy-chat.",
1831+]);
1832+expect(result.summary.errors).toEqual([]);
1833+expect(result.config.plugins?.installs?.["legacy-chat"]).toMatchObject({
1834+source: "npm",
1835+spec: "@openclaw/legacy-chat",
1836+installPath: "/tmp/openclaw-plugins/legacy-chat",
1837+version: "2.0.0",
1838+});
1839+});
1840+1841+it("fails closed without npm fallback when ClawHub returns integrity drift", async () => {
1842+resolveBundledPluginSourcesMock.mockReturnValue(new Map());
1843+installPluginFromClawHubMock.mockResolvedValue({
1844+ok: false,
1845+code: "archive_integrity_mismatch",
1846+error: "ClawHub ClawPack integrity mismatch.",
1847+});
1848+const config: OpenClawConfig = {
1849+channels: {
1850+"legacy-chat": {
1851+enabled: true,
1852+},
1853+},
1854+plugins: {
1855+load: { paths: [appBundledPluginRoot("legacy-chat")] },
1856+installs: {
1857+"legacy-chat": {
1858+source: "path",
1859+sourcePath: appBundledPluginRoot("legacy-chat"),
1860+installPath: appBundledPluginRoot("legacy-chat"),
1861+},
1862+},
1863+},
1864+};
1865+1866+const result = await syncPluginsForUpdateChannel({
1867+channel: "stable",
1868+externalizedBundledPluginBridges: [
1869+{
1870+bundledPluginId: "legacy-chat",
1871+preferredSource: "clawhub",
1872+clawhubSpec: "clawhub:legacy-chat@2026.5.1-beta.2",
1873+npmSpec: "@openclaw/legacy-chat",
1874+channelIds: ["legacy-chat"],
1875+},
1876+],
1877+ config,
1878+});
1879+1880+expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled();
1881+expect(result.changed).toBe(false);
1882+expect(result.config).toBe(config);
1883+expect(result.summary.errors).toEqual([
1884+"Failed to update legacy-chat: ClawHub ClawPack integrity mismatch. (ClawHub clawhub:legacy-chat@2026.5.1-beta.2).",
1885+]);
1886+});
1887+16651888it("externalizes bundled plugins that were enabled by default", async () => {
16661889resolveBundledPluginSourcesMock.mockReturnValue(new Map());
16671890installPluginFromNpmSpecMock.mockResolvedValue(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。