Files
orca/src/cli/skills.test.ts
Brennan Benson 31f643ca42 Add version-matched skill guides to the CLI (#8624)
* 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.
2026-07-14 02:17:55 -07:00

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('')
}