dify/cli/src/commands/skills/install/run.test.ts
2026-08-14 12:26:45 +00:00

204 lines
8.2 KiB
TypeScript

import type { SkillsInstallOptions } from './run'
import { existsSync } from 'node:fs'
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vite-plus/test'
import { renderSkill } from '@/help/skill'
import { runSkillsInstall } from './run'
const VERSION = '9.9.9-test'
const SKILL = renderSkill({ version: VERSION })
describe('runSkillsInstall', () => {
let home: string
beforeEach(async () => {
home = await mkdtemp(join(tmpdir(), 'difyctl-skills-'))
})
afterEach(async () => {
await rm(home, { recursive: true, force: true })
})
const opts = (over: Partial<SkillsInstallOptions>): SkillsInstallOptions => ({
version: VERSION,
write: false,
stdout: false,
agents: [],
home,
...over,
})
const claudeTarget = () => join(home, '.claude', 'skills', 'difyctl', 'SKILL.md')
it('--stdout returns the skill verbatim and writes nothing', async () => {
expect(await runSkillsInstall(opts({ stdout: true }))).toEqual({
kind: 'ok',
text: SKILL,
wrote: [],
})
})
it('dry-run lists detected agents and writes nothing', async () => {
await mkdir(join(home, '.claude'))
const result = await runSkillsInstall(opts({}))
expect(result.kind).toBe('ok')
if (result.kind !== 'ok') return
expect(result.text).toContain('Detected 1 agent: claude-code')
expect(result.text).toContain(`would write to claude-code: ${claudeTarget()}`)
expect(result.text).toContain('Re-run with --yes')
// A single detected agent: hint the manual-directory escape hatch, but not
// the subset selector (nothing to subset).
expect(result.text).toContain('skills install <dir>')
expect(result.text).not.toContain('--agent')
expect(result.wrote).toEqual([])
expect(existsSync(claudeTarget())).toBe(false)
})
it('dry-run summarizes detected agents and enumerates --agent names', async () => {
await mkdir(join(home, '.claude'))
await mkdir(join(home, '.codex'))
const result = await runSkillsInstall(opts({}))
expect(result.kind).toBe('ok')
if (result.kind !== 'ok') return
// The detection summary lists the selectable names; the footer just shows the flag.
expect(result.text).toContain('Detected 2 agents: claude-code, codex')
expect(result.text).toContain('--agent <name> to write only some')
expect(result.text).toContain('skills install <dir>')
expect(result.wrote).toEqual([])
})
it('--yes writes the skill and reports the actual path', async () => {
await mkdir(join(home, '.claude'))
const result = await runSkillsInstall(opts({ write: true }))
expect(result).toEqual({
kind: 'ok',
text: `wrote ${claudeTarget()}\n`,
wrote: [claudeTarget()],
})
expect(await readFile(claudeTarget(), 'utf8')).toBe(SKILL)
})
it('overwrites an existing skill in place', async () => {
await mkdir(join(home, '.claude', 'skills', 'difyctl'), { recursive: true })
await writeFile(claudeTarget(), 'stale skill', 'utf8')
await runSkillsInstall(opts({ write: true }))
expect(await readFile(claudeTarget(), 'utf8')).toBe(SKILL)
})
it('guides the user and writes nothing when no agents are detected', async () => {
const result = await runSkillsInstall(opts({ write: true }))
expect(result.kind).toBe('ok')
if (result.kind !== 'ok') return
expect(result.text).toContain('No agents detected')
expect(result.wrote).toEqual([])
})
it('rejects an --agent name that is not detected', async () => {
await mkdir(join(home, '.claude'))
const result = await runSkillsInstall(opts({ write: true, agents: ['bogus'] }))
expect(result.kind).toBe('usage')
if (result.kind !== 'usage') return
expect(result.message).toContain('bogus')
})
it('rejects --agent for an agent that is not present (none detected)', async () => {
const result = await runSkillsInstall(opts({ write: true, agents: ['claude-code'] }))
expect(result.kind).toBe('usage')
})
it('[dir] forces a single directory, bypassing detection', async () => {
const dest = join(home, 'forced')
const target = join(dest, 'SKILL.md')
const result = await runSkillsInstall(opts({ write: true, dir: dest }))
expect(result).toEqual({ kind: 'ok', text: `wrote ${target}\n`, wrote: [target] })
expect(await readFile(target, 'utf8')).toBe(SKILL)
})
it('writes one copy per detected agent', async () => {
await mkdir(join(home, '.claude'))
await mkdir(join(home, '.codex'))
await mkdir(join(home, '.config', 'opencode'), { recursive: true })
const result = await runSkillsInstall(opts({ write: true }))
expect(result.kind).toBe('ok')
if (result.kind !== 'ok') return
expect(result.wrote).toEqual([
join(home, '.claude', 'skills', 'difyctl', 'SKILL.md'),
join(home, '.agents', 'skills', 'difyctl', 'SKILL.md'),
join(home, '.config', 'opencode', 'skills', 'difyctl', 'SKILL.md'),
])
})
it('narrows writes to the named agent subset', async () => {
await mkdir(join(home, '.claude'))
await mkdir(join(home, '.codex'))
const result = await runSkillsInstall(opts({ write: true, agents: ['codex'] }))
expect(result.kind).toBe('ok')
if (result.kind !== 'ok') return
expect(result.wrote).toEqual([join(home, '.agents', 'skills', 'difyctl', 'SKILL.md')])
})
it('writes cursor and pi to their documented dirs (pi under agent/skills)', async () => {
await mkdir(join(home, '.cursor'))
await mkdir(join(home, '.pi'))
const result = await runSkillsInstall(opts({ write: true }))
expect(result.kind).toBe('ok')
if (result.kind !== 'ok') return
expect(result.wrote).toEqual([
join(home, '.cursor', 'skills', 'difyctl', 'SKILL.md'),
join(home, '.pi', 'agent', 'skills', 'difyctl', 'SKILL.md'),
])
expect(
await readFile(join(home, '.pi', 'agent', 'skills', 'difyctl', 'SKILL.md'), 'utf8'),
).toBe(SKILL)
})
it('writes qoder, windsurf and hermes to their documented dirs', async () => {
await mkdir(join(home, '.qoder'))
await mkdir(join(home, '.codeium', 'windsurf'), { recursive: true })
await mkdir(join(home, '.hermes'))
const result = await runSkillsInstall(opts({ write: true }))
expect(result.kind).toBe('ok')
if (result.kind !== 'ok') return
expect(result.wrote).toEqual([
join(home, '.qoder', 'skills', 'difyctl', 'SKILL.md'),
join(home, '.codeium', 'windsurf', 'skills', 'difyctl', 'SKILL.md'),
join(home, '.hermes', 'skills', 'difyctl', 'SKILL.md'),
])
})
it('writes the shared ~/.agents/skills path once for codex, amp and openclaw', async () => {
await mkdir(join(home, '.codex'))
await mkdir(join(home, '.config', 'amp'), { recursive: true })
await mkdir(join(home, '.openclaw'))
const shared = join(home, '.agents', 'skills', 'difyctl', 'SKILL.md')
const result = await runSkillsInstall(opts({ write: true }))
expect(result).toEqual({ kind: 'ok', text: `wrote ${shared}\n`, wrote: [shared] })
expect(await readFile(shared, 'utf8')).toBe(SKILL)
})
it('dry-run merges agents sharing a target into one line', async () => {
await mkdir(join(home, '.codex'))
await mkdir(join(home, '.config', 'amp'), { recursive: true })
await mkdir(join(home, '.openclaw'))
const shared = join(home, '.agents', 'skills', 'difyctl', 'SKILL.md')
const result = await runSkillsInstall(opts({}))
expect(result.kind).toBe('ok')
if (result.kind !== 'ok') return
// The header counts agents; the target line merges the ones sharing a path.
expect(result.text).toContain('Detected 3 agents: codex, amp, openclaw')
expect(result.text).toContain(`would write to codex, amp, openclaw: ${shared}`)
expect(result.wrote).toEqual([])
})
it('--agent amp alone writes the shared path once', async () => {
await mkdir(join(home, '.config', 'amp'), { recursive: true })
const shared = join(home, '.agents', 'skills', 'difyctl', 'SKILL.md')
const result = await runSkillsInstall(opts({ write: true, agents: ['amp'] }))
expect(result.kind).toBe('ok')
if (result.kind !== 'ok') return
expect(result.wrote).toEqual([shared])
})
})