mirror of
https://github.com/langgenius/dify.git
synced 2026-08-28 20:56:50 +08:00
204 lines
8.2 KiB
TypeScript
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])
|
|
})
|
|
})
|