diff --git a/packages/adt-cli/src/lib/utils/plugin-execution-error.test.ts b/packages/adt-cli/src/lib/utils/plugin-execution-error.test.ts index 6a7590230..76d74a00e 100644 --- a/packages/adt-cli/src/lib/utils/plugin-execution-error.test.ts +++ b/packages/adt-cli/src/lib/utils/plugin-execution-error.test.ts @@ -14,6 +14,26 @@ describe('formatPluginExecutionError', () => { ); }); + it('renders allow-listed diagnostic details without leaking arbitrary fields', () => { + const error = Object.assign( + new Error('An indexed file differs from its recorded content hash.'), + { + code: 'working_tree_diverged', + details: { + object: 'CLAS/ZCL_SAMPLE', + path: 'src/feature/zcl_sample.clas.abap', + token: 'must-not-be-logged', + }, + }, + ); + + const rendered = formatPluginExecutionError(error); + + expect(rendered).toContain('object=CLAS/ZCL_SAMPLE'); + expect(rendered).toContain('path=src/feature/zcl_sample.clas.abap'); + expect(rendered).not.toContain('must-not-be-logged'); + }); + it('does not stringify an arbitrary thrown value', () => { expect(formatPluginExecutionError({ token: 'must-not-be-logged' })).toBe( '❌ Command failed: unexpected failure', diff --git a/packages/adt-cli/src/lib/utils/plugin-execution-error.ts b/packages/adt-cli/src/lib/utils/plugin-execution-error.ts index 56780a1c3..e129b4e38 100644 --- a/packages/adt-cli/src/lib/utils/plugin-execution-error.ts +++ b/packages/adt-cli/src/lib/utils/plugin-execution-error.ts @@ -1,6 +1,13 @@ const GENERIC_FAILURE = '❌ Command failed: unexpected failure'; const CODE_PATTERN = /^[a-z0-9_]{1,64}$/i; const MAX_MESSAGE_LENGTH = 300; +const DIAGNOSTIC_DETAIL_KEYS = [ + 'object', + 'component', + 'path', + 'diagnostic', + 'changeKind', +] as const; // eslint-disable-next-line no-control-regex const ANSI_CSI = /\u001b\[[0-9;:]*[ -/]*[@-~]/gu; // eslint-disable-next-line no-control-regex @@ -19,6 +26,20 @@ function sanitizeLogText(text: string): string { .slice(0, MAX_MESSAGE_LENGTH); } +function formatDiagnosticDetails(error: Error): string { + const details = (error as { details?: unknown }).details; + if (details === null || typeof details !== 'object') return ''; + + const rendered = DIAGNOSTIC_DETAIL_KEYS.flatMap((key) => { + const value = (details as Record)[key]; + return typeof value === 'string' && value.length > 0 + ? [`${key}=${sanitizeLogText(value)}`] + : []; + }).join(', '); + + return rendered ? ` [${rendered}]` : ''; +} + /** * Render typed plugin failures as a stable single-line message so CI logs do * not expose implementation stacks, control sequences, or arbitrary thrown @@ -34,7 +55,8 @@ export function formatPluginExecutionError(error: unknown): string { return GENERIC_FAILURE; } const message = sanitizeLogText(error.message); + const details = formatDiagnosticDetails(error); return message - ? `❌ Command failed [${code}]: ${message}` - : `❌ Command failed [${code}]`; + ? `❌ Command failed [${code}]: ${message}${details}` + : `❌ Command failed [${code}]${details}`; }