mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 16:02:03 +00:00
* feat(agents): add first-class Muse Code harness Add Muse as a supervised Orca agent across desktop, mobile, session history, source control, local hooks, SSH, WSL, and native Windows. Preserve user settings, support Muse 1.3 hook environment allowlists, and recognize versioned foreground processes. Include question, waiting, completion, resume, and readiness coverage. Co-authored-by: homesh-dev <300847526+homesh-dev@users.noreply.github.com> Co-authored-by: jeffhuen <32542276+jeffhuen@users.noreply.github.com> Co-authored-by: John Cusack <johncusackccm@gmail.com> Co-authored-by: Adrien De oliveira <75085839+adriendeoliveira@users.noreply.github.com> * test(agents): cover Muse remote hook registration * test(agents): cover Muse hook and source-control contracts * test(agents): exclude Muse hook metadata from script mode check * test(agents): keep Muse skill picker coverage stable * test(ai-vault): include Muse in every-agent fixture * test(mobile): repin Muse agent icon closure * fix(muse): detect questions and approvals from structured Muse signals Muse 1.3 fires no hook for request_user_input, so a pending question left the pane "working". Its internal reminder subagents also post hooks with their own session ids (even after Stop), which surfaced "tool failed" rows and flipped finished panes back to working. - Read pending questions from Muse's session log (user_input_prompt_requested/settled) via the existing transcript poll, now generalized from Codex subagents to Muse on main and relay. - Drop child-session hooks (SubagentStart ids, or turn_id === session_id). - Treat Notification permission_prompt as the approval wait; PermissionRequest also fires for auto-approved calls, so it only caches the approval card. - Ignore Notification copy as the prompt; poll replays are not new prompts or turn boundaries. - Allowlist USERPROFILE so Windows cmd AutoRun doesn't fail every hook. * perf(muse): parse only question events from the session log Most Muse session-log lines are large model/tool records. Filter raw lines by the user_input_prompt_ marker before JSON.parse via an optional readJsonlCursor line filter. * fix(muse): unwrap batched log records and scope questions to the live turn Review follow-ups: question events inside retained_frame batches were skipped, and a question left open by a crash or interrupt stayed pending for the pane's life. Share the history scanner's retained_frame unwrapper, and only report a pending question whose run_id matches the hook turn_id. * refactor(muse): drop type assertion in retained_frame unwrap * fix(agent-hooks): satisfy exhaustive-switch lint in transcript poll policy --------- Co-authored-by: Adrien De oliveira <75085839+adriendeoliveira@users.noreply.github.com>
788 lines
20 KiB
TypeScript
788 lines
20 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'
|
|
],
|
|
stdinPayload: 'PROMPT',
|
|
label: 'Claude'
|
|
}
|
|
})
|
|
})
|
|
|
|
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',
|
|
'default',
|
|
'--variant',
|
|
'high'
|
|
],
|
|
stdinPayload: 'PROMPT',
|
|
label: 'OpenCode'
|
|
}
|
|
})
|
|
})
|
|
|
|
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',
|
|
'default'
|
|
],
|
|
stdinPayload: 'PROMPT',
|
|
label: 'OpenCode'
|
|
}
|
|
})
|
|
})
|
|
|
|
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('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', 'default'],
|
|
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', 'default']
|
|
}
|
|
})
|
|
})
|
|
|
|
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',
|
|
'default',
|
|
'--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', 'default']
|
|
}
|
|
})
|
|
})
|
|
|
|
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',
|
|
'default'
|
|
]
|
|
}
|
|
})
|
|
})
|
|
|
|
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',
|
|
'default'
|
|
]
|
|
}
|
|
})
|
|
})
|
|
|
|
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', 'default']
|
|
}
|
|
})
|
|
})
|
|
|
|
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')
|
|
})
|
|
})
|