mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
237 lines
8.5 KiB
TypeScript
237 lines
8.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
buildCommitPrompt,
|
|
cleanGeneratedCommitMessage,
|
|
extractAgentErrorMessage,
|
|
planCustomCommand,
|
|
STAGED_DIFF_BYTE_BUDGET,
|
|
tokenizeCustomCommandTemplate,
|
|
truncateDiffForPrompt
|
|
} from './commit-message-prompt'
|
|
|
|
describe('buildCommitPrompt', () => {
|
|
it('embeds the diff into the base prompt', () => {
|
|
const prompt = buildCommitPrompt('diff --git a/foo b/foo\n+hello', '')
|
|
expect(prompt).toContain('diff --git a/foo b/foo')
|
|
expect(prompt).toContain('+hello')
|
|
expect(prompt).toContain('First line: imperative mood')
|
|
})
|
|
|
|
it('appends a custom suffix when non-empty', () => {
|
|
const prompt = buildCommitPrompt('diff', 'Use Conventional Commits.')
|
|
expect(prompt).toContain('Additional user prompt:')
|
|
expect(prompt.endsWith('Use Conventional Commits.')).toBe(true)
|
|
})
|
|
|
|
it('does not append the suffix block for whitespace-only suffixes', () => {
|
|
const prompt = buildCommitPrompt('diff', ' \n ')
|
|
expect(prompt).not.toContain('Additional user prompt:')
|
|
})
|
|
})
|
|
|
|
describe('truncateDiffForPrompt', () => {
|
|
it('returns the diff unchanged when within budget', () => {
|
|
const diff = 'line\n'.repeat(10)
|
|
expect(truncateDiffForPrompt(diff)).toBe(diff)
|
|
})
|
|
|
|
it('truncates and appends a marker when over budget', () => {
|
|
const oversized = 'A'.repeat(STAGED_DIFF_BYTE_BUDGET + 100)
|
|
const result = truncateDiffForPrompt(oversized)
|
|
expect(result.length).toBeLessThan(oversized.length)
|
|
expect(result).toMatch(/diff truncated, 100 bytes omitted/)
|
|
})
|
|
|
|
it('honors a custom budget', () => {
|
|
const result = truncateDiffForPrompt('abcdefghij', 5)
|
|
expect(result.startsWith('abcde')).toBe(true)
|
|
expect(result).toMatch(/diff truncated, 5 bytes omitted/)
|
|
})
|
|
})
|
|
|
|
describe('cleanGeneratedCommitMessage', () => {
|
|
it('trims whitespace', () => {
|
|
expect(cleanGeneratedCommitMessage(' feat: hello \n')).toBe('feat: hello')
|
|
})
|
|
|
|
it('strips a single enclosing fenced code block', () => {
|
|
const raw = '```\nfeat: hello\n```'
|
|
expect(cleanGeneratedCommitMessage(raw)).toBe('feat: hello')
|
|
})
|
|
|
|
it('strips a fenced block with a language tag', () => {
|
|
const raw = '```text\nfix: bug\n```'
|
|
expect(cleanGeneratedCommitMessage(raw)).toBe('fix: bug')
|
|
})
|
|
|
|
it('drops a leading "Generating…" preamble line', () => {
|
|
const raw = 'Generating…\nfeat: hello world'
|
|
expect(cleanGeneratedCommitMessage(raw)).toBe('feat: hello world')
|
|
})
|
|
|
|
it('normalizes CRLF line endings', () => {
|
|
expect(cleanGeneratedCommitMessage('feat: a\r\nbody line\r\n')).toBe('feat: a\nbody line')
|
|
})
|
|
|
|
it('strips a leading list marker from the commit subject', () => {
|
|
expect(cleanGeneratedCommitMessage('● Add Copilot entry to agent results')).toBe(
|
|
'Add Copilot entry to agent results'
|
|
)
|
|
expect(cleanGeneratedCommitMessage('1. Add numbered entry')).toBe('Add numbered entry')
|
|
})
|
|
|
|
it('returns empty string when input is whitespace', () => {
|
|
expect(cleanGeneratedCommitMessage(' \n\t')).toBe('')
|
|
})
|
|
})
|
|
|
|
describe('extractAgentErrorMessage', () => {
|
|
it('returns the inner message from a Codex JSON error payload', () => {
|
|
const stderr = [
|
|
'--------',
|
|
'workdir: C:\\Storage\\Projects\\bagplanner',
|
|
'model: gpt-5.3-codex-spark',
|
|
'reasoning effort: medium',
|
|
'--------',
|
|
'user',
|
|
'You are generating a single git commit message...',
|
|
'hook: SessionStart',
|
|
'hook: SessionStart Completed',
|
|
'ERROR: {"type":"error","status":400,"error":{"type":"invalid_request_error","message":"The \'gpt-5.3-codex-spark\' model is not supported when using Codex with a ChatGPT account."}}'
|
|
].join('\n')
|
|
expect(extractAgentErrorMessage('', stderr)).toBe(
|
|
"The 'gpt-5.3-codex-spark' model is not supported when using Codex with a ChatGPT account."
|
|
)
|
|
})
|
|
|
|
it('returns the payload for non-JSON error lines', () => {
|
|
const out = 'preamble line\nERROR: {bad json oops'
|
|
expect(extractAgentErrorMessage(out, '')).toBe('{bad json oops')
|
|
})
|
|
|
|
it('uses the last ERROR line when several are emitted', () => {
|
|
const out = ['ERROR: first failure', 'retry message', 'ERROR: second failure'].join('\n')
|
|
expect(extractAgentErrorMessage(out, '')).toBe('second failure')
|
|
})
|
|
|
|
it('matches an `Error:` line emitted on stdout', () => {
|
|
expect(extractAgentErrorMessage('Error: model unavailable\n', '')).toBe('model unavailable')
|
|
})
|
|
|
|
it('matches ANSI-colored `Error:` lines emitted by CLIs', () => {
|
|
expect(
|
|
extractAgentErrorMessage('', '\u001b[91m\u001b[1mError: \u001b[0mNo payment method\n')
|
|
).toBe('No payment method')
|
|
})
|
|
|
|
it('matches tool-specific `Error during ...:` lines', () => {
|
|
expect(
|
|
extractAgentErrorMessage(
|
|
'',
|
|
'Error during droid execution: Authentication failed. Please log into Factory.\n'
|
|
)
|
|
).toBe('Authentication failed. Please log into Factory.')
|
|
})
|
|
|
|
it('matches wrapped provider error-code payloads with quoted message fields', () => {
|
|
const stdout = [
|
|
"Error code: 401 - {'error': {'message': 'The API Key appears to be invalid or ma",
|
|
"y have expired. Please verify your credentials and try again.', 'type': 'invalid",
|
|
"_authentication_error'}}"
|
|
].join('\n')
|
|
expect(extractAgentErrorMessage(stdout, '')).toBe(
|
|
'The API Key appears to be invalid or may have expired. Please verify your credentials and try again.'
|
|
)
|
|
})
|
|
|
|
it('returns null when no ERROR line is present', () => {
|
|
expect(extractAgentErrorMessage('plain log\nmore log\n', '')).toBeNull()
|
|
})
|
|
|
|
it('returns the JSON payload `message` field when no nested error is set', () => {
|
|
const out = 'ERROR: {"message":"top-level only"}'
|
|
expect(extractAgentErrorMessage(out, '')).toBe('top-level only')
|
|
})
|
|
})
|
|
|
|
describe('tokenizeCustomCommandTemplate', () => {
|
|
it('splits on whitespace', () => {
|
|
const r = tokenizeCustomCommandTemplate('claude -p')
|
|
expect(r).toEqual({ ok: true, tokens: ['claude', '-p'] })
|
|
})
|
|
|
|
it('groups double-quoted segments with spaces', () => {
|
|
const r = tokenizeCustomCommandTemplate('claude --msg "hello world"')
|
|
expect(r).toEqual({ ok: true, tokens: ['claude', '--msg', 'hello world'] })
|
|
})
|
|
|
|
it('groups single-quoted segments verbatim', () => {
|
|
const r = tokenizeCustomCommandTemplate(`agent --json '{"k":"v"}'`)
|
|
expect(r).toEqual({ ok: true, tokens: ['agent', '--json', '{"k":"v"}'] })
|
|
})
|
|
|
|
it('honors backslash escapes inside double quotes', () => {
|
|
const r = tokenizeCustomCommandTemplate('claude --msg "she said \\"hi\\""')
|
|
expect(r).toEqual({ ok: true, tokens: ['claude', '--msg', 'she said "hi"'] })
|
|
})
|
|
|
|
it('keeps adjacent quoted/unquoted regions in one token (a"b"c → abc)', () => {
|
|
const r = tokenizeCustomCommandTemplate('foo a"b"c')
|
|
expect(r).toEqual({ ok: true, tokens: ['foo', 'abc'] })
|
|
})
|
|
|
|
it('returns an error for an unclosed quote', () => {
|
|
const r = tokenizeCustomCommandTemplate('claude --msg "no end')
|
|
expect(r.ok).toBe(false)
|
|
if (!r.ok) {
|
|
expect(r.error).toMatch(/unclosed/i)
|
|
}
|
|
})
|
|
|
|
it('returns an empty token list for whitespace-only input', () => {
|
|
const r = tokenizeCustomCommandTemplate(' \t ')
|
|
expect(r).toEqual({ ok: true, tokens: [] })
|
|
})
|
|
})
|
|
|
|
describe('planCustomCommand', () => {
|
|
it('routes prompt via stdin when {prompt} is absent', () => {
|
|
const r = planCustomCommand('claude -p', 'COMMIT MSG')
|
|
expect(r).toEqual({ ok: true, binary: 'claude', args: ['-p'], stdinPayload: 'COMMIT MSG' })
|
|
})
|
|
|
|
it('substitutes {prompt} as a whole token via argv', () => {
|
|
const r = planCustomCommand('codex exec {prompt}', 'PROMPT')
|
|
expect(r).toEqual({ ok: true, binary: 'codex', args: ['exec', 'PROMPT'], stdinPayload: null })
|
|
})
|
|
|
|
it('treats "{prompt}" identically to bare {prompt} (no shell, no double-quoting)', () => {
|
|
const a = planCustomCommand('codex exec {prompt}', 'PROMPT')
|
|
const b = planCustomCommand('codex exec "{prompt}"', 'PROMPT')
|
|
expect(a).toEqual(b)
|
|
})
|
|
|
|
it('substitutes {prompt} embedded inside a token', () => {
|
|
const r = planCustomCommand('agent --msg={prompt}', 'PROMPT')
|
|
expect(r).toEqual({
|
|
ok: true,
|
|
binary: 'agent',
|
|
args: ['--msg=PROMPT'],
|
|
stdinPayload: null
|
|
})
|
|
})
|
|
|
|
it('errors on empty templates', () => {
|
|
const r = planCustomCommand(' ', 'PROMPT')
|
|
expect(r.ok).toBe(false)
|
|
})
|
|
|
|
it('propagates tokenizer errors', () => {
|
|
const r = planCustomCommand('agent "unclosed', 'PROMPT')
|
|
expect(r.ok).toBe(false)
|
|
if (!r.ok) {
|
|
expect(r.error).toMatch(/unclosed/i)
|
|
}
|
|
})
|
|
})
|