skillhub/cli/test/unit/agents/resolver.test.ts
dongmucat 96681b021a fix(cli): label scope by userRoots membership instead of cwd prefix
When --agent is provided without --scope, scope was inferred via
root.startsWith(cwd), which mislabels user roots as project when
cwd === home. Use profile.userRoots(home) membership instead, so the
candidate scope reflects the profile's intent rather than path prefix
overlap. The chosen root path itself is unchanged.
2026-05-15 14:43:25 +08:00

219 lines
7 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('explicit agent without scope labels root by userRoots membership when cwd === home', async () => {
const targets = await resolveInstallTargets({
cwd: '/home/u',
home: '/home/u',
agents: ['codex'],
json: false,
interactive: false
})
expect(targets[0]!.scope).toBe('user')
expect(targets[0]!.rootDir).toBe('/home/u/.codex/skills')
})
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')
})
test('rejects dir and scope together', async () => {
await expect(resolveInstallTargets({
cwd: '/repo',
dir: '/tmp/skills',
scope: 'user',
json: false,
interactive: false
})).rejects.toThrow('--dir cannot be used with --scope')
})
test('scope=project + agent codex returns project root with project scope', async () => {
const targets = await resolveInstallTargets({
cwd: '/repo',
home: '/home/u',
agents: ['codex'],
scope: 'project',
json: false,
interactive: false
})
expect(targets).toEqual([{ agent: 'codex', rootDir: '/repo/.codex/skills', scope: 'project', source: 'explicit' }])
})
test('scope=user + agent codex returns user root with user scope', async () => {
const targets = await resolveInstallTargets({
cwd: '/repo',
home: '/home/u',
agents: ['codex'],
scope: 'user',
json: false,
interactive: false
})
expect(targets).toEqual([{ agent: 'codex', rootDir: '/home/u/.codex/skills', scope: 'user', source: 'explicit' }])
})
test('scope=user + cwd === home + agent codex still labels candidate as user', async () => {
const targets = await resolveInstallTargets({
cwd: '/home/u',
home: '/home/u',
agents: ['codex'],
scope: 'user',
json: false,
interactive: false
})
expect(targets[0]!.scope).toBe('user')
})
test('scope=user clean env falls back to user agents skills', async () => {
const targets = await resolveInstallTargets({
cwd: '/repo',
home: '/nonexistent-home-' + Math.random().toString(36).slice(2),
agents: [],
scope: 'user',
json: false,
interactive: false
})
expect(targets).toEqual([{
agent: 'generic',
rootDir: targets[0]!.rootDir,
scope: 'user',
source: 'fallback'
}])
expect(targets[0]!.rootDir).toMatch(/\.agents\/skills$/)
expect(targets[0]!.rootDir.startsWith('/nonexistent-home-')).toBe(true)
})
test('scope=project clean env falls back to cwd agents skills', async () => {
const targets = await resolveInstallTargets({
cwd: '/nonexistent-repo-' + Math.random().toString(36).slice(2),
home: '/home/u',
agents: [],
scope: 'project',
json: false,
interactive: false
})
expect(targets).toHaveLength(1)
expect(targets[0]!.scope).toBe('project')
expect(targets[0]!.source).toBe('fallback')
expect(targets[0]!.rootDir).toMatch(/\.agents\/skills$/)
})
test('scope filters detected candidates and falls back when filtered empty', async () => {
const targets = await resolveInstallTargets({
cwd: '/repo',
home: '/home/u',
agents: [],
scope: 'user',
json: false,
interactive: false,
detected: [
{ agent: 'codex', rootDir: '/repo/.codex/skills', scope: 'project', source: 'detected' }
]
})
expect(targets).toHaveLength(1)
expect(targets[0]!.source).toBe('fallback')
expect(targets[0]!.scope).toBe('user')
})
test('scope filters detected candidates keeps matching scope', async () => {
const targets = await resolveInstallTargets({
cwd: '/repo',
home: '/home/u',
agents: [],
scope: 'user',
json: false,
interactive: false,
detected: [
{ agent: 'codex', rootDir: '/repo/.codex/skills', scope: 'project', source: 'detected' },
{ agent: 'codex', rootDir: '/home/u/.codex/skills', scope: 'user', source: 'detected' }
]
})
expect(targets).toHaveLength(1)
expect(targets[0]!.rootDir).toBe('/home/u/.codex/skills')
expect(targets[0]!.scope).toBe('user')
})
})