
























1+// Covers the hosted OpenClaw marketplace feed refresh command.
2+import { beforeEach, describe, expect, it, vi } from "vitest";
3+4+const mocks = vi.hoisted(() => {
5+const defaultRuntime = {
6+error: vi.fn(),
7+exit: vi.fn((code: number) => {
8+throw new Error(`exit ${code}`);
9+}),
10+log: vi.fn(),
11+writeJson: vi.fn(),
12+};
13+return {
14+ defaultRuntime,
15+getRuntimeConfig: vi.fn(),
16+loadConfiguredHostedOfficialExternalPluginCatalogEntries: vi.fn(),
17+};
18+});
19+20+vi.mock("../config/config.js", () => ({
21+assertConfigWriteAllowedInCurrentMode: vi.fn(),
22+getRuntimeConfig: mocks.getRuntimeConfig,
23+readConfigFileSnapshot: vi.fn(),
24+replaceConfigFile: vi.fn(),
25+}));
26+27+vi.mock("../runtime.js", () => ({
28+defaultRuntime: mocks.defaultRuntime,
29+}));
30+31+vi.mock("../plugins/official-external-plugin-catalog.js", () => ({
32+loadConfiguredHostedOfficialExternalPluginCatalogEntries:
33+mocks.loadConfiguredHostedOfficialExternalPluginCatalogEntries,
34+}));
35+36+describe("plugins marketplace refresh", () => {
37+beforeEach(() => {
38+mocks.defaultRuntime.error.mockClear();
39+mocks.defaultRuntime.exit.mockClear();
40+mocks.defaultRuntime.log.mockClear();
41+mocks.defaultRuntime.writeJson.mockClear();
42+mocks.getRuntimeConfig.mockReset();
43+mocks.loadConfiguredHostedOfficialExternalPluginCatalogEntries.mockReset();
44+});
45+46+it("refreshes the configured marketplace feed and prints JSON", async () => {
47+const config = {
48+marketplaces: {
49+feeds: { acme: { url: "https://packages.acme.example/openclaw/feed" } },
50+},
51+};
52+mocks.getRuntimeConfig.mockReturnValue(config);
53+mocks.loadConfiguredHostedOfficialExternalPluginCatalogEntries.mockResolvedValue({
54+source: "hosted",
55+entries: [{ name: "@acme/calendar" }, { name: "@acme/docs" }],
56+feed: {
57+schemaVersion: 1,
58+id: "acme-marketplace",
59+generatedAt: "2026-06-23T00:00:00.000Z",
60+sequence: 7,
61+entries: [],
62+},
63+metadata: {
64+url: "https://packages.acme.example/openclaw/feed",
65+status: 200,
66+checksum: "feed-sha",
67+etag: '"abc"',
68+},
69+});
70+71+const { runPluginMarketplaceRefreshCommand } = await import("./plugins-cli.runtime.js");
72+await runPluginMarketplaceRefreshCommand({
73+feedProfile: "acme",
74+expectedSha256: "feed-sha",
75+json: true,
76+});
77+78+expect(mocks.loadConfiguredHostedOfficialExternalPluginCatalogEntries).toHaveBeenCalledWith(
79+config,
80+{ feedProfile: "acme", expectedSha256: "feed-sha", requireSnapshotWrite: true },
81+);
82+expect(mocks.defaultRuntime.writeJson).toHaveBeenCalledWith({
83+source: "hosted",
84+entries: 2,
85+feed: {
86+id: "acme-marketplace",
87+generatedAt: "2026-06-23T00:00:00.000Z",
88+sequence: 7,
89+},
90+metadata: {
91+url: "https://packages.acme.example/openclaw/feed",
92+status: 200,
93+checksum: "feed-sha",
94+etag: '"abc"',
95+},
96+});
97+});
98+99+it("normalizes bare SHA-256 pins before refreshing", async () => {
100+const config = {
101+marketplaces: {
102+feeds: { acme: { url: "https://packages.acme.example/openclaw/feed" } },
103+},
104+};
105+mocks.getRuntimeConfig.mockReturnValue(config);
106+mocks.loadConfiguredHostedOfficialExternalPluginCatalogEntries.mockResolvedValue({
107+source: "hosted",
108+entries: [{ name: "@acme/calendar" }],
109+feed: {
110+schemaVersion: 1,
111+id: "acme-marketplace",
112+generatedAt: "2026-06-23T00:00:00.000Z",
113+sequence: 7,
114+entries: [],
115+},
116+metadata: {
117+url: "https://packages.acme.example/openclaw/feed",
118+status: 200,
119+checksum: "sha256:abcdef",
120+},
121+});
122+123+const { runPluginMarketplaceRefreshCommand } = await import("./plugins-cli.runtime.js");
124+await runPluginMarketplaceRefreshCommand({
125+feedProfile: "acme",
126+expectedSha256: "ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789",
127+json: true,
128+});
129+130+expect(mocks.loadConfiguredHostedOfficialExternalPluginCatalogEntries).toHaveBeenCalledWith(
131+config,
132+{
133+feedProfile: "acme",
134+expectedSha256: "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
135+requireSnapshotWrite: true,
136+},
137+);
138+139+mocks.loadConfiguredHostedOfficialExternalPluginCatalogEntries.mockClear();
140+141+await runPluginMarketplaceRefreshCommand({
142+feedProfile: "acme",
143+expectedSha256: "sha256:ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789",
144+json: true,
145+});
146+147+expect(mocks.loadConfiguredHostedOfficialExternalPluginCatalogEntries).toHaveBeenCalledWith(
148+config,
149+{
150+feedProfile: "acme",
151+expectedSha256: "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
152+requireSnapshotWrite: true,
153+},
154+);
155+});
156+157+it("reports bundled fallback without failing the command", async () => {
158+mocks.getRuntimeConfig.mockReturnValue({});
159+mocks.loadConfiguredHostedOfficialExternalPluginCatalogEntries.mockResolvedValue({
160+source: "bundled-fallback",
161+entries: [{ name: "@openclaw/acpx" }],
162+error: "hosted catalog feed returned HTTP 503",
163+metadata: {
164+url: "https://clawhub.ai/v1/feeds/plugins",
165+status: 503,
166+},
167+});
168+169+const { runPluginMarketplaceRefreshCommand } = await import("./plugins-cli.runtime.js");
170+await runPluginMarketplaceRefreshCommand({});
171+172+const output = mocks.defaultRuntime.log.mock.calls.map(([value]) => String(value)).join("\n");
173+expect(output).toContain("bundled fallback");
174+expect(output).toContain("hosted catalog feed returned HTTP 503");
175+expect(mocks.defaultRuntime.exit).not.toHaveBeenCalled();
176+});
177+178+it("redacts query-bearing feed URLs from refresh output", async () => {
179+mocks.getRuntimeConfig.mockReturnValue({});
180+mocks.loadConfiguredHostedOfficialExternalPluginCatalogEntries.mockResolvedValue({
181+source: "bundled-fallback",
182+entries: [{ name: "@openclaw/acpx" }],
183+error:
184+"hosted catalog feed fetch failed for https://clawhub.ai/v1/feeds/plugins?token=secret#frag",
185+metadata: {
186+url: "https://clawhub.ai/v1/feeds/plugins?token=secret#frag",
187+status: 503,
188+},
189+});
190+191+const { runPluginMarketplaceRefreshCommand } = await import("./plugins-cli.runtime.js");
192+await runPluginMarketplaceRefreshCommand({
193+feedUrl: "https://clawhub.ai/v1/feeds/plugins?token=secret#frag",
194+json: true,
195+});
196+197+expect(mocks.defaultRuntime.writeJson).toHaveBeenCalledWith(
198+expect.objectContaining({
199+metadata: expect.objectContaining({ url: "https://clawhub.ai/v1/feeds/plugins" }),
200+error: "hosted catalog feed fetch failed for https://clawhub.ai/v1/feeds/plugins",
201+}),
202+);
203+204+mocks.defaultRuntime.writeJson.mockClear();
205+mocks.defaultRuntime.log.mockClear();
206+207+await runPluginMarketplaceRefreshCommand({
208+feedUrl: "https://clawhub.ai/v1/feeds/plugins?token=secret#frag",
209+});
210+211+const output = mocks.defaultRuntime.log.mock.calls.map(([value]) => String(value)).join("\n");
212+expect(output).toContain("https://clawhub.ai/v1/feeds/plugins");
213+expect(output).not.toContain("token=secret");
214+expect(output).not.toContain("#frag");
215+});
216+217+it("fails checksum-pinned refreshes that fall back", async () => {
218+mocks.getRuntimeConfig.mockReturnValue({});
219+mocks.loadConfiguredHostedOfficialExternalPluginCatalogEntries.mockResolvedValue({
220+source: "bundled-fallback",
221+entries: [{ name: "@openclaw/acpx" }],
222+error: "hosted catalog feed checksum mismatch: expected sha256:expected",
223+metadata: {
224+url: "https://clawhub.ai/v1/feeds/plugins",
225+status: 200,
226+checksum: "sha256:actual",
227+},
228+});
229+230+const { runPluginMarketplaceRefreshCommand } = await import("./plugins-cli.runtime.js");
231+await expect(
232+runPluginMarketplaceRefreshCommand({ expectedSha256: "sha256:expected", json: true }),
233+).rejects.toThrow("exit 1");
234+235+expect(mocks.defaultRuntime.writeJson).toHaveBeenCalledWith(
236+expect.objectContaining({ source: "bundled-fallback" }),
237+);
238+expect(mocks.defaultRuntime.error).toHaveBeenCalledWith(
239+"Pinned marketplace feed refresh did not accept a fresh hosted payload (source: bundled-fallback).",
240+);
241+expect(mocks.defaultRuntime.exit).toHaveBeenCalledWith(1);
242+});
243+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。