Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib/SessionSummaryService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,7 @@ describe('SessionSummaryService', () => {
expect(launchClaude).toHaveBeenCalledWith('Generated prompt content', {
headless: true,
model: 'sonnet',
noSessionPersistence: true,
})

// Verify comment was posted to issue
Expand Down
1 change: 1 addition & 0 deletions src/lib/SessionSummaryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,7 @@ export class SessionSummaryService {
const reportResult = await launchClaude(prompt, {
headless: true,
model: summaryModel,
noSessionPersistence: true,
})

if (!reportResult || typeof reportResult !== 'string' || reportResult.trim() === '') {
Expand Down
82 changes: 82 additions & 0 deletions src/utils/claude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2952,6 +2952,88 @@ describe('claude utils', () => {
// Should only have been called once (no retry)
expect(execa).toHaveBeenCalledTimes(1)
})

it('should retry without --bare when session-in-use --resume retry fails with auth error', async () => {
// Set up OAuth token so bare mode will be auto-applied
process.env.CLAUDE_CODE_OAUTH_TOKEN = 'sk-ant-oat01-test-token'

const sessionId = '01af28fe-8630-4778-ae85-39398ab84f54'

// First call: fails with session-in-use error (bare mode auto-applied)
mockExeca().mockRejectedValueOnce({
stderr: `Error: Session ID ${sessionId} is already in use.`,
exitCode: 1,
})

// Second call (--resume retry): fails with auth error
const authError = Object.assign(new Error('authentication_failed'), {
stderr: 'Invalid API key',
exitCode: 1,
})
mockExeca().mockRejectedValueOnce(authError)

// Third call: succeeds (retry without bare)
mockExeca().mockResolvedValueOnce({
stdout: 'success without bare',
exitCode: 0,
})

const result = await launchClaude('test prompt', {
headless: true,
noSessionPersistence: true,
sessionId,
})

expect(result).toBe('success without bare')
expect(execa).toHaveBeenCalledTimes(3)

// First call should have --bare and --session-id
const firstCallArgs = mockExeca().mock.calls[0][1] as string[]
expect(firstCallArgs).toContain('--bare')
expect(firstCallArgs).toContain('--session-id')

// Second call (--resume) should have --bare and --resume
const secondCallArgs = mockExeca().mock.calls[1][1] as string[]
expect(secondCallArgs).toContain('--bare')
expect(secondCallArgs).toContain('--resume')

// Third call should NOT have --bare (fallback)
const thirdCallArgs = mockExeca().mock.calls[2][1] as string[]
expect(thirdCallArgs).not.toContain('--bare')

// Verify warning was logged
expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('Bare mode failed during --resume retry'))
})

it('should throw when session-in-use --resume retry fails with non-auth error', async () => {
// Set up OAuth token so bare mode will be auto-applied
process.env.CLAUDE_CODE_OAUTH_TOKEN = 'sk-ant-oat01-test-token'

const sessionId = '01af28fe-8630-4778-ae85-39398ab84f54'

// First call: fails with session-in-use error
mockExeca().mockRejectedValueOnce({
stderr: `Error: Session ID ${sessionId} is already in use.`,
exitCode: 1,
})

// Second call (--resume retry): fails with non-auth error
mockExeca().mockRejectedValueOnce({
stderr: 'Some other error on retry',
exitCode: 1,
})

await expect(
launchClaude('test prompt', {
headless: true,
noSessionPersistence: true,
sessionId,
})
).rejects.toThrow('Claude CLI error: Some other error on retry')

// Should have been called exactly twice (initial + resume retry, no bare fallback)
expect(execa).toHaveBeenCalledTimes(2)
})
})

describe.runIf(process.platform === 'darwin')('launchClaudeInNewTerminalWindow', () => {
Expand Down
9 changes: 9 additions & 0 deletions src/utils/claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,15 @@ export async function launchClaude(
const retryExecaError = retryError as { stderr?: string; message?: string }
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- intentional: empty string stderr should fall through to message
const retryErrorMessage = retryExecaError.stderr || retryExecaError.message || 'Unknown Claude CLI error'

if (attempt === 1 && bareModeAutoApplied) {
const isAuthError = /not logged in|unauthorized|authentication|invalid api key|Could not resolve credentials/i.test(retryErrorMessage)
if (isAuthError) {
logger.warn('Bare mode failed during --resume retry (likely expired OAuth token), retrying without --bare')
continue
}
}

throw new Error(`Claude CLI error: ${redactSettings(retryErrorMessage)}`)
}
}
Expand Down
Loading