|
1 | 1 | /* @vitest-environment node */ |
2 | 2 |
|
3 | 3 | import { afterEach, describe, expect, it, vi } from 'vitest' |
| 4 | +import { ApiRoutes } from '../../schema/index.js' |
4 | 5 | import type { GlobalOpts } from '../types' |
5 | 6 |
|
6 | 7 | const mockApiRequest = vi.fn() |
| 8 | +const mockDownloadZip = vi.fn() |
7 | 9 | vi.mock('../../http.js', () => ({ |
8 | 10 | apiRequest: (...args: unknown[]) => mockApiRequest(...args), |
| 11 | + downloadZip: (...args: unknown[]) => mockDownloadZip(...args), |
9 | 12 | })) |
10 | 13 |
|
11 | 14 | const mockGetRegistry = vi.fn(async () => 'https://clawdhub.com') |
12 | 15 | vi.mock('../registry.js', () => ({ |
13 | 16 | getRegistry: () => mockGetRegistry(), |
14 | 17 | })) |
15 | 18 |
|
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 | +} |
17 | 27 | vi.mock('../ui.js', () => ({ |
18 | 28 | createSpinner: vi.fn(() => mockSpinner), |
| 29 | + fail: (message: string) => { |
| 30 | + throw new Error(message) |
| 31 | + }, |
19 | 32 | 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(), |
20 | 45 | })) |
21 | 46 |
|
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') |
23 | 64 |
|
24 | 65 | const mockLog = vi.spyOn(console, 'log').mockImplementation(() => {}) |
25 | 66 |
|
@@ -123,3 +164,27 @@ describe('cmdExplore', () => { |
123 | 164 | expect(second.searchParams.get('sort')).toBe('trending') |
124 | 165 | }) |
125 | 166 | }) |
| 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