mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 08:03:28 +00:00
* Add version-matched bundled skill guides * Clarify skill freshness rollout PRs * Add canonical skills show alias * fix(skills): address guide review feedback * fix(skills): make guide commands cross-platform * fix(skills): apply the ORCA convention to the emulator guides Review follow-up: the emulator guides still instructed literal `orca emulator ...` in sh fences with no Linux disambiguation, so on unmanaged Linux they could launch the GNOME screen reader — the exact failure the executable-selection preamble prevents. Both emulator guides now carry the preamble and ORCA placeholder across fences, tables, and prose, and the cross-platform safety test covers all four converted guides. Also replaces computer-use's "unless a block names a shell" carve-out, which contradicted its own POSIX example, with the unconditional placeholder rule.
199 lines
5.9 KiB
TypeScript
199 lines
5.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { guideModuleLoadMock, runtimeClientConstructorMock } = vi.hoisted(() => ({
|
|
guideModuleLoadMock: vi.fn(),
|
|
runtimeClientConstructorMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('./bundled-skill-guides.js', () => {
|
|
guideModuleLoadMock()
|
|
return {
|
|
BUNDLED_SKILL_GUIDES: [
|
|
{
|
|
name: 'zeta',
|
|
description: 'Use when zeta work\nspans lines.',
|
|
markdown: '# Zeta\n',
|
|
fullMarkdown: '# Zeta\n\n## References\n\nZeta reference.\n',
|
|
aliases: []
|
|
},
|
|
{
|
|
name: 'alpha',
|
|
description: 'Use when alpha work is needed.',
|
|
markdown: '# Alpha\n\nShort.\n',
|
|
fullMarkdown: '# Alpha\n\nShort.\n\n## References\n\nFull.\n',
|
|
aliases: ['legacy-alpha']
|
|
}
|
|
]
|
|
}
|
|
})
|
|
|
|
vi.mock('./runtime-client', () => {
|
|
class RuntimeClient {
|
|
constructor() {
|
|
runtimeClientConstructorMock()
|
|
}
|
|
}
|
|
|
|
class RuntimeClientError extends Error {
|
|
readonly code: string
|
|
readonly data?: unknown
|
|
|
|
constructor(code: string, message: string, data?: unknown) {
|
|
super(message)
|
|
this.code = code
|
|
this.data = data
|
|
}
|
|
}
|
|
|
|
class RuntimeRpcFailureError extends RuntimeClientError {
|
|
readonly response: unknown
|
|
|
|
constructor(response: unknown) {
|
|
super('runtime_error', 'runtime_error')
|
|
this.response = response
|
|
}
|
|
}
|
|
|
|
return {
|
|
RuntimeClient,
|
|
RuntimeClientError,
|
|
RuntimeRpcFailureError,
|
|
serveOrcaApp: vi.fn(),
|
|
getDefaultUserDataPath: vi.fn(() => '/tmp/orca-user-data')
|
|
}
|
|
})
|
|
|
|
import { dispatch } from './dispatch'
|
|
import { main } from './index'
|
|
|
|
describe('orca skills CLI', () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks()
|
|
runtimeClientConstructorMock.mockClear()
|
|
process.exitCode = undefined
|
|
})
|
|
|
|
it('keeps the bundled table off the eager command-registry path', async () => {
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
expect(guideModuleLoadMock).not.toHaveBeenCalled()
|
|
await main(['status', '--help'], '/tmp/repo')
|
|
expect(guideModuleLoadMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('dispatches an alias locally and emits the exact Markdown', async () => {
|
|
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
|
|
|
|
await dispatch(['skills', 'get'], {
|
|
flags: new Map([['topic', 'legacy-alpha']]),
|
|
get client(): never {
|
|
throw new Error('skills get accessed RuntimeClient')
|
|
},
|
|
cwd: '/tmp/repo',
|
|
json: false
|
|
})
|
|
|
|
expect(stdoutText(stdoutSpy)).toBe('# Alpha\n\nShort.\n')
|
|
})
|
|
|
|
it('lists canonical topics deterministically without constructing RuntimeClient', async () => {
|
|
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
|
|
|
|
await main(['skills', 'list'], '/tmp/repo')
|
|
|
|
expect(stdoutText(stdoutSpy)).toBe(
|
|
'alpha: Use when alpha work is needed.\nzeta: Use when zeta work spans lines.\n'
|
|
)
|
|
expect(runtimeClientConstructorMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('emits full Markdown for --full', async () => {
|
|
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
|
|
|
|
await main(['skills', 'get', 'alpha', '--full'], '/tmp/repo')
|
|
|
|
expect(stdoutText(stdoutSpy)).toBe('# Alpha\n\nShort.\n\n## References\n\nFull.\n')
|
|
})
|
|
|
|
it('supports the canonical single-item show verb as an alias', async () => {
|
|
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
|
|
|
|
await main(['skills', 'show', 'alpha'], '/tmp/repo')
|
|
|
|
expect(stdoutText(stdoutSpy)).toBe('# Alpha\n\nShort.\n')
|
|
})
|
|
|
|
it('gives list --json a stable canonical schema', async () => {
|
|
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
|
|
|
|
await main(['skills', 'list', '--json'], '/tmp/repo')
|
|
|
|
expect(stdoutText(stdoutSpy)).toBe(
|
|
`${JSON.stringify(
|
|
{
|
|
topics: [
|
|
{ name: 'alpha', description: 'Use when alpha work is needed.' },
|
|
{ name: 'zeta', description: 'Use when zeta work spans lines.' }
|
|
]
|
|
},
|
|
null,
|
|
2
|
|
)}\n`
|
|
)
|
|
})
|
|
|
|
it('gives alias get --json the canonical name, selection, and Markdown', async () => {
|
|
const stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
|
|
|
|
await main(['skills', 'get', 'legacy-alpha', '--full', '--json'], '/tmp/repo')
|
|
|
|
expect(stdoutText(stdoutSpy)).toBe(
|
|
`${JSON.stringify(
|
|
{
|
|
name: 'alpha',
|
|
full: true,
|
|
markdown: '# Alpha\n\nShort.\n\n## References\n\nFull.\n'
|
|
},
|
|
null,
|
|
2
|
|
)}\n`
|
|
)
|
|
})
|
|
|
|
it('shows leaf, group, and root help for skills', async () => {
|
|
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
|
|
await main(['skills', 'get', '--help'], '/tmp/repo')
|
|
await main(['skills', '--help'], '/tmp/repo')
|
|
await main(['--help'], '/tmp/repo')
|
|
|
|
expect(String(logSpy.mock.calls[0]?.[0])).toContain(
|
|
'Usage: orca skills get <topic> [--full] [--json]'
|
|
)
|
|
expect(String(logSpy.mock.calls[1]?.[0])).toContain(
|
|
'Commands:\n list List version-matched skill guides'
|
|
)
|
|
expect(String(logSpy.mock.calls[1]?.[0])).toContain(
|
|
'get Print a version-matched skill guide'
|
|
)
|
|
expect(String(logSpy.mock.calls[2]?.[0])).toContain('Skills:\n skills list')
|
|
expect(runtimeClientConstructorMock).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('returns a nonzero error with all canonical topics for an unknown topic', async () => {
|
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
await main(['skills', 'get', 'missing'], '/tmp/repo')
|
|
|
|
expect(process.exitCode).toBe(1)
|
|
expect(errorSpy).toHaveBeenCalledWith(
|
|
'Unknown skill topic "missing". Available topics: alpha, zeta'
|
|
)
|
|
expect(runtimeClientConstructorMock).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
function stdoutText(spy: ReturnType<typeof vi.spyOn>): string {
|
|
return spy.mock.calls.map((call) => String(call[0])).join('')
|
|
}
|