|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest'; |
|
|
|
|
|
|
|
|
type SpyInstance = ReturnType<typeof vi.spyOn>; |
|
|
import { reportError } from './errorReporting.js'; |
|
|
import fs from 'node:fs/promises'; |
|
|
import os from 'node:os'; |
|
|
|
|
|
|
|
|
vi.mock('node:fs/promises'); |
|
|
vi.mock('node:os'); |
|
|
|
|
|
describe('reportError', () => { |
|
|
let consoleErrorSpy: SpyInstance; |
|
|
const MOCK_TMP_DIR = '/tmp'; |
|
|
const MOCK_TIMESTAMP = '2025-01-01T00-00-00-000Z'; |
|
|
|
|
|
beforeEach(() => { |
|
|
vi.resetAllMocks(); |
|
|
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); |
|
|
(os.tmpdir as Mock).mockReturnValue(MOCK_TMP_DIR); |
|
|
vi.spyOn(Date.prototype, 'toISOString').mockReturnValue(MOCK_TIMESTAMP); |
|
|
}); |
|
|
|
|
|
afterEach(() => { |
|
|
vi.restoreAllMocks(); |
|
|
}); |
|
|
|
|
|
const getExpectedReportPath = (type: string) => |
|
|
`${MOCK_TMP_DIR}/gemini-client-error-${type}-${MOCK_TIMESTAMP}.json`; |
|
|
|
|
|
it('should generate a report and log the path', async () => { |
|
|
const error = new Error('Test error'); |
|
|
error.stack = 'Test stack'; |
|
|
const baseMessage = 'An error occurred.'; |
|
|
const context = { data: 'test context' }; |
|
|
const type = 'test-type'; |
|
|
const expectedReportPath = getExpectedReportPath(type); |
|
|
|
|
|
(fs.writeFile as Mock).mockResolvedValue(undefined); |
|
|
|
|
|
await reportError(error, baseMessage, context, type); |
|
|
|
|
|
expect(os.tmpdir).toHaveBeenCalledTimes(1); |
|
|
expect(fs.writeFile).toHaveBeenCalledWith( |
|
|
expectedReportPath, |
|
|
JSON.stringify( |
|
|
{ |
|
|
error: { message: 'Test error', stack: error.stack }, |
|
|
context, |
|
|
}, |
|
|
null, |
|
|
2, |
|
|
), |
|
|
); |
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith( |
|
|
`${baseMessage} Full report available at: ${expectedReportPath}`, |
|
|
); |
|
|
}); |
|
|
|
|
|
it('should handle errors that are plain objects with a message property', async () => { |
|
|
const error = { message: 'Test plain object error' }; |
|
|
const baseMessage = 'Another error.'; |
|
|
const type = 'general'; |
|
|
const expectedReportPath = getExpectedReportPath(type); |
|
|
|
|
|
(fs.writeFile as Mock).mockResolvedValue(undefined); |
|
|
await reportError(error, baseMessage); |
|
|
|
|
|
expect(fs.writeFile).toHaveBeenCalledWith( |
|
|
expectedReportPath, |
|
|
JSON.stringify( |
|
|
{ |
|
|
error: { message: 'Test plain object error' }, |
|
|
}, |
|
|
null, |
|
|
2, |
|
|
), |
|
|
); |
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith( |
|
|
`${baseMessage} Full report available at: ${expectedReportPath}`, |
|
|
); |
|
|
}); |
|
|
|
|
|
it('should handle string errors', async () => { |
|
|
const error = 'Just a string error'; |
|
|
const baseMessage = 'String error occurred.'; |
|
|
const type = 'general'; |
|
|
const expectedReportPath = getExpectedReportPath(type); |
|
|
|
|
|
(fs.writeFile as Mock).mockResolvedValue(undefined); |
|
|
await reportError(error, baseMessage); |
|
|
|
|
|
expect(fs.writeFile).toHaveBeenCalledWith( |
|
|
expectedReportPath, |
|
|
JSON.stringify( |
|
|
{ |
|
|
error: { message: 'Just a string error' }, |
|
|
}, |
|
|
null, |
|
|
2, |
|
|
), |
|
|
); |
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith( |
|
|
`${baseMessage} Full report available at: ${expectedReportPath}`, |
|
|
); |
|
|
}); |
|
|
|
|
|
it('should log fallback message if writing report fails', async () => { |
|
|
const error = new Error('Main error'); |
|
|
const baseMessage = 'Failed operation.'; |
|
|
const writeError = new Error('Failed to write file'); |
|
|
const context = ['some context']; |
|
|
const type = 'general'; |
|
|
const expectedReportPath = getExpectedReportPath(type); |
|
|
|
|
|
(fs.writeFile as Mock).mockRejectedValue(writeError); |
|
|
|
|
|
await reportError(error, baseMessage, context, type); |
|
|
|
|
|
expect(fs.writeFile).toHaveBeenCalledWith( |
|
|
expectedReportPath, |
|
|
expect.any(String), |
|
|
); |
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith( |
|
|
`${baseMessage} Additionally, failed to write detailed error report:`, |
|
|
writeError, |
|
|
); |
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith( |
|
|
'Original error that triggered report generation:', |
|
|
error, |
|
|
); |
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith('Original context:', context); |
|
|
}); |
|
|
|
|
|
it('should handle stringification failure of report content (e.g. BigInt in context)', async () => { |
|
|
const error = new Error('Main error'); |
|
|
error.stack = 'Main stack'; |
|
|
const baseMessage = 'Failed operation with BigInt.'; |
|
|
const context = { a: BigInt(1) }; |
|
|
const type = 'bigint-fail'; |
|
|
const stringifyError = new TypeError( |
|
|
'Do not know how to serialize a BigInt', |
|
|
); |
|
|
const expectedMinimalReportPath = getExpectedReportPath(type); |
|
|
|
|
|
|
|
|
const originalJsonStringify = JSON.stringify; |
|
|
let callCount = 0; |
|
|
vi.spyOn(JSON, 'stringify').mockImplementation((value, replacer, space) => { |
|
|
callCount++; |
|
|
if (callCount === 1) { |
|
|
|
|
|
throw stringifyError; |
|
|
} |
|
|
|
|
|
return originalJsonStringify(value, replacer, space); |
|
|
}); |
|
|
|
|
|
(fs.writeFile as Mock).mockResolvedValue(undefined); |
|
|
|
|
|
await reportError(error, baseMessage, context, type); |
|
|
|
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith( |
|
|
`${baseMessage} Could not stringify report content (likely due to context):`, |
|
|
stringifyError, |
|
|
); |
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith( |
|
|
'Original error that triggered report generation:', |
|
|
error, |
|
|
); |
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith( |
|
|
'Original context could not be stringified or included in report.', |
|
|
); |
|
|
|
|
|
expect(fs.writeFile).toHaveBeenCalledWith( |
|
|
expectedMinimalReportPath, |
|
|
originalJsonStringify( |
|
|
{ error: { message: error.message, stack: error.stack } }, |
|
|
null, |
|
|
2, |
|
|
), |
|
|
); |
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith( |
|
|
`${baseMessage} Partial report (excluding context) available at: ${expectedMinimalReportPath}`, |
|
|
); |
|
|
}); |
|
|
|
|
|
it('should generate a report without context if context is not provided', async () => { |
|
|
const error = new Error('Error without context'); |
|
|
error.stack = 'No context stack'; |
|
|
const baseMessage = 'Simple error.'; |
|
|
const type = 'general'; |
|
|
const expectedReportPath = getExpectedReportPath(type); |
|
|
|
|
|
(fs.writeFile as Mock).mockResolvedValue(undefined); |
|
|
await reportError(error, baseMessage, undefined, type); |
|
|
|
|
|
expect(fs.writeFile).toHaveBeenCalledWith( |
|
|
expectedReportPath, |
|
|
JSON.stringify( |
|
|
{ |
|
|
error: { message: 'Error without context', stack: error.stack }, |
|
|
}, |
|
|
null, |
|
|
2, |
|
|
), |
|
|
); |
|
|
expect(consoleErrorSpy).toHaveBeenCalledWith( |
|
|
`${baseMessage} Full report available at: ${expectedReportPath}`, |
|
|
); |
|
|
}); |
|
|
}); |
|
|
|