Files
hapi/cli/src/commands/agentCommandOptions.test.ts
T

246 lines
9.6 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { AGY_PERMISSION_MODES, GEMINI_PERMISSION_MODES, OPENCODE_PERMISSION_MODES } from '@hapi/protocol/modes'
import { parseRemoteAgentCommandOptions } from './agentCommandOptions'
describe('parseRemoteAgentCommandOptions', () => {
it('maps --yolo to the flavor\'s own auto-approve mode, not a hardcoded "yolo"', () => {
// agy renamed its auto-approve mode to 'always-proceed'; 'yolo' is not a
// valid agy mode, so --yolo must resolve to the flavor's equivalent.
expect(parseRemoteAgentCommandOptions(['--yolo'], AGY_PERMISSION_MODES).permissionMode)
.toBe('always-proceed')
// Flavors that DO have a 'yolo' mode keep it.
expect(parseRemoteAgentCommandOptions(['--yolo'], OPENCODE_PERMISSION_MODES).permissionMode)
.toBe('yolo')
})
it('parses --hapi-session-id into existingSessionId (pty reopen id reuse)', () => {
// The runner emits --hapi-session-id when reopening a pty session so the
// child reuses the existing hub row. agy (this shared parser) must consume
// it — else the flag is silently dropped and reopen mints a new id + 404.
expect(parseRemoteAgentCommandOptions(['--hapi-session-id', 'hub-id-1'], AGY_PERMISSION_MODES).existingSessionId)
.toBe('hub-id-1')
})
it('parses common remote agent flags', () => {
expect(parseRemoteAgentCommandOptions([
'--started-by', 'runner',
'--hapi-starting-mode', 'remote',
'--existing-session-id', 'hapi-session-1',
'--permission-mode', 'yolo',
'--resume', 'session-1',
'--model', 'model-a'
], GEMINI_PERMISSION_MODES)).toEqual({
startedBy: 'runner',
startingMode: 'remote',
existingSessionId: 'hapi-session-1',
permissionMode: 'yolo',
resumeSessionId: 'session-1',
model: 'model-a'
})
})
it('accepts pty as the agy runner starting mode', () => {
expect(parseRemoteAgentCommandOptions([
'--started-by', 'runner',
'--hapi-starting-mode', 'pty',
], AGY_PERMISSION_MODES, ['local', 'remote', 'pty'])).toMatchObject({
startedBy: 'runner',
startingMode: 'pty',
})
})
it('parses --existing-session-id and rejects a missing value', () => {
expect(parseRemoteAgentCommandOptions([
'--existing-session-id', 'preallocated-hapi-id'
], OPENCODE_PERMISSION_MODES).existingSessionId).toBe('preallocated-hapi-id')
expect(() => parseRemoteAgentCommandOptions([
'--existing-session-id'
], OPENCODE_PERMISSION_MODES)).toThrow('Missing --existing-session-id value')
})
it('does not let --yolo override an explicit permission mode that appeared first', () => {
expect(parseRemoteAgentCommandOptions([
'--permission-mode', 'default',
'--yolo'
], OPENCODE_PERMISSION_MODES).permissionMode).toBe('default')
})
it('accepts OpenCode plan permission mode', () => {
expect(parseRemoteAgentCommandOptions([
'--permission-mode',
'plan'
], OPENCODE_PERMISSION_MODES).permissionMode).toBe('plan')
})
it('keeps current unknown-arg behavior by ignoring unrecognized flags', () => {
expect(parseRemoteAgentCommandOptions([
'--unknown',
'value',
'--model',
'model-a'
], GEMINI_PERMISSION_MODES)).toEqual({
model: 'model-a'
})
})
it('rejects invalid constrained values', () => {
expect(() => parseRemoteAgentCommandOptions([
'--hapi-starting-mode',
'sideways'
], GEMINI_PERMISSION_MODES)).toThrow('Invalid --hapi-starting-mode')
expect(() => parseRemoteAgentCommandOptions([
'--permission-mode',
'bypassPermissions'
], GEMINI_PERMISSION_MODES)).toThrow('Invalid --permission-mode value')
})
it('parses model reasoning effort', () => {
expect(parseRemoteAgentCommandOptions([
'--model-reasoning-effort',
'high'
], OPENCODE_PERMISSION_MODES).modelReasoningEffort).toBe('high')
})
it('requires values for resume and model flags', () => {
expect(() => parseRemoteAgentCommandOptions(['--resume'], OPENCODE_PERMISSION_MODES)).toThrow('Missing --resume value')
expect(() => parseRemoteAgentCommandOptions(['--model'], OPENCODE_PERMISSION_MODES)).toThrow('Missing --model value')
expect(() => parseRemoteAgentCommandOptions(['--model-reasoning-effort'], OPENCODE_PERMISSION_MODES)).toThrow('Missing --model-reasoning-effort value')
expect(() => parseRemoteAgentCommandOptions(['--existing-session-id'], OPENCODE_PERMISSION_MODES)).toThrow('Missing --existing-session-id value')
})
it('accepts OpenCode-native -s / --session as resume aliases', () => {
expect(parseRemoteAgentCommandOptions(
['-s', 'opencode-sess-1'],
OPENCODE_PERMISSION_MODES
).resumeSessionId).toBe('opencode-sess-1')
expect(parseRemoteAgentCommandOptions(
['--session', 'opencode-sess-2'],
OPENCODE_PERMISSION_MODES
).resumeSessionId).toBe('opencode-sess-2')
})
it('rejects -s / --session with no value', () => {
expect(() => parseRemoteAgentCommandOptions(['-s'], OPENCODE_PERMISSION_MODES)).toThrow('Missing -s value')
expect(() => parseRemoteAgentCommandOptions(['--session'], OPENCODE_PERMISSION_MODES)).toThrow('Missing --session value')
})
it('a later -s overrides a prior --resume (last-write-wins)', () => {
expect(parseRemoteAgentCommandOptions(
['--resume', 'first', '-s', 'second'],
OPENCODE_PERMISSION_MODES
).resumeSessionId).toBe('second')
})
})
describe('parseRemoteAgentCommandOptions — pi flavor', () => {
// Pi RPC mode has no permission switching, so the command passes an empty
// allow-list. These tests cover the non-permission flags using a non-empty
// allow-list purely as a parser fixture — the parser's behavior is
// independent of the modes' contents.
const ALLOWED = OPENCODE_PERMISSION_MODES
it('accepts --model and stores it on options', () => {
const result = parseRemoteAgentCommandOptions(
['--model', 'claude-sonnet-4-5'],
ALLOWED
)
expect(result.model).toBe('claude-sonnet-4-5')
})
it('--session-id stores the value as resumeSessionId (Pi-specific flag)', () => {
// Pi uses --session-id for exact session resume (RPC mode), not the
// generic --resume that other flavors use.
const result = parseRemoteAgentCommandOptions(
['--session-id', 'pi-sess-123'],
ALLOWED
)
expect(result.resumeSessionId).toBe('pi-sess-123')
})
it('--resume is also accepted as an alias for session resume', () => {
// Some flavor paths pass --resume; the parser should accept it
// uniformly so callers do not need to branch on flavor.
const result = parseRemoteAgentCommandOptions(
['--resume', 'sess-id'],
ALLOWED
)
expect(result.resumeSessionId).toBe('sess-id')
})
it('a later --resume overrides a prior --session-id (last-write-wins)', () => {
const result = parseRemoteAgentCommandOptions(
['--session-id', 'first', '--resume', 'second'],
ALLOWED
)
expect(result.resumeSessionId).toBe('second')
})
it('rejects --session-id with no value', () => {
expect(() => parseRemoteAgentCommandOptions(
['--session-id'],
ALLOWED
)).toThrow('Missing --session-id value')
})
it('parses --started-by runner', () => {
const result = parseRemoteAgentCommandOptions(
['--started-by', 'runner'],
ALLOWED
)
expect(result.startedBy).toBe('runner')
})
it('parses --started-by terminal', () => {
const result = parseRemoteAgentCommandOptions(
['--started-by', 'terminal'],
ALLOWED
)
expect(result.startedBy).toBe('terminal')
})
it('parses --hapi-starting-mode remote', () => {
const result = parseRemoteAgentCommandOptions(
['--hapi-starting-mode', 'remote'],
ALLOWED
)
expect(result.startingMode).toBe('remote')
})
it('parses --hapi-starting-mode local', () => {
const result = parseRemoteAgentCommandOptions(
['--hapi-starting-mode', 'local'],
ALLOWED
)
expect(result.startingMode).toBe('local')
})
it('rejects invalid --hapi-starting-mode', () => {
expect(() => parseRemoteAgentCommandOptions(
['--hapi-starting-mode', 'invalid'],
ALLOWED
)).toThrow('Invalid --hapi-starting-mode')
})
it('handles a full pi invocation end-to-end', () => {
const result = parseRemoteAgentCommandOptions(
[
'--started-by', 'runner',
'--hapi-starting-mode', 'remote',
'--model', 'claude-sonnet-4-5',
'--session-id', 'pi-sess-full',
'--existing-session-id', 'hapi-session-pi-full',
],
ALLOWED
)
expect(result).toEqual({
startedBy: 'runner',
startingMode: 'remote',
model: 'claude-sonnet-4-5',
resumeSessionId: 'pi-sess-full',
existingSessionId: 'hapi-session-pi-full',
})
})
})