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" ;
68
79const 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 ( ) ;
2226
2327vi . mock ( "../../infra/clawhub.js" , ( ) => ( {
2428 fetchClawHubSkillDetail : fetchClawHubSkillDetailMock ,
2529 fetchClawHubSkillInstallResolution : fetchClawHubSkillInstallResolutionMock ,
30+ fetchClawHubSkillVerification : fetchClawHubSkillVerificationMock ,
2631 downloadClawHubSkillArchive : downloadClawHubSkillArchiveMock ,
2732 downloadClawHubSkillArchiveUrl : downloadClawHubSkillArchiveUrlMock ,
2833 downloadClawHubGitHubSkillArchive : downloadClawHubGitHubSkillArchiveMock ,
@@ -162,9 +167,14 @@ async function writeClawHubOriginFixture(params: {
162167}
163168
164169describe ( "skills-clawhub" , ( ) => {
170+ afterEach ( async ( ) => {
171+ await tempDirs . cleanup ( ) ;
172+ } ) ;
173+
165174 beforeEach ( ( ) => {
166175 fetchClawHubSkillDetailMock . mockReset ( ) ;
167176 fetchClawHubSkillInstallResolutionMock . mockReset ( ) ;
177+ fetchClawHubSkillVerificationMock . mockReset ( ) ;
168178 downloadClawHubSkillArchiveMock . mockReset ( ) ;
169179 downloadClawHubSkillArchiveUrlMock . mockReset ( ) ;
170180 downloadClawHubGitHubSkillArchiveMock . mockReset ( ) ;
@@ -205,19 +215,36 @@ describe("skills-clawhub", () => {
205215 downloadUrl : "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+ } ) ;
208229 downloadClawHubSkillArchiveMock . mockResolvedValue ( {
209230 archivePath : "/tmp/agentreceipt.zip" ,
210231 integrity : "sha256-test" ,
232+ sha256Hex : "a" . repeat ( 64 ) ,
233+ artifact : "archive" ,
211234 cleanup : archiveCleanupMock ,
212235 } ) ;
213236 downloadClawHubSkillArchiveUrlMock . mockResolvedValue ( {
214237 archivePath : "/tmp/agentreceipt.zip" ,
215238 integrity : "sha256-test" ,
239+ sha256Hex : "a" . repeat ( 64 ) ,
240+ artifact : "archive" ,
216241 cleanup : archiveCleanupMock ,
217242 } ) ;
218243 downloadClawHubGitHubSkillArchiveMock . mockResolvedValue ( {
219244 archivePath : "/tmp/github-agentreceipt.zip" ,
220245 integrity : "sha256-github-test" ,
246+ sha256Hex : "b" . repeat ( 64 ) ,
247+ artifact : "archive" ,
221248 cleanup : archiveCleanupMock ,
222249 } ) ;
223250 reportClawHubSkillInstallTelemetryMock . mockResolvedValue ( undefined ) ;
@@ -263,13 +290,125 @@ describe("skills-clawhub", () => {
263290 baseUrl : undefined ,
264291 root : "/tmp/workspace" ,
265292 skills : expect . objectContaining ( {
266- agentreceipt : {
293+ agentreceipt : expect . objectContaining ( {
267294 version : "1.0.0" ,
268295 installedAt : expect . any ( Number ) ,
269296 registry : "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 } ) ;
274413
275414 it ( "installs GitHub-backed ClawHub skills from the pinned resolver source path" , async ( ) => {
0 commit comments