


























11// ClawHub lifecycle tests cover registry metadata lookup and error handling.
2+import { createHash } from "node:crypto";
23import fs from "node:fs/promises";
34import os from "node:os";
45import path from "node:path";
5-import { beforeEach, describe, expect, it, vi } from "vitest";
6+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7+import { createTrackedTempDirs } from "../../test-utils/tracked-temp-dirs.js";
6879const fetchClawHubSkillDetailMock = vi.fn();
810const fetchClawHubSkillInstallResolutionMock = vi.fn();
11+const fetchClawHubSkillVerificationMock = vi.fn();
912const downloadClawHubSkillArchiveMock = vi.fn();
1013const downloadClawHubSkillArchiveUrlMock = vi.fn();
1114const downloadClawHubGitHubSkillArchiveMock = vi.fn();
@@ -19,10 +22,12 @@ const withExtractedArchiveRootMock = vi.fn();
1922const installPackageDirMock = vi.fn();
2023const evaluateSkillInstallPolicyMock = vi.fn();
2124const pathExistsMock = vi.fn();
25+const tempDirs = createTrackedTempDirs();
22262327vi.mock("../../infra/clawhub.js", () => ({
2428fetchClawHubSkillDetail: fetchClawHubSkillDetailMock,
2529fetchClawHubSkillInstallResolution: fetchClawHubSkillInstallResolutionMock,
30+fetchClawHubSkillVerification: fetchClawHubSkillVerificationMock,
2631downloadClawHubSkillArchive: downloadClawHubSkillArchiveMock,
2732downloadClawHubSkillArchiveUrl: downloadClawHubSkillArchiveUrlMock,
2833downloadClawHubGitHubSkillArchive: downloadClawHubGitHubSkillArchiveMock,
@@ -162,9 +167,14 @@ async function writeClawHubOriginFixture(params: {
162167}
163168164169describe("skills-clawhub", () => {
170+afterEach(async () => {
171+await tempDirs.cleanup();
172+});
173+165174beforeEach(() => {
166175fetchClawHubSkillDetailMock.mockReset();
167176fetchClawHubSkillInstallResolutionMock.mockReset();
177+fetchClawHubSkillVerificationMock.mockReset();
168178downloadClawHubSkillArchiveMock.mockReset();
169179downloadClawHubSkillArchiveUrlMock.mockReset();
170180downloadClawHubGitHubSkillArchiveMock.mockReset();
@@ -205,19 +215,36 @@ describe("skills-clawhub", () => {
205215downloadUrl: "https://clawhub.ai/api/v1/download?slug=agentreceipt&version=1.0.0",
206216},
207217});
218+fetchClawHubSkillVerificationMock.mockResolvedValue({
219+schema: "clawhub.skill.verify.v1",
220+ok: true,
221+decision: "pass",
222+reasons: [],
223+card: { available: true, sha256: "card-sha" },
224+artifact: { sourceFingerprint: "source-fp" },
225+provenance: { source: "unavailable" },
226+security: { status: "clean", signals: { staticScan: { engineVersion: "v2.4.24" } } },
227+signature: { status: "unsigned" },
228+});
208229downloadClawHubSkillArchiveMock.mockResolvedValue({
209230archivePath: "/tmp/agentreceipt.zip",
210231integrity: "sha256-test",
232+sha256Hex: "a".repeat(64),
233+artifact: "archive",
211234cleanup: archiveCleanupMock,
212235});
213236downloadClawHubSkillArchiveUrlMock.mockResolvedValue({
214237archivePath: "/tmp/agentreceipt.zip",
215238integrity: "sha256-test",
239+sha256Hex: "a".repeat(64),
240+artifact: "archive",
216241cleanup: archiveCleanupMock,
217242});
218243downloadClawHubGitHubSkillArchiveMock.mockResolvedValue({
219244archivePath: "/tmp/github-agentreceipt.zip",
220245integrity: "sha256-github-test",
246+sha256Hex: "b".repeat(64),
247+artifact: "archive",
221248cleanup: archiveCleanupMock,
222249});
223250reportClawHubSkillInstallTelemetryMock.mockResolvedValue(undefined);
@@ -263,13 +290,125 @@ describe("skills-clawhub", () => {
263290baseUrl: undefined,
264291root: "/tmp/workspace",
265292skills: expect.objectContaining({
266-agentreceipt: {
293+agentreceipt: expect.objectContaining({
267294version: "1.0.0",
268295installedAt: expect.any(Number),
269296registry: "https://clawhub.ai",
270-},
297+}),
271298}),
272299});
300+const telemetrySkills = reportClawHubSkillInstallTelemetryMock.mock.calls[0]?.[0]?.skills as
301+| Record<string, Record<string, unknown>>
302+| undefined;
303+expect(Object.keys(telemetrySkills?.agentreceipt ?? {}).toSorted()).toEqual([
304+"installedAt",
305+"registry",
306+"version",
307+]);
308+});
309+310+it("persists install artifact and verification provenance in the ClawHub lockfile", async () => {
311+const workspaceDir = await tempDirs.make("openclaw-skills-lock-");
312+const skillContent = "---\nname: agentreceipt\ndescription: Receipt helper\n---\n";
313+const skillSha256 = createHash("sha256").update(skillContent).digest("hex");
314+installPackageDirMock.mockImplementationOnce(async (params: { targetDir: string }) => {
315+await fs.mkdir(params.targetDir, { recursive: true });
316+await fs.writeFile(path.join(params.targetDir, "SKILL.md"), skillContent, "utf8");
317+return { ok: true, targetDir: params.targetDir };
318+});
319+320+try {
321+const result = await installSkillFromClawHub({
322+ workspaceDir,
323+slug: "agentreceipt",
324+});
325+326+expectInstalledSkill(result, {
327+slug: "agentreceipt",
328+version: "1.0.0",
329+targetDir: path.join(workspaceDir, "skills", "agentreceipt"),
330+});
331+expect(fetchClawHubSkillVerificationMock).toHaveBeenCalledWith({
332+slug: "agentreceipt",
333+version: "1.0.0",
334+baseUrl: undefined,
335+});
336+const lock = JSON.parse(
337+await fs.readFile(path.join(workspaceDir, ".clawhub", "lock.json"), "utf8"),
338+) as { skills: Record<string, Record<string, unknown>> };
339+expect(lock.skills.agentreceipt).toMatchObject({
340+version: "1.0.0",
341+registry: "https://clawhub.ai",
342+artifact: {
343+kind: "archive",
344+sha256: "a".repeat(64),
345+integrity: "sha256-test",
346+},
347+skillFile: {
348+path: "SKILL.md",
349+sha256: skillSha256,
350+},
351+verification: {
352+schema: "clawhub.skill.verify.v1",
353+ok: true,
354+decision: "pass",
355+reasons: [],
356+provenance: { source: "unavailable" },
357+security: { status: "clean", signals: { staticScan: { engineVersion: "v2.4.24" } } },
358+},
359+});
360+const origin = JSON.parse(
361+await fs.readFile(
362+path.join(workspaceDir, "skills", "agentreceipt", ".clawhub", "origin.json"),
363+"utf8",
364+),
365+) as Record<string, unknown>;
366+expect(origin).toMatchObject({
367+version: 1,
368+slug: "agentreceipt",
369+installedVersion: "1.0.0",
370+artifact: {
371+kind: "archive",
372+sha256: "a".repeat(64),
373+integrity: "sha256-test",
374+},
375+skillFile: {
376+path: "SKILL.md",
377+sha256: skillSha256,
378+},
379+});
380+} finally {
381+await fs.rm(workspaceDir, { recursive: true, force: true });
382+}
383+});
384+385+it("keeps installing when the ClawHub verification snapshot is unavailable", async () => {
386+const workspaceDir = await tempDirs.make("openclaw-skills-lock-");
387+fetchClawHubSkillVerificationMock.mockRejectedValueOnce(new Error("verification down"));
388+installPackageDirMock.mockImplementationOnce(async (params: { targetDir: string }) => {
389+await fs.mkdir(params.targetDir, { recursive: true });
390+await fs.writeFile(path.join(params.targetDir, "SKILL.md"), "# AgentReceipt\n", "utf8");
391+return { ok: true, targetDir: params.targetDir };
392+});
393+394+try {
395+const result = await installSkillFromClawHub({
396+ workspaceDir,
397+slug: "agentreceipt",
398+});
399+400+expectInstalledSkill(result, { slug: "agentreceipt", version: "1.0.0" });
401+const lock = JSON.parse(
402+await fs.readFile(path.join(workspaceDir, ".clawhub", "lock.json"), "utf8"),
403+) as { skills: Record<string, Record<string, unknown>> };
404+expect(lock.skills.agentreceipt?.verification).toBeUndefined();
405+expect(lock.skills.agentreceipt?.artifact).toMatchObject({
406+sha256: "a".repeat(64),
407+integrity: "sha256-test",
408+});
409+} finally {
410+await fs.rm(workspaceDir, { recursive: true, force: true });
411+}
273412});
274413275414it("installs GitHub-backed ClawHub skills from the pinned resolver source path", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。