Files
orca/src/cli/command-suggestion.test.ts
e2b4bc2c2c feat(cli): make the CLI self-correcting and self-describing for agents (#6303)
* feat(cli): make the CLI self-correcting and self-describing for agents

Agents build a generalized model of how CLIs work and apply it to every
tool. When orca diverged — `rm` where git uses `remove` — a reasonable
first guess (`orca worktree remove`) dead-ended on a bare "Unknown
command" with no path forward. This makes the CLI degrade gracefully when
the orca-cli skill isn't loaded in context.

- First-class CommandSpec.aliases, resolved to the canonical path before
  dispatch (no new handler registrations). `worktree remove`/`delete` now
  resolve to `rm`; the ad-hoc `terminal focus` duplicate spec/handler is
  migrated onto the mechanism.
- Did-you-mean suggestions on unknown commands and unknown flags, ranked
  by edit distance over the live registry, surfaced in both stderr and
  --json error.data (reusing the existing nextSteps channel).
- `orca agent-context [--json]`: a versioned, machine-readable dump of the
  command schema. Pure local read (no RPC), so it works over SSH and when
  the app isn't running.
- CI guards: specs<->handlers parity, and a vocabulary policy that fails
  on new off-policy deletion/read verbs (existing ones grandfathered).

* Address PR review feedback (#6303)

- agent-context now emits each command's effective flag set (globals +
  conditional --page), not just allowedFlags, so the schema no longer
  under-reports --json/--help. Shared as effectiveAllowedFlags() between
  validation and the schema.
- Collision check now covers alias paths too, so a duplicate alias that
  would silently shadow a real command fails the build.

* fix(cli): harden agent recovery and introspection

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
Co-authored-by: Orca <help@stably.ai>
2026-07-10 19:17:01 -07:00

86 lines
2.6 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { CommandSpec } from './args'
import { levenshtein, suggestCommands, unknownCommandData } from './command-suggestion'
const specs: CommandSpec[] = [
{
path: ['worktree', 'rm'],
aliases: [['worktree', 'remove']],
summary: 'Remove a worktree',
usage: 'orca worktree rm',
allowedFlags: []
},
{
path: ['worktree', 'list'],
summary: 'List worktrees',
usage: 'orca worktree list',
allowedFlags: []
},
{
path: ['terminal', 'send'],
summary: 'Send input',
usage: 'orca terminal send',
allowedFlags: []
}
]
describe('levenshtein', () => {
it('returns 0 for identical strings', () => {
expect(levenshtein('rm', 'rm')).toBe(0)
})
it('counts single-edit distance', () => {
expect(levenshtein('remov', 'remove')).toBe(1)
})
it('handles empty operands', () => {
expect(levenshtein('', 'abc')).toBe(3)
expect(levenshtein('abc', '')).toBe(3)
})
})
describe('suggestCommands', () => {
it('suggests the closest command for a near-miss verb', () => {
expect(suggestCommands(specs, ['worktree', 'remov'])).toContain('worktree rm')
})
it('includes alias paths among suggestions', () => {
// `worktree remove` is the alias; a typo near it should surface it (an exact
// match would resolve as a real command, not trigger a suggestion).
expect(suggestCommands(specs, ['worktree', 'remov'])).toContain('worktree remove')
})
it('returns nothing for a wildly-off token', () => {
expect(suggestCommands(specs, ['worktree', 'zzzzz'])).toEqual([])
})
it('only considers commands of the same depth', () => {
expect(suggestCommands(specs, ['worktree', 'list', 'extra'])).toEqual([])
})
it('suggests a top-level command group near-miss', () => {
expect(suggestCommands(specs, ['worktre'])).toEqual(['worktree'])
})
it('ranks closer matches first', () => {
const result = suggestCommands(specs, ['terminal', 'sen'])
expect(result[0]).toBe('terminal send')
})
})
describe('unknownCommandData', () => {
it('produces a human nextSteps line when a suggestion exists', () => {
const data = unknownCommandData(specs, ['worktree', 'remov'])
expect(data.suggestions).toContain('worktree rm')
expect(data.nextSteps[0]).toContain('Did you mean')
expect(data.nextSteps[0]).toContain('orca worktree rm')
})
it('produces empty nextSteps when nothing is close', () => {
const data = unknownCommandData(specs, ['worktree', 'zzzzz'])
expect(data.suggestions).toEqual([])
expect(data.nextSteps).toEqual([])
})
})