diff --git a/src/utils/ci_summary.ts b/src/utils/ci_summary.ts index 9ae8606..34f9fd7 100644 --- a/src/utils/ci_summary.ts +++ b/src/utils/ci_summary.ts @@ -71,7 +71,8 @@ function failuresOf(output: JsonOutput): Failure[] { /** One line, no table-breaking pipes, bounded length. */ function cell(text: string, max = 200): string { - const flat = text.replace(/\s+/g, ' ').replace(/\|/g, '\\|').trim(); + // Backslashes too: otherwise an input `\|` becomes `\\|` and the pipe breaks the table. + const flat = text.replace(/\s+/g, ' ').replace(/[\\|]/g, '\\$&').trim(); return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat; } diff --git a/tests/utils/ci_summary.test.ts b/tests/utils/ci_summary.test.ts index e54c464..57635fe 100644 --- a/tests/utils/ci_summary.test.ts +++ b/tests/utils/ci_summary.test.ts @@ -84,6 +84,21 @@ describe('renderMarkdown', () => { expect(md).not.toContain('search on'); }); + test('a backslash before a pipe cannot unescape it and break the table', () => { + const md = renderMarkdown({ + ...maestro, + runs: [ + { + ...maestro.runs[0], + flows: [ + flow(2, 'checkout', false, { errors: ['path C:\\tmp\\| x'] }), + ], + }, + ], + }); + expect(md).toContain('| path C:\\\\tmp\\\\\\| x |'); + }); + test('a passed Espresso run without flows', () => { const md = renderMarkdown(espresso); expect(md).toContain('### TestingBot Espresso: ✅ passed');