skillhub/cli/test/unit/agents/resolver.test.ts
dongmucat 351dddc912 feat(cli): add SkillHub CLI v1 with full command suite
Implement complete CLI tool for SkillHub with 12 commands, 7 backend API endpoints, and comprehensive documentation.

CLI Commands:
- help, version: Basic information
- login, logout, whoami: Authentication management
- search: Discover published skills
- install: Install skills to agent directories (14 Tier 1 agents supported)
- list, remove, doctor: Local skill management
- publish: Publish skill packages
- update: Self-update mechanism

Backend API:
- Add /api/cli/v1 endpoints for auth, search, resolve, download, delete, publish
- Implement CliAuthController and CliSkillController
- Add security policies for CLI routes
- Full test coverage (19 backend tests)

CLI Implementation:
- TypeScript with strict mode, Bun runtime
- Pure JS zip handling (fflate) for cross-platform compatibility
- 15 agent profiles (14 Tier 1 + generic fallback)
- Secure token storage (0600 permissions)
- Path safety validation for remove operations
- Comprehensive error handling (404/403/network distinction)
- 41 unit and integration tests

Documentation:
- CLI user guide (Chinese and English)
- README updates with quick start
- GitHub Actions workflow for cross-platform CI

Quality:
- lint: 0 errors
- typecheck: pass
- test: 41/41 pass
- build: 0.30 MB (target=node for npm/npx compatibility)
2026-04-29 15:37:04 +08:00

92 lines
3 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import { resolveInstallTargets } from '../../../src/agents/resolver'
describe('resolveInstallTargets', () => {
test('rejects dir and agent together before filesystem writes', async () => {
await expect(resolveInstallTargets({
cwd: '/repo',
dir: '/tmp/skills',
agents: ['codex'],
json: false,
interactive: false
})).rejects.toThrow('--dir cannot be used with --agent')
})
test('falls back to cwd .agents skills when nothing detected', async () => {
const targets = await resolveInstallTargets({
cwd: '/repo',
agents: [],
json: false,
interactive: false,
detected: []
})
expect(targets).toEqual([{ agent: 'generic', rootDir: '/repo/.agents/skills', scope: 'project', source: 'fallback' }])
})
test('uses explicit dir when provided', async () => {
const targets = await resolveInstallTargets({
cwd: '/repo',
dir: '/tmp/my-skills',
json: false,
interactive: false
})
expect(targets).toEqual([{ agent: 'custom', rootDir: '/tmp/my-skills', scope: 'user', source: 'explicit' }])
})
test('explicit agent resolves the profile user root by default', async () => {
const targets = await resolveInstallTargets({
cwd: '/repo',
home: '/home/u',
agents: ['codex'],
json: false,
interactive: false
})
expect(targets).toEqual([{ agent: 'codex', rootDir: '/home/u/.codex/skills', scope: 'user', source: 'explicit' }])
})
test('deduplicates repeated explicit agents by target root', async () => {
const targets = await resolveInstallTargets({
cwd: '/repo',
home: '/home/u',
agents: ['codex', 'codex'],
json: false,
interactive: false
})
expect(targets).toHaveLength(1)
expect(targets[0]!.rootDir).toBe('/home/u/.codex/skills')
})
test('returns single detected target directly', async () => {
const targets = await resolveInstallTargets({
cwd: '/repo',
agents: [],
json: false,
interactive: false,
detected: [{ agent: 'codex', rootDir: '/repo/.codex/skills', scope: 'project', source: 'detected' }]
})
expect(targets).toHaveLength(1)
expect(targets[0]!.agent).toBe('codex')
})
test('rejects multiple detected targets in non-interactive mode', async () => {
await expect(resolveInstallTargets({
cwd: '/repo',
agents: [],
json: false,
interactive: false,
detected: [
{ agent: 'codex', rootDir: '/repo/.codex/skills', scope: 'project', source: 'detected' },
{ agent: 'claude-code', rootDir: '/repo/.claude/skills', scope: 'project', source: 'detected' }
]
})).rejects.toThrow('multiple install targets detected')
})
test('rejects unknown agent', async () => {
await expect(resolveInstallTargets({
cwd: '/repo',
agents: ['unknown-agent'],
json: false,
interactive: false
})).rejects.toThrow('unknown agent: unknown-agent')
})
})