From 744067d0f7f0e49d9d96d10775df955c2000494e Mon Sep 17 00:00:00 2001 From: Carlos Eberhardt Date: Mon, 18 Aug 2025 07:34:05 -0500 Subject: [PATCH] added more detailed error output to log when stepzen request fails. --- src/errors/CliError.ts | 48 ++++- src/errors/handler.ts | 57 +++++- src/services/cli.ts | 68 ++++++- src/test/unit/errors/cliErrorHandling.test.ts | 182 ++++++++++++++++++ 4 files changed, 343 insertions(+), 12 deletions(-) create mode 100644 src/test/unit/errors/cliErrorHandling.test.ts diff --git a/src/errors/CliError.ts b/src/errors/CliError.ts index f42c00b..d00973c 100644 --- a/src/errors/CliError.ts +++ b/src/errors/CliError.ts @@ -1,19 +1,63 @@ import { StepZenError } from './StepZenError'; +/** + * Structured error data extracted from StepZen CLI output + */ +export interface StepZenCliErrorDetails { + /** The original error message */ + originalMessage: string; + /** The type of error (authorization, graphql, etc.) */ + errorType?: string; + /** Specific error details for GraphQL errors */ + graphqlErrors?: string[]; + /** Authorization error details */ + authError?: string; +} + /** * Error class for CLI-related errors * Represents errors that occur when interacting with the StepZen CLI */ export class CliError extends StepZenError { + /** Structured error details extracted from CLI output */ + public details?: StepZenCliErrorDetails; + /** * Creates a new CLI error * * @param message The error message * @param code A unique code representing the specific error * @param cause The underlying cause of this error (optional) + * @param details Structured error details extracted from CLI output (optional) */ - constructor(message: string, code: string = 'CLI_ERROR', cause?: unknown) { + constructor(message: string, code: string = 'CLI_ERROR', cause?: unknown, details?: StepZenCliErrorDetails) { super(message, code, cause); this.name = 'CliError'; + this.details = details; } -} \ No newline at end of file + + /** + * Creates a string representation of the error including details if available + */ + public toString(): string { + let result = `${this.name}[${this.code}]: ${this.message}`; + + if (this.details) { + if (this.details.errorType) { + result += `\nError Type: ${this.details.errorType}`; + } + + if (this.details.authError) { + result += `\nAuthorization Error: ${this.details.authError}`; + } + + if (this.details.graphqlErrors && this.details.graphqlErrors.length > 0) { + result += `\nGraphQL Errors:\n${this.details.graphqlErrors.map(err => `- ${err}`).join('\n')}`; + } + } + + return result; + } +} + +// Made with Bob diff --git a/src/errors/handler.ts b/src/errors/handler.ts index f5cf711..a995958 100644 --- a/src/errors/handler.ts +++ b/src/errors/handler.ts @@ -19,6 +19,18 @@ export function handleError(error: unknown): StepZenError { // Step 2: Log the full error with stack trace logger.error(`${normalizedError.name}[${normalizedError.code}]: ${normalizedError.message}`, normalizedError); + // For CLI errors with details, log the structured information + if (normalizedError instanceof CliError && normalizedError.details) { + if (normalizedError.details.errorType === 'authorization') { + logger.error(`Authorization Error: ${normalizedError.details.authError || 'Access denied'}`); + } else if (normalizedError.details.errorType === 'graphql' && normalizedError.details.graphqlErrors) { + logger.error('GraphQL Errors:'); + normalizedError.details.graphqlErrors.forEach(err => { + logger.error(`- ${err}`); + }); + } + } + // Step 3: Show VS Code notification with friendly message showErrorNotification(normalizedError); @@ -109,10 +121,16 @@ function showErrorNotification(error: StepZenError): void { // Generate a user-friendly message based on error type let friendlyMessage = getFriendlyErrorMessage(error); + // For CLI errors with details, add more context to the notification + let detailMessage = error.message; + if (error instanceof CliError && error.details) { + detailMessage = getDetailedErrorMessage(error); + } + // Show notification with "Show Logs" action vscode.window.showErrorMessage( friendlyMessage, - { modal: false, detail: error.message }, + { modal: false, detail: detailMessage }, 'Show Logs' ).then(selection => { if (selection === 'Show Logs') { @@ -121,6 +139,30 @@ function showErrorNotification(error: StepZenError): void { }); } +/** + * Get a detailed error message for CLI errors with structured details + * + * @param error The CliError with details + * @returns A detailed error message + */ +function getDetailedErrorMessage(error: CliError): string { + if (!error.details) { + return error.message; + } + + let detailMessage = error.message; + + // Add specific details based on error type + if (error.details.errorType === 'authorization') { + detailMessage = `Authorization Error: ${error.details.authError || 'Access denied'}`; + } + else if (error.details.errorType === 'graphql' && error.details.graphqlErrors) { + detailMessage = 'GraphQL Errors:\n' + error.details.graphqlErrors.map(err => `- ${err}`).join('\n'); + } + + return detailMessage; +} + /** * Get a user-friendly error message based on error type * @@ -130,6 +172,15 @@ function showErrorNotification(error: StepZenError): void { function getFriendlyErrorMessage(error: StepZenError): string { // Customize message based on error type if (error instanceof CliError) { + // For CLI errors with details, provide more specific messages + if (error.details) { + if (error.details.errorType === 'authorization') { + return `StepZen Authorization Error: ${error.details.authError || 'Access denied'}`; + } + else if (error.details.errorType === 'graphql') { + return 'StepZen GraphQL Error: The request contains errors'; + } + } return `StepZen CLI Error: ${error.message}`; } @@ -143,4 +194,6 @@ function getFriendlyErrorMessage(error: StepZenError): string { // Default message for base StepZenError return `StepZen Error: ${error.message}`; -} \ No newline at end of file +} + +// Made with Bob diff --git a/src/services/cli.ts b/src/services/cli.ts index 159bac7..478d77e 100644 --- a/src/services/cli.ts +++ b/src/services/cli.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import * as fs from 'fs'; import * as os from 'os'; import { logger } from './logger'; -import { CliError } from '../errors'; +import { CliError, StepZenCliErrorDetails } from '../errors'; import { TEMP_FILE_PATTERNS, TIMEOUTS } from "../utils/constants"; /** @@ -289,6 +289,50 @@ export class StepzenCliService { } } + /** + * Parse StepZen CLI error output to extract structured error information + * + * @param stderr The stderr output from the CLI + * @returns Structured error details + */ + private parseCliErrorOutput(stderr: string): StepZenCliErrorDetails { + const details: StepZenCliErrorDetails = { + originalMessage: stderr + }; + + // Check for authorization errors + if (stderr.includes('Action denied: You are not authorized')) { + details.errorType = 'authorization'; + // Extract the specific auth error message + const authMatch = stderr.match(/Action denied: ([^\n]+)/); + if (authMatch && authMatch[1]) { + details.authError = authMatch[1].trim(); + } + } + // Check for GraphQL errors + else if (stderr.includes('Errors returned by the GraphQL server:')) { + details.errorType = 'graphql'; + details.graphqlErrors = []; + + // Extract GraphQL errors - they typically appear after this line + const lines = stderr.split('\n'); + let collectingErrors = false; + + for (const line of lines) { + const trimmedLine = line.trim(); + if (collectingErrors && trimmedLine.startsWith('-')) { + // This is a GraphQL error line, extract the error message + details.graphqlErrors.push(trimmedLine.substring(1).trim()); + } + + if (trimmedLine.includes('Errors returned by the GraphQL server:')) { + collectingErrors = true; + } + } + } + + return details; + } /** * Spawn a StepZen CLI process and capture output @@ -350,15 +394,21 @@ export class StepzenCliService { proc.on('close', (code) => { if (code !== 0) { - // Create a more descriptive error with both exit code and stderr content - const errorMsg = stderr.trim() - ? `StepZen CLI exited with code ${code}: ${stderr.trim()}` - : `StepZen CLI exited with code ${code}`; - + // Parse the stderr to extract structured error information + const errorDetails = this.parseCliErrorOutput(stderr.trim()); + + // Create a more descriptive error message + let errorMsg = `StepZen CLI exited with code ${code}`; + if (stderr.trim()) { + errorMsg += `: ${stderr.trim()}`; + } + + // Create a CliError with the structured error details reject(new CliError( errorMsg, 'COMMAND_FAILED', - stderr ? new Error(stderr) : undefined + stderr ? new Error(stderr) : undefined, + errorDetails )); } else { logger.debug(`StepZen CLI process completed with exit code 0`); @@ -367,4 +417,6 @@ export class StepzenCliService { }); }); } -} \ No newline at end of file +} + +// Made with Bob diff --git a/src/test/unit/errors/cliErrorHandling.test.ts b/src/test/unit/errors/cliErrorHandling.test.ts new file mode 100644 index 0000000..fa7089c --- /dev/null +++ b/src/test/unit/errors/cliErrorHandling.test.ts @@ -0,0 +1,182 @@ +import * as assert from 'assert'; +import { CliError, StepZenCliErrorDetails } from '../../../errors/CliError'; +import { handleError } from '../../../errors/handler'; +import { logger } from '../../../services/logger'; +import * as vscode from 'vscode'; +import * as sinon from 'sinon'; + +// We'll use a sandbox to manage our stubs +let sandbox: sinon.SinonSandbox; + +suite('CLI Error Handling', () => { + // Set up stubs before each test + setup(() => { + sandbox = sinon.createSandbox(); + + // Stub logger methods + sandbox.stub(logger, 'error'); + sandbox.stub(logger, 'debug'); + sandbox.stub(logger, 'info'); + sandbox.stub(logger, 'warn'); + sandbox.stub(logger, 'showOutput'); + + // Stub vscode.window.showErrorMessage + sandbox.stub(vscode.window, 'showErrorMessage').resolves(undefined); + }); + + // Clean up stubs after each test + teardown(() => { + sandbox.restore(); + }); + + suite('Authorization Errors', () => { + test('should correctly parse and handle authorization errors', () => { + // Create a mock CLI error with authorization details + const errorDetails: StepZenCliErrorDetails = { + originalMessage: 'Error: Failed to execute the GraphQL query\nAction denied: You are not authorized to perform this action', + errorType: 'authorization', + authError: 'You are not authorized to perform this action' + }; + + const error = new CliError( + 'StepZen CLI exited with code 1: Error: Failed to execute the GraphQL query', + 'COMMAND_FAILED', + new Error('CLI error'), + errorDetails + ); + + // Handle the error + handleError(error); + + // Verify error was logged correctly + assert.ok( + (logger.error as sinon.SinonStub).calledWith( + sinon.match(/CliError\[COMMAND_FAILED\]/), + sinon.match.any + ), + 'Should log the error with code' + ); + + assert.ok( + (logger.error as sinon.SinonStub).calledWith( + sinon.match(/Authorization Error: You are not authorized to perform this action/) + ), + 'Should log the authorization error details' + ); + + // Verify notification was shown with correct message + assert.ok( + (vscode.window.showErrorMessage as sinon.SinonStub).calledWith( + sinon.match(/StepZen Authorization Error/), + sinon.match.any, + 'Show Logs' + ), + 'Should show error notification with authorization error' + ); + }); + }); + + suite('GraphQL Errors', () => { + test('should correctly parse and handle GraphQL errors', () => { + // Create a mock CLI error with GraphQL error details + const errorDetails: StepZenCliErrorDetails = { + originalMessage: 'Error: Failed to execute the GraphQL query\nErrors returned by the GraphQL server:\n - (8:13) Cannot query field "ssno" on type "Contact". Did you mean "ssn"?', + errorType: 'graphql', + graphqlErrors: ['(8:13) Cannot query field "ssno" on type "Contact". Did you mean "ssn"?'] + }; + + const error = new CliError( + 'StepZen CLI exited with code 1: Error: Failed to execute the GraphQL query', + 'COMMAND_FAILED', + new Error('CLI error'), + errorDetails + ); + + // Handle the error + handleError(error); + + // Verify error was logged correctly + assert.ok( + (logger.error as sinon.SinonStub).calledWith( + sinon.match(/CliError\[COMMAND_FAILED\]/), + sinon.match.any + ), + 'Should log the error with code' + ); + + assert.ok( + (logger.error as sinon.SinonStub).calledWith('GraphQL Errors:'), + 'Should log GraphQL errors header' + ); + + assert.ok( + (logger.error as sinon.SinonStub).calledWith( + sinon.match(/- \(8:13\) Cannot query field "ssno" on type "Contact"/) + ), + 'Should log the specific GraphQL error' + ); + + // Verify notification was shown with correct message + assert.ok( + (vscode.window.showErrorMessage as sinon.SinonStub).calledWith( + 'StepZen GraphQL Error: The request contains errors', + sinon.match.any, + 'Show Logs' + ), + 'Should show error notification with GraphQL error' + ); + }); + }); + + suite('Generic CLI Errors', () => { + test('should handle generic CLI errors without specific details', () => { + const error = new CliError( + 'Failed to execute StepZen request', + 'REQUEST_FAILED' + ); + + // Handle the error + handleError(error); + + // Verify error was logged correctly + assert.ok( + (logger.error as sinon.SinonStub).calledWith( + sinon.match(/CliError\[REQUEST_FAILED\]/), + sinon.match.any + ), + 'Should log the error with code' + ); + + // Verify notification was shown with correct message + assert.ok( + (vscode.window.showErrorMessage as sinon.SinonStub).calledWith( + 'StepZen CLI Error: Failed to execute StepZen request', + sinon.match.any, + 'Show Logs' + ), + 'Should show error notification with generic CLI error' + ); + }); + }); + + suite('Error Normalization', () => { + test('should normalize standard Error objects with CLI-related messages', () => { + const stdError = new Error('Command stepzen request exited with code 1'); + + // Handle the error + const normalized = handleError(stdError); + + // Verify it was normalized to a CliError + assert.ok(normalized instanceof CliError, 'Should normalize to CliError'); + assert.strictEqual(normalized.code, 'CLI_OPERATION_FAILED', 'Should have correct error code'); + + // Verify notification was shown + assert.ok( + (vscode.window.showErrorMessage as sinon.SinonStub).called, + 'Should show error notification' + ); + }); + }); +}); + +// Made with Bob