skillhub/cli/test/unit/commands/sync-command.test.ts
XiaoSeS a5a723d8f7 fix(cli): align publish and namespace sync semantics
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
2026-09-04 16:42:58 +08:00

100 lines
3.4 KiB
TypeScript

import { describe, expect, test } from 'bun:test'
import {
requireSyncNamespace,
resolvePullSelection
} from '../../../src/commands/sync'
import type { SyncStatusEntry } from '../../../src/services/sync-service'
import { computeStrictIsTTY } from '../../../src/shared/tty'
function entry(slug: string, status: SyncStatusEntry['status']): SyncStatusEntry {
return {
namespace: 'team-a',
slug,
status,
...(status === 'orphaned' ? {} : { remoteVersion: '1.0.0' }),
changedFiles: []
}
}
describe('sync command selection', () => {
test('requires an explicit non-global namespace', () => {
expect(() => requireSyncNamespace(undefined)).toThrow('--namespace is required')
expect(() => requireSyncNamespace('global')).toThrow('global does not support namespace sync')
expect(requireSyncNamespace('team-a')).toBe('team-a')
})
test('uses the same strict TTY rule as interactive install flows', () => {
expect(computeStrictIsTTY({ stdinIsTTY: true, stdoutIsTTY: true, json: false })).toBe(true)
expect(computeStrictIsTTY({ stdinIsTTY: false, stdoutIsTTY: true, json: false })).toBe(false)
expect(computeStrictIsTTY({ stdinIsTTY: true, stdoutIsTTY: false, json: false })).toBe(false)
expect(computeStrictIsTTY({ stdinIsTTY: true, stdoutIsTTY: true, json: true })).toBe(false)
})
test('accepts and deduplicates explicit skill selections without prompting', async () => {
let prompted = false
const selected = await resolvePullSelection(
[entry('first', 'not-installed'), entry('second', 'update-available')],
['second', 'first', 'second'],
{
interactive: false,
prune: false,
prompt: async () => {
prompted = true
return []
}
}
)
expect(selected).toEqual(['second', 'first'])
expect(prompted).toBe(false)
})
test('rejects non-interactive pull without an explicit selection', async () => {
await expect(resolvePullSelection([entry('demo', 'not-installed')], undefined, {
interactive: false,
prune: false,
prompt: async () => ['demo']
})).rejects.toThrow('requires at least one --skill')
})
test('interactive TTY returns exactly the prompt multi-selection', async () => {
const selected = await resolvePullSelection(
[entry('first', 'not-installed'), entry('second', 'update-available')],
undefined,
{
interactive: true,
prune: false,
prompt: async candidates => {
expect(candidates.map(candidate => candidate.slug)).toEqual(['first', 'second'])
return ['second']
}
}
)
expect(selected).toEqual(['second'])
})
test('interactive cancel or empty selection is a no-op', async () => {
const selected = await resolvePullSelection([entry('demo', 'not-installed')], undefined, {
interactive: true,
prune: false,
prompt: async () => []
})
expect(selected).toEqual([])
})
test('prune only makes managed orphan slugs selectable when explicitly enabled', async () => {
await expect(resolvePullSelection([entry('old', 'orphaned')], ['old'], {
interactive: false,
prune: false,
prompt: async () => []
})).rejects.toThrow('skill not found in namespace')
expect(await resolvePullSelection([entry('old', 'orphaned')], ['old'], {
interactive: false,
prune: true,
prompt: async () => []
})).toEqual(['old'])
})
})