fix(cli): escape newlines in cypher markdown cells

A multi-line cell value (e.g. a symbol's `content`) was rendered with raw
newlines via String(v), so one logical row spanned multiple physical lines.
That corrupts the markdown table and breaks `cypher --limit`'s line-based
slice (it kept the wrong number of rows, often zero, while row_count
over-claimed). Collapse newlines in formatCypherAsMarkdown so one physical
line == one row; the existing CLI slice is now correct and the pre-existing
un---limited corruption is fixed too. (#2310 review)
This commit is contained in:
Gergo Magyar 2026-06-30 11:42:51 +00:00
parent 3b71abf93b
commit 1ffc9c3e73
2 changed files with 23 additions and 1 deletions

View file

@ -2319,7 +2319,11 @@ export class LocalBackend {
const v = row[k];
if (v === null || v === undefined) return '';
if (typeof v === 'object') return JSON.stringify(v);
return String(v);
// Collapse newlines so a multi-line cell value (e.g. a symbol's
// `content`) stays on one physical line. Otherwise the rendered row
// spans multiple lines, which corrupts the table and breaks the
// CLI's `--limit` line-based slicing (#2310 review).
return String(v).replace(/\r?\n/g, ' ');
})
.join(' | ') +
' |',

View file

@ -3436,6 +3436,24 @@ describe('cypher result formatting', () => {
expect(result.row_count).toBe(2);
});
it('keeps one markdown line per row when a cell value contains newlines (#2310)', async () => {
// A multi-line `content` value must not split its row across physical lines —
// otherwise the rendered table is corrupt and the CLI `--limit` line-slice
// keeps the wrong number of rows.
(executeParameterized as any).mockResolvedValue([
{ name: 'a', content: 'export function a() {\n return 1;\n}' },
{ name: 'b', content: 'line1\nline2' },
]);
const result = await backend.callTool('cypher', {
query: 'MATCH (n:Function) RETURN n.name AS name, n.content AS content',
});
const lines = result.markdown.split('\n');
// header + separator + exactly one line per data row, no embedded newlines.
expect(lines).toHaveLength(2 + result.row_count);
expect(result.row_count).toBe(2);
expect(result.markdown).not.toMatch(/\n[^|]/);
});
it('returns empty array as-is', async () => {
(executeParameterized as any).mockResolvedValue([]);
const result = await backend.callTool('cypher', {