103 lines
5.4 KiB
TypeScript
103 lines
5.4 KiB
TypeScript
import { expect, mock, test } from 'claude-code/testing'
|
|
|
|
import { bumpVersion, parseEntries, renderGuide, upsertEntry, validateSkill } from './skillgen'
|
|
|
|
const SKILL = '---\nname: anima-greet\ndescription: 打招呼\n---\n\n用角色口吻向使用者打招呼,並簡短說明今天的待辦。\n'
|
|
const GENERATED = JSON.stringify({ skill: SKILL, example: '早安' })
|
|
|
|
type Calls = { argv: string[] }[]
|
|
|
|
// 啟動角色並替身所有外部:模型、檔案系統、git/tea。
|
|
async function setup($: any, on: any, opts: { reply?: (prompt: string) => string | undefined; existing?: boolean } = {}) {
|
|
const writes: Record<string, string> = {}
|
|
const procs: Calls = []
|
|
mock.store(on)
|
|
on('command.register', () => ({ value: { name: 'x' } }) as never)
|
|
on('session.start', () => ({ cwd: '/repo' }) as never)
|
|
on('session.cwd', () => ({ value: '/repo' }))
|
|
on('command.run', () => ({ text: '' }))
|
|
on('prompt.compose', () => ({ sections: [] }))
|
|
on('model.complete', (_: any, e: any) => {
|
|
const t = opts.reply ? opts.reply(String(e.prompt)) : String(e.prompt).startsWith('請撰寫') ? GENERATED : '嗨!'
|
|
return { value: t === undefined ? { isAnswered: false, reason: 'empty-reply' } : { isAnswered: true, text: t, usage: {} } }
|
|
})
|
|
on('process.run', (_: any, e: any) => {
|
|
procs.push({ argv: [...e.argv] })
|
|
const s = e.argv.join(' ')
|
|
if (s.startsWith('git rev-parse')) return { value: { exitCode: 0, stdout: '/repo\n', stderr: '' } }
|
|
if (s.startsWith('git symbolic-ref')) return { value: { exitCode: 0, stdout: 'origin/main\n', stderr: '' } }
|
|
if (s.startsWith('tea logins')) return { value: { exitCode: 0, stdout: 'admin\n', stderr: '' } }
|
|
if (s.startsWith('tea pulls')) return { value: { exitCode: 0, stdout: 'https://g/pulls/1\n', stderr: '' } }
|
|
return { value: { exitCode: 0, stdout: '', stderr: '' } }
|
|
})
|
|
on('fs.exists', (_: any, e: any) => ({ value: Boolean(opts.existing && String(e.path).endsWith('anima-greet/SKILL.md')) }))
|
|
on('fs.read', (_: any, e: any) => ({
|
|
value: String(e.path).endsWith('plugin.json') ? '{\n "version": "0.3.0"\n}' : String(e.path).endsWith('README.md') ? '# jsc-anima\n' : '',
|
|
}))
|
|
on('fs.write', (_: any, e: any) => {
|
|
writes[e.path] = e.text
|
|
return { value: undefined }
|
|
})
|
|
await $.session.start({ source: 'startup', cwd: '/repo' } as never)
|
|
return { writes, procs }
|
|
}
|
|
|
|
const run = ($: any, args: string) => $.command.run({ command: 'anima-skill', args } as never)
|
|
|
|
test('未啟動角色時工具二被拒絕並提示先啟動', async ($, on) => {
|
|
const { procs } = await setup($, on)
|
|
const r = await run($, 'greet 打招呼')
|
|
expect(r.text.includes('請先啟動角色')).toBe(true)
|
|
expect(procs.length).toBe(0)
|
|
})
|
|
|
|
test('新增技能:寫檔、升 minor、更新 README、建立 PR', async ($, on) => {
|
|
const { writes, procs } = await setup($, on)
|
|
await $.command.run({ command: 'sao-asuna', args: '' } as never)
|
|
const r = await run($, 'greet 打招呼')
|
|
const key = (suffix: string) => Object.keys(writes).find(k => k.endsWith(suffix))!
|
|
expect(writes[key('skills/anima-greet/SKILL.md')]!.includes('name: anima-greet')).toBe(true)
|
|
expect(writes[key('.claude-plugin/plugin.json')]!.includes('"0.4.0"')).toBe(true)
|
|
expect(writes[key('/README.md')]!.includes('目前版本:0.4.0') || Object.values(writes).some(v => v.includes('目前版本:0.4.0'))).toBe(true)
|
|
expect(writes[key('skills/jsc-anima/README.md')]!.includes('/jsc-anima:anima-greet')).toBe(true)
|
|
expect(procs.some(p => p.argv.join(' ').startsWith('git commit') && p.argv.join(' ').includes('feat: 新增技能'))).toBe(true)
|
|
const pr = procs.find(p => p.argv[0] === 'tea' && p.argv[1] === 'pulls')!
|
|
expect(pr.argv.includes('main')).toBe(true)
|
|
expect(pr.argv.includes('feat/anima-greet')).toBe(true)
|
|
expect(r.text.includes('https://g/pulls/1')).toBe(true)
|
|
})
|
|
|
|
test('技能已存在則覆蓋並升 patch', async ($, on) => {
|
|
const { writes } = await setup($, on, { existing: true })
|
|
await $.command.run({ command: 'sao-asuna', args: '' } as never)
|
|
await run($, 'greet 打招呼')
|
|
expect(Object.entries(writes).some(([k, v]) => k.endsWith('plugin.json') && v.includes('"0.3.1"'))).toBe(true)
|
|
})
|
|
|
|
test('驗證失敗或實跑失敗就不建立 PR', async ($, on) => {
|
|
const bad = await setup($, on, { reply: () => JSON.stringify({ skill: 'no frontmatter', example: 'x' }) })
|
|
await $.command.run({ command: 'sao-asuna', args: '' } as never)
|
|
const r1 = await run($, 'greet 打招呼')
|
|
expect(r1.text.includes('驗證失敗')).toBe(true)
|
|
expect(bad.procs.some(p => p.argv[0] === 'tea')).toBe(false)
|
|
expect(Object.keys(bad.writes).length).toBe(0)
|
|
})
|
|
|
|
test('實跑無回應就不建立 PR', async ($, on) => {
|
|
const s = await setup($, on, { reply: p => (p.startsWith('請撰寫') ? GENERATED : undefined) })
|
|
await $.command.run({ command: 'sao-asuna', args: '' } as never)
|
|
const r = await run($, 'greet 打招呼')
|
|
expect(r.text.includes('實跑失敗')).toBe(true)
|
|
expect(s.procs.some(p => p.argv[0] === 'tea')).toBe(false)
|
|
})
|
|
|
|
test('技能驗證與版本規則', () => {
|
|
expect(validateSkill(SKILL, 'greet').length).toBe(0)
|
|
expect(validateSkill(SKILL, 'other').length).toBe(1)
|
|
expect(bumpVersion('0.3.4', 'minor')).toBe('0.4.0')
|
|
expect(bumpVersion('0.3.4', 'patch')).toBe('0.3.5')
|
|
const list = upsertEntry(parseEntries(renderGuide([{ skill: 'a', description: 'A' }])), { skill: 'b', description: 'B' })
|
|
expect(list.length).toBe(2)
|
|
expect(renderGuide(list).includes('# /jsc-anima:jsc-anima')).toBe(true)
|
|
})
|