Skip to content

Commit d7a017e

Browse files
committed
fix: add update lookup test (#22) (thanks @daveonkels)
1 parent fffdf82 commit d7a017e

File tree

2 files changed

+68
-2
lines changed

2 files changed

+68
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixed
66
- Registry: drop missing skills during search hydration (thanks @aaronn, #28).
7+
- CLI: use path-based skill metadata lookup for updates (thanks @daveonkels, #22).
78

89
## 0.3.0 - 2026-01-19
910

packages/clawdhub/src/cli/commands/skills.test.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,66 @@
11
/* @vitest-environment node */
22

33
import { afterEach, describe, expect, it, vi } from 'vitest'
4+
import { ApiRoutes } from '../../schema/index.js'
45
import type { GlobalOpts } from '../types'
56

67
const mockApiRequest = vi.fn()
8+
const mockDownloadZip = vi.fn()
79
vi.mock('../../http.js', () => ({
810
apiRequest: (...args: unknown[]) => mockApiRequest(...args),
11+
downloadZip: (...args: unknown[]) => mockDownloadZip(...args),
912
}))
1013

1114
const mockGetRegistry = vi.fn(async () => 'https://clawdhub.com')
1215
vi.mock('../registry.js', () => ({
1316
getRegistry: () => mockGetRegistry(),
1417
}))
1518

16-
const mockSpinner = { stop: vi.fn(), fail: vi.fn() }
19+
const mockSpinner = {
20+
stop: vi.fn(),
21+
fail: vi.fn(),
22+
start: vi.fn(),
23+
succeed: vi.fn(),
24+
isSpinning: false,
25+
text: '',
26+
}
1727
vi.mock('../ui.js', () => ({
1828
createSpinner: vi.fn(() => mockSpinner),
29+
fail: (message: string) => {
30+
throw new Error(message)
31+
},
1932
formatError: (error: unknown) => (error instanceof Error ? error.message : String(error)),
33+
isInteractive: () => false,
34+
promptConfirm: vi.fn(async () => false),
35+
}))
36+
37+
vi.mock('../../skills.js', () => ({
38+
extractZipToDir: vi.fn(),
39+
hashSkillFiles: vi.fn(),
40+
listTextFiles: vi.fn(),
41+
readLockfile: vi.fn(),
42+
readSkillOrigin: vi.fn(),
43+
writeLockfile: vi.fn(),
44+
writeSkillOrigin: vi.fn(),
2045
}))
2146

22-
const { clampLimit, cmdExplore, formatExploreLine } = await import('./skills')
47+
vi.mock('node:fs/promises', () => ({
48+
mkdir: vi.fn(),
49+
rm: vi.fn(),
50+
stat: vi.fn(),
51+
}))
52+
53+
const { clampLimit, cmdExplore, cmdUpdate, formatExploreLine } = await import('./skills')
54+
const {
55+
extractZipToDir,
56+
hashSkillFiles,
57+
listTextFiles,
58+
readLockfile,
59+
readSkillOrigin,
60+
writeLockfile,
61+
writeSkillOrigin,
62+
} = await import('../../skills.js')
63+
const { rm, stat } = await import('node:fs/promises')
2364

2465
const mockLog = vi.spyOn(console, 'log').mockImplementation(() => {})
2566

@@ -123,3 +164,27 @@ describe('cmdExplore', () => {
123164
expect(second.searchParams.get('sort')).toBe('trending')
124165
})
125166
})
167+
168+
describe('cmdUpdate', () => {
169+
it('uses path-based skill lookup when no local fingerprint is available', async () => {
170+
mockApiRequest.mockResolvedValue({ latestVersion: { version: '1.0.0' } })
171+
mockDownloadZip.mockResolvedValue(new Uint8Array([1, 2, 3]))
172+
vi.mocked(readLockfile).mockResolvedValue({
173+
skills: { demo: { version: '0.1.0', installedAt: 123 } },
174+
})
175+
vi.mocked(writeLockfile).mockResolvedValue()
176+
vi.mocked(readSkillOrigin).mockResolvedValue(null)
177+
vi.mocked(writeSkillOrigin).mockResolvedValue()
178+
vi.mocked(extractZipToDir).mockResolvedValue()
179+
vi.mocked(listTextFiles).mockResolvedValue([])
180+
vi.mocked(hashSkillFiles).mockReturnValue({ fingerprint: 'hash', files: [] })
181+
vi.mocked(stat).mockRejectedValue(new Error('missing'))
182+
vi.mocked(rm).mockResolvedValue()
183+
184+
await cmdUpdate(makeOpts(), 'demo', {}, false)
185+
186+
const [, args] = mockApiRequest.mock.calls[0] ?? []
187+
expect(args?.path).toBe(`${ApiRoutes.skills}/${encodeURIComponent('demo')}`)
188+
expect(args?.url).toBeUndefined()
189+
})
190+
})

0 commit comments

Comments
 (0)