skillhub/cli/test/unit/services/remove-service.test.ts
dongmucat 05177e3085 test(cli): add P0/P1/P2 test coverage for security and error paths
Add 36 test cases covering:
- Path traversal and symlink attack prevention in archive extraction
- SkillHubClient error handling (401/403/404/network) for all endpoints
- Inventory store concurrent writes and stale lock recovery
- Config store read/write round-trip
- Platform utilities (package-manager, updater, paths)
- Output formatting (printResult, humanize)

Tests use cross-platform commands (node) instead of shell builtins
for CI compatibility across macOS/Linux/Windows.
2026-04-29 15:37:05 +08:00

74 lines
2.5 KiB
TypeScript

import { access, mkdir, mkdtemp } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, test } from 'bun:test'
import { removeLocalSkill } from '../../../src/services/remove-service'
import { InventoryStore } from '../../../src/stores/inventory-store'
async function exists(path: string): Promise<boolean> {
try {
await access(path)
return true
} catch {
return false
}
}
describe('removeLocalSkill', () => {
test('removes all current-registry installs with the same slug across namespaces', async () => {
const home = await mkdtemp(join(tmpdir(), 'skillhub-remove-home-'))
const root = await mkdtemp(join(tmpdir(), 'skillhub-remove-root-'))
const globalDir = join(root, 'codex', 'demo')
const teamDir = join(root, 'claude', 'demo')
await mkdir(globalDir, { recursive: true })
await mkdir(teamDir, { recursive: true })
const store = new InventoryStore(home)
await store.write({
items: [
{
registry: 'https://skill.xfyun.cn',
namespace: 'global',
slug: 'demo',
version: '1.0.0',
targets: [{ agent: 'codex', rootDir: join(root, 'codex'), installDir: globalDir, installedAt: '2026-04-20T00:00:00Z' }]
},
{
registry: 'https://skill.xfyun.cn',
namespace: 'team',
slug: 'demo',
version: '1.0.0',
targets: [{ agent: 'claude-code', rootDir: join(root, 'claude'), installDir: teamDir, installedAt: '2026-04-20T00:00:00Z' }]
}
]
})
const result = await removeLocalSkill({ registry: 'https://skill.xfyun.cn', slug: 'demo', home })
expect(result.removed.map(item => item.namespace).sort()).toEqual(['global', 'team'])
expect(await exists(globalDir)).toBe(false)
expect(await exists(teamDir)).toBe(false)
expect((await store.read()).items).toEqual([])
})
test('throws on path traversal in installDir', async () => {
const home = await mkdtemp(join(tmpdir(), 'skillhub-remove-traversal-'))
const store = new InventoryStore(home)
await store.write({
items: [
{
registry: 'https://skill.xfyun.cn',
namespace: 'global',
slug: 'evil',
version: '1.0.0',
targets: [{ agent: 'codex', rootDir: '/safe/root', installDir: '/etc/passwd', installedAt: '2026-04-20T00:00:00Z' }]
}
]
})
await expect(
removeLocalSkill({ registry: 'https://skill.xfyun.cn', slug: 'evil', home })
).rejects.toThrow('unsafe remove path')
})
})