mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* fix(claude): keep Orca's background Claude runs off the user's MCP servers and hooks The model picker and / menu listings, chat naming, and commit message, PR, branch and Jira text generation each start a short Claude run. Every one but the / probe loaded the user's MCP servers and hooks, so each run started every server (one cut off a Notion OAuth refresh and lost the sign-in) and fired SessionStart hooks. Every background run now passes --strict-mcp-config and disableAllHooks. A user's own inline --settings merges into the one settings value Claude keeps, and their --mcp-config still loads. When Claude refuses strict mode because an enterprise MCP config is present (which already keeps user servers out), the run is retried once without the flag. Tracked in STA-11095 and #30411. * fix: centralize background Claude generation and retry policy * fix: rebuild Claude background retries from generated arguments * test: include branch in generation retry fixture * fix: preserve user settings arguments in background generation * test: define retry fixture streams across runtimes
891 lines
23 KiB
TypeScript
891 lines
23 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { planCommitMessageGeneration, planAgentBinary } from './commit-message-plan'
|
|
|
|
describe('planCommitMessageGeneration', () => {
|
|
it('keeps extension-provided Pi models available in generated Git text plans', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{ agentId: 'pi', model: 'local-extension/model' },
|
|
'Write a commit message'
|
|
)
|
|
expect(result.ok).toBe(true)
|
|
if (!result.ok) {
|
|
throw new Error(result.error)
|
|
}
|
|
expect(result.plan.args).not.toContain('--no-extensions')
|
|
expect(result.plan.args).toEqual(
|
|
expect.arrayContaining([
|
|
'--no-session',
|
|
'--no-tools',
|
|
'--no-skills',
|
|
'--no-context-files',
|
|
'--model',
|
|
'local-extension/model'
|
|
])
|
|
)
|
|
expect(result.plan.stdinPayload).toBe('Write a commit message')
|
|
})
|
|
|
|
it('plans Claude non-interactive generation with the prompt on stdin only', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'claude',
|
|
model: 'sonnet',
|
|
thinkingLevel: 'high'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
plan: {
|
|
binary: 'claude',
|
|
args: [
|
|
'-p',
|
|
'--output-format',
|
|
'text',
|
|
'--model',
|
|
'sonnet',
|
|
'--permission-mode',
|
|
'plan',
|
|
'--effort',
|
|
'high',
|
|
'--strict-mcp-config',
|
|
'--settings',
|
|
'{"disableAllHooks":true}'
|
|
],
|
|
stdinPayload: 'PROMPT',
|
|
label: 'Claude'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('preserves a literal --settings prompt value without generating hooks settings', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{ agentId: 'claude', model: 'sonnet', agentArgs: '--append-system-prompt "--settings"' },
|
|
'PROMPT'
|
|
)
|
|
expect(result.ok && result.plan.args.slice(-3)).toEqual([
|
|
'--strict-mcp-config',
|
|
'--append-system-prompt',
|
|
'--settings'
|
|
])
|
|
expect(result.ok && result.plan.args).not.toContain('{"disableAllHooks":true}')
|
|
})
|
|
|
|
it('plans OpenCode run with prompt on stdin and model variant', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'opencode',
|
|
model: 'opencode/gpt-5.4-mini',
|
|
thinkingLevel: 'high'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
plan: {
|
|
binary: 'opencode',
|
|
args: [
|
|
'run',
|
|
'--model',
|
|
'opencode/gpt-5.4-mini',
|
|
'--agent',
|
|
'build',
|
|
'--format',
|
|
'json',
|
|
'--variant',
|
|
'high'
|
|
],
|
|
stdinPayload: 'PROMPT',
|
|
label: 'OpenCode',
|
|
outputFormat: 'opencode-json'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('keeps OpenCode preset command overrides while sending the prompt on stdin', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'opencode',
|
|
model: 'opencode/gpt-5.4-mini',
|
|
agentCommandOverride: 'npx opencode'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
plan: {
|
|
binary: 'npx',
|
|
args: [
|
|
'opencode',
|
|
'run',
|
|
'--model',
|
|
'opencode/gpt-5.4-mini',
|
|
'--agent',
|
|
'build',
|
|
'--format',
|
|
'json'
|
|
],
|
|
stdinPayload: 'PROMPT',
|
|
label: 'OpenCode',
|
|
outputFormat: 'opencode-json'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('plans Amp execute generation without the removed archive flag', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'amp',
|
|
model: 'large',
|
|
thinkingLevel: 'medium'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
plan: {
|
|
binary: 'amp',
|
|
args: [
|
|
'--execute',
|
|
'--no-notifications',
|
|
'--no-ide',
|
|
'--no-jetbrains',
|
|
'--mode',
|
|
'large',
|
|
'--effort',
|
|
'medium'
|
|
],
|
|
stdinPayload: 'PROMPT',
|
|
label: 'Amp'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('plans Jcode run generation with an argv prompt', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'jcode',
|
|
model: 'default'
|
|
},
|
|
'name this branch'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
plan: {
|
|
binary: 'jcode',
|
|
// --tool-profile none: the prompt is a staged patch, and jcode's default
|
|
// profile would expose shell/read/write/MCP to it.
|
|
args: [
|
|
'--no-update',
|
|
'--quiet',
|
|
'--no-selfdev',
|
|
'--tool-profile',
|
|
'none',
|
|
'run',
|
|
'--json',
|
|
'name this branch'
|
|
],
|
|
stdinPayload: null,
|
|
label: 'Jcode'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('allows discovered dynamic models that are not in the seed catalog', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'cursor',
|
|
model: 'gpt-5.2',
|
|
thinkingLevel: 'xhigh'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
plan: {
|
|
binary: 'cursor-agent',
|
|
args: [
|
|
'--print',
|
|
'--mode',
|
|
'ask',
|
|
'--trust',
|
|
'--output-format',
|
|
'text',
|
|
'--model',
|
|
'gpt-5.2',
|
|
'PROMPT'
|
|
],
|
|
stdinPayload: null,
|
|
label: 'Cursor'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('plans Antigravity generation with the prompt attached to --print, not stdin (#19539, #14059)', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'antigravity',
|
|
model: 'Gemini 3.5 Flash (Medium)'
|
|
},
|
|
'real commit prompt'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
plan: {
|
|
binary: 'agy',
|
|
args: ['--print=real commit prompt', '--sandbox', '--model', 'Gemini 3.5 Flash (Medium)'],
|
|
stdinPayload: null,
|
|
label: 'Antigravity'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('keeps a leading-dash Antigravity prompt bound to --print instead of parsing as an option', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{ agentId: 'antigravity', model: 'Gemini 3.5 Flash (Medium)' },
|
|
'-fix: something'
|
|
)
|
|
|
|
expect(result.ok).toBe(true)
|
|
expect(result.ok && result.plan.args.slice(0, 2)).toEqual([
|
|
'--print=-fix: something',
|
|
'--sandbox'
|
|
])
|
|
})
|
|
|
|
// Why: pins argv construction only. Real agy 1.2.1 separately rejects a --print value
|
|
// that exactly matches a registered flag name (its own heuristic, independent of this
|
|
// fix) — verified `agy --print=--sandbox` still errors there. Real prompts are never
|
|
// literally a bare flag name, so this doesn't affect actual generation.
|
|
it('still glues an Antigravity prompt that collides with a flag name onto --print', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{ agentId: 'antigravity', model: 'Gemini 3.5 Flash (Medium)' },
|
|
'--sandbox'
|
|
)
|
|
|
|
expect(result.ok).toBe(true)
|
|
expect(result.ok && result.plan.args.slice(0, 2)).toEqual(['--print=--sandbox', '--sandbox'])
|
|
})
|
|
|
|
// Why: agy has no documented stdin mode for --print (#19539's body: "--print ... is
|
|
// not a boolean flag that automatically reads from stdin; it expects the prompt
|
|
// string as its option argument"), so a large staged patch now rides on argv. This
|
|
// is the same unguarded argv delivery cursor/kimi/copilot already use (see the
|
|
// parity assertion below) — pinned here as a known property, not a regression.
|
|
it('puts a large Antigravity prompt on argv with no size guard, same as other argv-delivery agents', () => {
|
|
const bigPrompt = 'y'.repeat(70_000)
|
|
const result = planCommitMessageGeneration(
|
|
{ agentId: 'antigravity', model: 'Gemini 3.5 Flash (Medium)' },
|
|
bigPrompt
|
|
)
|
|
|
|
expect(result.ok).toBe(true)
|
|
expect(result.ok && result.plan.args[0]).toBe(`--print=${bigPrompt}`)
|
|
expect(result.ok && result.plan.stdinPayload).toBeNull()
|
|
|
|
const cursorResult = planCommitMessageGeneration(
|
|
{ agentId: 'cursor', model: 'auto' },
|
|
bigPrompt
|
|
)
|
|
expect(cursorResult.ok).toBe(true)
|
|
expect(cursorResult.ok && cursorResult.plan.args.at(-1)).toBe(bigPrompt)
|
|
expect(cursorResult.ok && cursorResult.plan.stdinPayload).toBeNull()
|
|
})
|
|
|
|
// Why: real #14059 reproduction config — CLI arguments field repeats --model and adds
|
|
// --add-dir/--effort/--dangerously-skip-permissions. Confirms none of it gets swallowed
|
|
// into the --print operand and the duplicate --model is deduped the same way every
|
|
// other spec's recipe args already are (DEFAULT_SINGLETON_OPTIONS, unaffected by
|
|
// argument order).
|
|
it('keeps #14059-style recipe CLI arguments intact and deduped around the print operand', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'antigravity',
|
|
model: 'Gemini 3.5 Flash (Medium)',
|
|
agentArgs:
|
|
'--add-dir . --model gemini-3.6-flash --effort low --dangerously-skip-permissions'
|
|
},
|
|
'Generate a concise git commit message for the currently staged changes.'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
plan: {
|
|
binary: 'agy',
|
|
args: [
|
|
'--print=Generate a concise git commit message for the currently staged changes.',
|
|
'--sandbox',
|
|
'--model',
|
|
'gemini-3.6-flash',
|
|
'--add-dir',
|
|
'.',
|
|
'--effort',
|
|
'low',
|
|
'--dangerously-skip-permissions'
|
|
],
|
|
stdinPayload: null,
|
|
label: 'Antigravity'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('plans Codex exec as non-interactive read-only generation with the prompt on stdin only', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'codex',
|
|
model: 'gpt-5.4-mini',
|
|
thinkingLevel: 'medium'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
plan: {
|
|
binary: 'codex',
|
|
args: [
|
|
'exec',
|
|
'--ephemeral',
|
|
'--skip-git-repo-check',
|
|
'-s',
|
|
'read-only',
|
|
'--model',
|
|
'gpt-5.4-mini',
|
|
'-c',
|
|
'model_reasoning_effort=medium'
|
|
],
|
|
stdinPayload: 'PROMPT',
|
|
label: 'Codex'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('plans Muse exec with a positional prompt and no workspace side effects', () => {
|
|
const result = planCommitMessageGeneration({ agentId: 'muse', model: 'default' }, 'PROMPT')
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
plan: {
|
|
binary: 'muse',
|
|
args: [
|
|
'exec',
|
|
'--no-session-log',
|
|
'--approval-mode',
|
|
'never',
|
|
'--disable-sandbox',
|
|
'--disable-shell',
|
|
'--disable-write',
|
|
'--disable-web-tools',
|
|
'--',
|
|
'PROMPT'
|
|
],
|
|
stdinPayload: null,
|
|
label: 'Muse'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('uses preset agent command overrides as the spawn command prefix', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'codex',
|
|
model: 'gpt-5.4-mini',
|
|
agentCommandOverride: 'npx codex'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
plan: {
|
|
binary: 'npx',
|
|
args: [
|
|
'codex',
|
|
'exec',
|
|
'--ephemeral',
|
|
'--skip-git-repo-check',
|
|
'-s',
|
|
'read-only',
|
|
'--model',
|
|
'gpt-5.4-mini'
|
|
],
|
|
stdinPayload: 'PROMPT'
|
|
}
|
|
})
|
|
})
|
|
|
|
it.each([
|
|
['long option', '--model gpt-5.6-luna', ['--model', 'gpt-5.6-luna'], []],
|
|
['short option', '-m gpt-5.6-luna', ['-m', 'gpt-5.6-luna'], []],
|
|
['equals form', '--model=gpt-5.6-luna', ['--model=gpt-5.6-luna'], []],
|
|
['attached short form', '-mgpt-5.6-luna', ['-mgpt-5.6-luna'], []],
|
|
[
|
|
'sibling arguments',
|
|
'--model gpt-5.6-luna --sandbox read-only',
|
|
['--model', 'gpt-5.6-luna'],
|
|
['--sandbox', 'read-only']
|
|
]
|
|
])(
|
|
'lets Codex recipe args override the generated model via %s',
|
|
(_, agentArgs, overrideArgs, trailingArgs) => {
|
|
const result = planCommitMessageGeneration(
|
|
{ agentId: 'codex', model: 'gpt-5.4-mini', thinkingLevel: 'medium', agentArgs },
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
plan: {
|
|
args: [
|
|
'exec',
|
|
'--ephemeral',
|
|
'--skip-git-repo-check',
|
|
'-s',
|
|
'read-only',
|
|
...overrideArgs,
|
|
'-c',
|
|
'model_reasoning_effort=medium',
|
|
...trailingArgs
|
|
],
|
|
stdinPayload: 'PROMPT'
|
|
}
|
|
})
|
|
}
|
|
)
|
|
|
|
it('keeps Codex recipe arguments unchanged when they do not override the model', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'codex',
|
|
model: 'gpt-5.4-mini',
|
|
agentArgs: '--sandbox workspace-write'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
plan: {
|
|
args: [
|
|
'exec',
|
|
'--ephemeral',
|
|
'--skip-git-repo-check',
|
|
'-s',
|
|
'read-only',
|
|
'--model',
|
|
'gpt-5.4-mini',
|
|
'--sandbox',
|
|
'workspace-write'
|
|
]
|
|
}
|
|
})
|
|
})
|
|
|
|
it('keeps the generated Codex model when model-like text follows an option terminator', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'codex',
|
|
model: 'gpt-5.4-mini',
|
|
agentArgs: '-- --model literal'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
plan: {
|
|
args: [
|
|
'exec',
|
|
'--ephemeral',
|
|
'--skip-git-repo-check',
|
|
'-s',
|
|
'read-only',
|
|
'--model',
|
|
'gpt-5.4-mini',
|
|
'--',
|
|
'--model',
|
|
'literal'
|
|
]
|
|
}
|
|
})
|
|
})
|
|
|
|
it('lets OpenCode recipe args override the generated model instead of repeating it', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'opencode',
|
|
model: 'opencode/gpt-5.4-mini',
|
|
agentArgs: '--model opencode/gpt-5.5'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
plan: {
|
|
args: ['run', '--model', 'opencode/gpt-5.5', '--agent', 'build', '--format', 'json'],
|
|
stdinPayload: 'PROMPT'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('overrides the generated OpenCode model from a short-form recipe alias', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{ agentId: 'opencode', model: 'opencode/gpt-5.4-mini', agentArgs: '-m opencode/gpt-5.5' },
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
plan: {
|
|
args: ['run', '-m', 'opencode/gpt-5.5', '--agent', 'build', '--format', 'json']
|
|
}
|
|
})
|
|
})
|
|
|
|
it('overrides OpenCode singleton flags beyond the model', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'opencode',
|
|
model: 'opencode/gpt-5.4-mini',
|
|
thinkingLevel: 'high',
|
|
agentArgs: '--agent plan --format json --variant low'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
plan: {
|
|
args: [
|
|
'run',
|
|
'--model',
|
|
'opencode/gpt-5.4-mini',
|
|
'--agent',
|
|
'plan',
|
|
'--format',
|
|
'json',
|
|
'--variant',
|
|
'low'
|
|
]
|
|
}
|
|
})
|
|
})
|
|
|
|
it('appends per-action CLI arguments that do not repeat a generated OpenCode flag', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{ agentId: 'opencode', model: 'opencode/gpt-5.4-mini', agentArgs: '--share' },
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
plan: {
|
|
args: [
|
|
'run',
|
|
'--model',
|
|
'opencode/gpt-5.4-mini',
|
|
'--agent',
|
|
'build',
|
|
'--format',
|
|
'json',
|
|
'--share'
|
|
],
|
|
stdinPayload: 'PROMPT'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('collapses a singleton flag the user typed twice in one field', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'opencode',
|
|
model: 'opencode/gpt-5.4-mini',
|
|
agentArgs: '--model opencode/first -m opencode/second'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
plan: {
|
|
args: ['run', '--model', 'opencode/first', '--agent', 'build', '--format', 'json']
|
|
}
|
|
})
|
|
})
|
|
|
|
it('overrides the generated Amp mode rather than repeating it', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{ agentId: 'amp', model: 'smart', agentArgs: '--mode rush' },
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
plan: {
|
|
args: ['--execute', '--no-notifications', '--no-ide', '--no-jetbrains', '--mode', 'rush']
|
|
}
|
|
})
|
|
})
|
|
|
|
it('keeps a model flag in the agent command override and removes the generated duplicate', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'opencode',
|
|
model: 'opencode/gpt-5.4-mini',
|
|
agentCommandOverride: 'npx opencode --model opencode/gpt-5.5 --log-level DEBUG'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
plan: {
|
|
binary: 'npx',
|
|
args: [
|
|
'opencode',
|
|
'--model',
|
|
'opencode/gpt-5.5',
|
|
'--log-level',
|
|
'DEBUG',
|
|
'run',
|
|
'--agent',
|
|
'build',
|
|
'--format',
|
|
'json'
|
|
]
|
|
}
|
|
})
|
|
})
|
|
|
|
it('does not move command override options across an option terminator', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'opencode',
|
|
model: 'opencode/gpt-5.4-mini',
|
|
agentCommandOverride: 'opencode --model opencode/from-override -- --model literal'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
plan: {
|
|
args: [
|
|
'--model',
|
|
'opencode/from-override',
|
|
'--',
|
|
'--model',
|
|
'literal',
|
|
'run',
|
|
'--model',
|
|
'opencode/gpt-5.4-mini',
|
|
'--agent',
|
|
'build',
|
|
'--format',
|
|
'json'
|
|
]
|
|
}
|
|
})
|
|
})
|
|
|
|
it('lets recipe args outrank a command override that also sets the model', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'opencode',
|
|
model: 'opencode/gpt-5.4-mini',
|
|
agentCommandOverride: 'opencode --model opencode/from-override',
|
|
agentArgs: '--model opencode/from-recipe'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
plan: {
|
|
binary: 'opencode',
|
|
args: ['run', '--model', 'opencode/from-recipe', '--agent', 'build', '--format', 'json']
|
|
}
|
|
})
|
|
})
|
|
|
|
it('keeps custom per-action CLI arguments before a positional prompt', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'custom',
|
|
model: '',
|
|
customAgentCommand: 'agent --message {prompt}',
|
|
agentArgs: '--model gpt-5.5'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
ok: true,
|
|
plan: {
|
|
binary: 'agent',
|
|
args: ['--message', '--model', 'gpt-5.5', 'PROMPT'],
|
|
stdinPayload: null,
|
|
label: 'agent'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('appends custom per-action CLI arguments when the prompt is sent on stdin', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'custom',
|
|
model: '',
|
|
customAgentCommand: 'agent --message',
|
|
agentArgs: '--model gpt-5.5'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toMatchObject({
|
|
ok: true,
|
|
plan: {
|
|
args: ['--message', '--model', 'gpt-5.5'],
|
|
stdinPayload: 'PROMPT'
|
|
}
|
|
})
|
|
})
|
|
|
|
it('rejects invalid per-action CLI arguments before spawning', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'claude',
|
|
model: 'haiku',
|
|
agentArgs: '--model "unterminated'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
ok: false,
|
|
error: 'CLI arguments are invalid: Unclosed quote in command template.'
|
|
})
|
|
})
|
|
|
|
it('rejects invalid preset agent command overrides before spawning', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'claude',
|
|
model: 'haiku',
|
|
agentCommandOverride: 'claude "unterminated'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
ok: false,
|
|
error: 'Agent command override is invalid: Unclosed quote in command template.'
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('backslash mode reaches every command the user can type (#11375)', () => {
|
|
const WINDOWS_BINARY = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
|
|
|
|
it('keeps an agent command override intact in literal mode', () => {
|
|
const posix = planAgentBinary('claude', WINDOWS_BINARY)
|
|
const literal = planAgentBinary('claude', WINDOWS_BINARY, 'literal')
|
|
|
|
// The bug: POSIX escaping eats every separator, so the binary is not found.
|
|
expect(posix.ok && posix.binary).toBe('C:WindowsSystem32WindowsPowerShellv1.0powershell.exe')
|
|
expect(literal.ok && literal.binary).toBe(WINDOWS_BINARY)
|
|
})
|
|
|
|
it('keeps a quoted path containing spaces intact in literal mode', () => {
|
|
const literal = planAgentBinary('claude', '"C:\\Program Files\\nodejs\\node.exe"', 'literal')
|
|
|
|
expect(literal.ok && literal.binary).toBe('C:\\Program Files\\nodejs\\node.exe')
|
|
})
|
|
|
|
it('keeps extra CLI args intact through planCommitMessageGeneration', () => {
|
|
const plan = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'claude',
|
|
model: 'sonnet',
|
|
agentCommandOverride: WINDOWS_BINARY,
|
|
agentArgs: '--config C:\\Users\\me\\.claude.json',
|
|
backslash: 'literal'
|
|
},
|
|
'prompt'
|
|
)
|
|
|
|
expect(plan.ok && plan.plan.binary).toBe(WINDOWS_BINARY)
|
|
expect(plan.ok && plan.plan.args).toContain('C:\\Users\\me\\.claude.json')
|
|
})
|
|
|
|
it('defaults to POSIX escaping when no mode is given', () => {
|
|
const plan = planCommitMessageGeneration(
|
|
{ agentId: 'claude', model: 'sonnet', agentArgs: '--dir /my\\ dir' },
|
|
'prompt'
|
|
)
|
|
|
|
expect(plan.ok && plan.plan.args).toContain('/my dir')
|
|
})
|
|
})
|
|
|
|
describe('OpenCode format metadata respects option terminators', () => {
|
|
it.each(['opencode', 'opencode2'] as const)(
|
|
'ignores literal recipe format values for %s',
|
|
(agentId) => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId,
|
|
model: 'opencode/gpt-5.4-mini',
|
|
agentArgs: '--format default -- --format json'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
expect(result.ok).toBe(true)
|
|
if (!result.ok) {
|
|
throw new Error(result.error)
|
|
}
|
|
expect(result.plan.args).toContain('--')
|
|
expect(result.plan.outputFormat).toBeUndefined()
|
|
}
|
|
)
|
|
|
|
it('ignores literal equals-form flags after a command override terminator', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'opencode',
|
|
model: 'opencode/gpt-5.4-mini',
|
|
agentCommandOverride: 'opencode --format default -- --format=json'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
expect(result.ok).toBe(true)
|
|
if (!result.ok) {
|
|
throw new Error(result.error)
|
|
}
|
|
expect(result.plan.outputFormat).toBeUndefined()
|
|
})
|
|
|
|
it('retains JSON metadata for the active option before a literal default value', () => {
|
|
const result = planCommitMessageGeneration(
|
|
{
|
|
agentId: 'opencode',
|
|
model: 'opencode/gpt-5.4-mini',
|
|
agentArgs: '--format=json -- --format default'
|
|
},
|
|
'PROMPT'
|
|
)
|
|
expect(result.ok).toBe(true)
|
|
if (!result.ok) {
|
|
throw new Error(result.error)
|
|
}
|
|
expect(result.plan.outputFormat).toBe('opencode-json')
|
|
})
|
|
})
|