// @vitest-environment node import { beforeEach, describe, expect, it, vi } from 'vitest'; const mocks = vi.hoisted(() => ({ install: vi.fn(), getAllSkillConfigs: vi.fn(), updateSkillConfig: vi.fn(), openPath: vi.fn(), MockSkillInstallServiceError: class MockSkillInstallServiceError extends Error { status: number; code: string; constructor(message: string, status: number, code: string) { super(message); this.status = status; this.code = code; } }, })); vi.mock('@service/skill-install-service', () => ({ SkillInstallService: class { install = mocks.install; }, SkillInstallServiceError: mocks.MockSkillInstallServiceError, })); vi.mock('../electron/utils/skill-config', () => ({ getAllSkillConfigs: mocks.getAllSkillConfigs, updateSkillConfig: mocks.updateSkillConfig, })); vi.mock('../electron/utils/paths', () => ({ getOpenClawConfigDir: () => 'C:/Users/Administrator/.openclaw', })); vi.mock('electron', () => ({ shell: { openPath: mocks.openPath, }, })); import { normalizeRequest } from '../electron/api/route-utils'; import { handleSkillRoutes } from '../electron/api/routes/skills'; const ctx = { gatewayManager: null, providerApiService: null, mainWindow: null, clawHubService: { getMarketplaceCapability: vi.fn(), search: vi.fn(), install: vi.fn(), uninstall: vi.fn(), listInstalled: vi.fn(), openSkillReadme: vi.fn(), openSkillPath: vi.fn(), }, } as any; describe('skill install routes', () => { beforeEach(() => { mocks.install.mockReset(); mocks.getAllSkillConfigs.mockReset(); mocks.updateSkillConfig.mockReset(); mocks.openPath.mockReset(); }); it('keeps /api/clawhub/install compatible with the marketplace payload', async () => { mocks.install.mockResolvedValue({ success: true, slug: 'demo-skill', baseDir: 'C:/Users/Administrator/.openclaw/skills/demo-skill', source: 'marketplace', enabled: true, }); const response = await handleSkillRoutes(normalizeRequest({ path: '/api/clawhub/install', method: 'POST', body: JSON.stringify({ slug: 'demo-skill', version: '1.2.3', force: true, }), }), ctx); expect(mocks.install).toHaveBeenCalledWith({ kind: 'marketplace', slug: 'demo-skill', version: '1.2.3', force: true, }); expect(response?.ok).toBe(true); expect(response?.json).toMatchObject({ success: true, slug: 'demo-skill', source: 'marketplace', }); }); it('handles /api/skills/install for github-url payloads', async () => { mocks.install.mockResolvedValue({ success: true, slug: 'minimax-xlsx', baseDir: 'C:/Users/Administrator/.openclaw/skills/minimax-xlsx', source: 'github-url', enabled: true, }); const response = await handleSkillRoutes(normalizeRequest({ path: '/api/skills/install', method: 'POST', body: JSON.stringify({ kind: 'github-url', url: 'https://github.com/MiniMax-AI/skills/blob/main/skills/minimax-xlsx/SKILL.md', }), }), ctx); expect(mocks.install).toHaveBeenCalledWith({ kind: 'github-url', url: 'https://github.com/MiniMax-AI/skills/blob/main/skills/minimax-xlsx/SKILL.md', }); expect(response?.ok).toBe(true); expect(response?.json).toMatchObject({ success: true, slug: 'minimax-xlsx', source: 'github-url', }); }); it('returns the service status code when install validation fails', async () => { mocks.install.mockRejectedValue(new mocks.MockSkillInstallServiceError( 'GitHub skill URL is invalid.', 400, 'invalid_github_url', )); const response = await handleSkillRoutes(normalizeRequest({ path: '/api/skills/install', method: 'POST', body: JSON.stringify({ kind: 'github-url', url: 'https://example.com/not-supported', }), }), ctx); expect(response?.ok).toBe(false); expect(response?.status).toBe(400); expect(response?.error).toBe('GitHub skill URL is invalid.'); }); });