Make errorReporting test windows compatible. (#4856)
This commit is contained in:
parent
e500eb5562
commit
f379aa833f
|
@ -4,36 +4,36 @@
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from 'vitest';
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { reportError } from './errorReporting.js';
|
||||||
|
|
||||||
// Use a type alias for SpyInstance as it's not directly exported
|
// Use a type alias for SpyInstance as it's not directly exported
|
||||||
type SpyInstance = ReturnType<typeof vi.spyOn>;
|
type SpyInstance = ReturnType<typeof vi.spyOn>;
|
||||||
import { reportError } from './errorReporting.js';
|
|
||||||
import fs from 'node:fs/promises';
|
|
||||||
import os from 'node:os';
|
|
||||||
|
|
||||||
// Mock dependencies
|
|
||||||
vi.mock('node:fs/promises');
|
|
||||||
vi.mock('node:os');
|
|
||||||
|
|
||||||
describe('reportError', () => {
|
describe('reportError', () => {
|
||||||
let consoleErrorSpy: SpyInstance;
|
let consoleErrorSpy: SpyInstance;
|
||||||
const MOCK_TMP_DIR = '/tmp';
|
let testDir: string;
|
||||||
const MOCK_TIMESTAMP = '2025-01-01T00-00-00-000Z';
|
const MOCK_TIMESTAMP = '2025-01-01T00-00-00-000Z';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
|
// Create a temporary directory for logs
|
||||||
|
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gemini-report-test-'));
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
(os.tmpdir as Mock).mockReturnValue(MOCK_TMP_DIR);
|
|
||||||
vi.spyOn(Date.prototype, 'toISOString').mockReturnValue(MOCK_TIMESTAMP);
|
vi.spyOn(Date.prototype, 'toISOString').mockReturnValue(MOCK_TIMESTAMP);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(async () => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
|
// Clean up the temporary directory
|
||||||
|
await fs.rm(testDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
const getExpectedReportPath = (type: string) =>
|
const getExpectedReportPath = (type: string) =>
|
||||||
`${MOCK_TMP_DIR}/gemini-client-error-${type}-${MOCK_TIMESTAMP}.json`;
|
path.join(testDir, `gemini-client-error-${type}-${MOCK_TIMESTAMP}.json`);
|
||||||
|
|
||||||
it('should generate a report and log the path', async () => {
|
it('should generate a report and log the path', async () => {
|
||||||
const error = new Error('Test error');
|
const error = new Error('Test error');
|
||||||
|
@ -43,22 +43,18 @@ describe('reportError', () => {
|
||||||
const type = 'test-type';
|
const type = 'test-type';
|
||||||
const expectedReportPath = getExpectedReportPath(type);
|
const expectedReportPath = getExpectedReportPath(type);
|
||||||
|
|
||||||
(fs.writeFile as Mock).mockResolvedValue(undefined);
|
await reportError(error, baseMessage, context, type, testDir);
|
||||||
|
|
||||||
await reportError(error, baseMessage, context, type);
|
// Verify the file was written
|
||||||
|
const reportContent = await fs.readFile(expectedReportPath, 'utf-8');
|
||||||
|
const parsedReport = JSON.parse(reportContent);
|
||||||
|
|
||||||
expect(os.tmpdir).toHaveBeenCalledTimes(1);
|
expect(parsedReport).toEqual({
|
||||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
error: { message: 'Test error', stack: 'Test stack' },
|
||||||
expectedReportPath,
|
context,
|
||||||
JSON.stringify(
|
});
|
||||||
{
|
|
||||||
error: { message: 'Test error', stack: error.stack },
|
// Verify the console log
|
||||||
context,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
`${baseMessage} Full report available at: ${expectedReportPath}`,
|
`${baseMessage} Full report available at: ${expectedReportPath}`,
|
||||||
);
|
);
|
||||||
|
@ -70,19 +66,15 @@ describe('reportError', () => {
|
||||||
const type = 'general';
|
const type = 'general';
|
||||||
const expectedReportPath = getExpectedReportPath(type);
|
const expectedReportPath = getExpectedReportPath(type);
|
||||||
|
|
||||||
(fs.writeFile as Mock).mockResolvedValue(undefined);
|
await reportError(error, baseMessage, undefined, type, testDir);
|
||||||
await reportError(error, baseMessage);
|
|
||||||
|
const reportContent = await fs.readFile(expectedReportPath, 'utf-8');
|
||||||
|
const parsedReport = JSON.parse(reportContent);
|
||||||
|
|
||||||
|
expect(parsedReport).toEqual({
|
||||||
|
error: { message: 'Test plain object error' },
|
||||||
|
});
|
||||||
|
|
||||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
||||||
expectedReportPath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
error: { message: 'Test plain object error' },
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
`${baseMessage} Full report available at: ${expectedReportPath}`,
|
`${baseMessage} Full report available at: ${expectedReportPath}`,
|
||||||
);
|
);
|
||||||
|
@ -94,19 +86,15 @@ describe('reportError', () => {
|
||||||
const type = 'general';
|
const type = 'general';
|
||||||
const expectedReportPath = getExpectedReportPath(type);
|
const expectedReportPath = getExpectedReportPath(type);
|
||||||
|
|
||||||
(fs.writeFile as Mock).mockResolvedValue(undefined);
|
await reportError(error, baseMessage, undefined, type, testDir);
|
||||||
await reportError(error, baseMessage);
|
|
||||||
|
const reportContent = await fs.readFile(expectedReportPath, 'utf-8');
|
||||||
|
const parsedReport = JSON.parse(reportContent);
|
||||||
|
|
||||||
|
expect(parsedReport).toEqual({
|
||||||
|
error: { message: 'Just a string error' },
|
||||||
|
});
|
||||||
|
|
||||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
||||||
expectedReportPath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
error: { message: 'Just a string error' },
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
`${baseMessage} Full report available at: ${expectedReportPath}`,
|
`${baseMessage} Full report available at: ${expectedReportPath}`,
|
||||||
);
|
);
|
||||||
|
@ -115,22 +103,15 @@ describe('reportError', () => {
|
||||||
it('should log fallback message if writing report fails', async () => {
|
it('should log fallback message if writing report fails', async () => {
|
||||||
const error = new Error('Main error');
|
const error = new Error('Main error');
|
||||||
const baseMessage = 'Failed operation.';
|
const baseMessage = 'Failed operation.';
|
||||||
const writeError = new Error('Failed to write file');
|
|
||||||
const context = ['some context'];
|
const context = ['some context'];
|
||||||
const type = 'general';
|
const type = 'general';
|
||||||
const expectedReportPath = getExpectedReportPath(type);
|
const nonExistentDir = path.join(testDir, 'non-existent-dir');
|
||||||
|
|
||||||
(fs.writeFile as Mock).mockRejectedValue(writeError);
|
await reportError(error, baseMessage, context, type, nonExistentDir);
|
||||||
|
|
||||||
await reportError(error, baseMessage, context, type);
|
|
||||||
|
|
||||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
||||||
expectedReportPath,
|
|
||||||
expect.any(String),
|
|
||||||
); // It still tries to write
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
`${baseMessage} Additionally, failed to write detailed error report:`,
|
`${baseMessage} Additionally, failed to write detailed error report:`,
|
||||||
writeError,
|
expect.any(Error), // The actual write error
|
||||||
);
|
);
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
'Original error that triggered report generation:',
|
'Original error that triggered report generation:',
|
||||||
|
@ -163,9 +144,7 @@ describe('reportError', () => {
|
||||||
return originalJsonStringify(value, replacer, space);
|
return originalJsonStringify(value, replacer, space);
|
||||||
});
|
});
|
||||||
|
|
||||||
(fs.writeFile as Mock).mockResolvedValue(undefined); // Mock for the minimal report write
|
await reportError(error, baseMessage, context, type, testDir);
|
||||||
|
|
||||||
await reportError(error, baseMessage, context, type);
|
|
||||||
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
`${baseMessage} Could not stringify report content (likely due to context):`,
|
`${baseMessage} Could not stringify report content (likely due to context):`,
|
||||||
|
@ -178,15 +157,14 @@ describe('reportError', () => {
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
'Original context could not be stringified or included in report.',
|
'Original context could not be stringified or included in report.',
|
||||||
);
|
);
|
||||||
// Check that it attempts to write a minimal report
|
|
||||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
// Check that it writes a minimal report
|
||||||
expectedMinimalReportPath,
|
const reportContent = await fs.readFile(expectedMinimalReportPath, 'utf-8');
|
||||||
originalJsonStringify(
|
const parsedReport = JSON.parse(reportContent);
|
||||||
{ error: { message: error.message, stack: error.stack } },
|
expect(parsedReport).toEqual({
|
||||||
null,
|
error: { message: error.message, stack: error.stack },
|
||||||
2,
|
});
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
`${baseMessage} Partial report (excluding context) available at: ${expectedMinimalReportPath}`,
|
`${baseMessage} Partial report (excluding context) available at: ${expectedMinimalReportPath}`,
|
||||||
);
|
);
|
||||||
|
@ -199,19 +177,15 @@ describe('reportError', () => {
|
||||||
const type = 'general';
|
const type = 'general';
|
||||||
const expectedReportPath = getExpectedReportPath(type);
|
const expectedReportPath = getExpectedReportPath(type);
|
||||||
|
|
||||||
(fs.writeFile as Mock).mockResolvedValue(undefined);
|
await reportError(error, baseMessage, undefined, type, testDir);
|
||||||
await reportError(error, baseMessage, undefined, type);
|
|
||||||
|
const reportContent = await fs.readFile(expectedReportPath, 'utf-8');
|
||||||
|
const parsedReport = JSON.parse(reportContent);
|
||||||
|
|
||||||
|
expect(parsedReport).toEqual({
|
||||||
|
error: { message: 'Error without context', stack: 'No context stack' },
|
||||||
|
});
|
||||||
|
|
||||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
||||||
expectedReportPath,
|
|
||||||
JSON.stringify(
|
|
||||||
{
|
|
||||||
error: { message: 'Error without context', stack: error.stack },
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
`${baseMessage} Full report available at: ${expectedReportPath}`,
|
`${baseMessage} Full report available at: ${expectedReportPath}`,
|
||||||
);
|
);
|
||||||
|
|
|
@ -27,10 +27,11 @@ export async function reportError(
|
||||||
baseMessage: string,
|
baseMessage: string,
|
||||||
context?: Content[] | Record<string, unknown> | unknown[],
|
context?: Content[] | Record<string, unknown> | unknown[],
|
||||||
type = 'general',
|
type = 'general',
|
||||||
|
reportingDir = os.tmpdir(), // for testing
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||||
const reportFileName = `gemini-client-error-${type}-${timestamp}.json`;
|
const reportFileName = `gemini-client-error-${type}-${timestamp}.json`;
|
||||||
const reportPath = path.join(os.tmpdir(), reportFileName);
|
const reportPath = path.join(reportingDir, reportFileName);
|
||||||
|
|
||||||
let errorToReport: { message: string; stack?: string };
|
let errorToReport: { message: string; stack?: string };
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
|
Loading…
Reference in New Issue