feat: ship skill and MCP workflows with signed updater for v0.0.7
This commit is contained in:
1 parent
1b52df0603
commit
a01085c656
136 files changed
+18646
-217
No files matched your search
@@ -0,0 +1,79 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import {
|
||||
CapabilityPicker,
|
||||
capabilitySelectionKey,
|
||||
detectCapabilitySlashTrigger,
|
||||
filterCapabilityItems,
|
||||
resolveCapabilityPickerKey,
|
||||
type CapabilityPickerItem
|
||||
} from '../src/renderer/src/workbuddy/composer/CapabilityPicker'
|
||||
|
||||
const items: CapabilityPickerItem[] = [
|
||||
{ id: 'skill:review', name: '代码审查', description: '检查实现缺陷与风险', kind: 'skill' },
|
||||
{ id: 'mcp-server', name: 'postgres', description: '查询本地数据库', kind: 'mcp' },
|
||||
{ id: 'compact', name: 'compact', description: '压缩当前上下文', kind: 'command' }
|
||||
]
|
||||
const checks: string[] = []
|
||||
const check = (name: string, actual: unknown, expected: unknown): void => {
|
||||
assert.deepEqual(actual, expected, name)
|
||||
checks.push(name)
|
||||
}
|
||||
|
||||
check('empty query preserves registration order', filterCapabilityItems(items, ''), items)
|
||||
check('filter matches names and IDs case-insensitively',
|
||||
filterCapabilityItems(items, '/POST').map(item => item.id), ['mcp-server'])
|
||||
check('filter matches descriptions', filterCapabilityItems(items, '上下文').map(item => item.id), ['compact'])
|
||||
check('filter matches kind labels', filterCapabilityItems(items, '命令').map(item => item.id), ['compact'])
|
||||
check('multiple terms must all match', filterCapabilityItems(items, '数据库 本地').map(item => item.id), ['mcp-server'])
|
||||
check('selection identity includes kind', capabilitySelectionKey({ id: 'compact', kind: 'command' }), 'command:compact')
|
||||
|
||||
check('slash at input start is detected',
|
||||
detectCapabilitySlashTrigger('/comp', 5, items), { start: 0, end: 5, query: 'comp' })
|
||||
const independent = '请使用 /mcp'
|
||||
check('independent slash token is detected',
|
||||
detectCapabilitySlashTrigger(independent, independent.length, items), { start: 4, end: independent.length, query: 'mcp' })
|
||||
check('plain slash opens the catalog when items exist',
|
||||
detectCapabilitySlashTrigger('/', 1, items), { start: 0, end: 1, query: '' })
|
||||
for (const value of ['https://example.com/a', 'src/renderer/file.ts', 'C:\\work\\file.ts', '/Users/me/file.txt', 'file:///tmp/a']) {
|
||||
check('path or URL does not trigger: ' + value, detectCapabilitySlashTrigger(value, value.length, items), null)
|
||||
}
|
||||
check('unknown leading token is rejected when catalog is available',
|
||||
detectCapabilitySlashTrigger('/Users', 6, items), null)
|
||||
check('slash inside ordinary text is not independent',
|
||||
detectCapabilitySlashTrigger('path/compact', 12, items), null)
|
||||
|
||||
check('IME composition suppresses Enter',
|
||||
resolveCapabilityPickerKey({ key: 'Enter', isComposing: true, itemCount: 3, activeIndex: 1 }), { type: 'none' })
|
||||
check('IME keyCode 229 suppresses selection',
|
||||
resolveCapabilityPickerKey({ key: 'Enter', keyCode: 229, itemCount: 3, activeIndex: 1 }), { type: 'none' })
|
||||
check('ArrowDown wraps', resolveCapabilityPickerKey({ key: 'ArrowDown', itemCount: 3, activeIndex: 2 }),
|
||||
{ type: 'move', index: 0 })
|
||||
check('ArrowUp wraps', resolveCapabilityPickerKey({ key: 'ArrowUp', itemCount: 3, activeIndex: 0 }),
|
||||
{ type: 'move', index: 2 })
|
||||
check('Home and End select boundaries',
|
||||
[resolveCapabilityPickerKey({ key: 'Home', itemCount: 3, activeIndex: 2 }),
|
||||
resolveCapabilityPickerKey({ key: 'End', itemCount: 3, activeIndex: 0 })],
|
||||
[{ type: 'move', index: 0 }, { type: 'move', index: 2 }])
|
||||
check('Enter selects the active item',
|
||||
resolveCapabilityPickerKey({ key: 'Enter', itemCount: 3, activeIndex: 1 }), { type: 'select', index: 1 })
|
||||
check('Escape closes', resolveCapabilityPickerKey({ key: 'Escape', itemCount: 3, activeIndex: 1 }), { type: 'close' })
|
||||
check('Enter is ignored on an empty result', resolveCapabilityPickerKey({ key: 'Enter', itemCount: 0, activeIndex: 0 }),
|
||||
{ type: 'none' })
|
||||
|
||||
const html = renderToStaticMarkup(
|
||||
<CapabilityPicker items={items} selected={[{ id: 'skill:review', kind: 'skill' }]}
|
||||
onSelect={() => undefined} onClose={() => undefined} />
|
||||
)
|
||||
check('SSR renders combobox and listbox', [html.includes('role="combobox"'), html.includes('role="listbox"')], [true, true])
|
||||
check('SSR exposes type labels', ['技能', 'MCP', '命令'].every(label => html.includes(label)), true)
|
||||
check('SSR marks selected identity', html.includes('aria-selected="true"'), true)
|
||||
check('SSR renders descriptions', html.includes('检查实现缺陷与风险'), true)
|
||||
check('SSR seeds external query',
|
||||
renderToStaticMarkup(<CapabilityPicker items={items} selected={[]} onSelect={() => undefined}
|
||||
onClose={() => undefined} query="compact" />).includes('data-kind="command"'), true)
|
||||
check('SSR renders explicit empty state',
|
||||
renderToStaticMarkup(<CapabilityPicker items={items} selected={[]} onSelect={() => undefined}
|
||||
onClose={() => undefined} query="missing" />).includes('没有匹配的能力'), true)
|
||||
|
||||
console.log(JSON.stringify({ result: 'PASS', checks: checks.length }))
|
||||
Reference in new issue
Block a user