diff --git a/src/formatters/vault.ts b/src/formatters/vault.ts index 02d014c..0f45153 100644 --- a/src/formatters/vault.ts +++ b/src/formatters/vault.ts @@ -238,6 +238,17 @@ export function formatFileRead(response: FileReadResponse): string { // Content - handle both string and fragment array formats lines.push(''); + if (content != null && typeof content !== 'string' && !Array.isArray(content)) { + // Unrecognized/binary structured passthrough — render a safe note + // instead of falling through to the _Formatter error_ fallback. + lines.push(header(2, 'Content')); + lines.push(''); + lines.push('_(non-text content; use `raw: true` for the structured payload)_'); + lines.push(divider()); + lines.push(summaryFooter()); + return joinLines(lines); + } + if (Array.isArray(content)) { // Fragment-based response const fragments = content; @@ -260,18 +271,17 @@ export function formatFileRead(response: FileReadResponse): string { lines.push(`... and ${fragments.length - 5} more fragments`); } } else { - // Simple string content + // Verbatim string content (ADR-203: content reads are faithful by + // default — the formatted default path must NOT truncate, or an agent + // cannot derive a byte-matching edit.window oldText without raw:true, + // which is the exact #133 friction this ADR retires). The data layer + // already bounds size to READ_PAGE_CHARS (or it's an explicit + // returnFullFile override), so emitting the full block is safe. No + // wrapping code fence: the body may itself contain ``` fences, and a + // wrapper would corrupt round-trip fidelity. lines.push(header(2, 'Content')); lines.push(''); - - const contentLines = content.split('\n'); - const previewLines = contentLines.slice(0, 50); - lines.push('```markdown'); - lines.push(previewLines.join('\n')); - if (contentLines.length > 50) { - lines.push(`\n... (${contentLines.length - 50} more lines)`); - } - lines.push('```'); + lines.push(content); } if (pagination && pagination.paginated) { diff --git a/tests/vault-read-fidelity.test.ts b/tests/vault-read-fidelity.test.ts index bc3ceb5..cbb6f63 100644 --- a/tests/vault-read-fidelity.test.ts +++ b/tests/vault-read-fidelity.test.ts @@ -102,22 +102,33 @@ describe('vault.read fidelity & pagination (ADR-203)', () => { expect(r.fragmentMetadata).toBeDefined(); }); - test('formatter renders all shapes without the _Formatter error_ crash', () => { - // whole-file string shape - expect(() => formatFileRead({ - path: 's.md', content: TRICKY, - metadata: { totalLines: 14, bytes: TRICKY.length }, - pagination: { paginated: false, page: 1, pageLineStart: 1, pageLineEnd: 14, totalLines: 14, bytes: TRICKY.length, hasMore: false, nextPage: null }, - } as any)).not.toThrow(); - // paginated shape + test('formatted (non-raw) default output is byte-faithful — no truncation, no fence corruption', () => { + // The blocking #133 case: an agent reading the DEFAULT (raw:false) + // formatted output must be able to lift a byte-exact edit.window oldText. const out = formatFileRead({ + path: 's.md', content: TRICKY, + metadata: { totalLines: TRICKY.split('\n').length, bytes: TRICKY.length }, + pagination: { paginated: false, page: 1, pageLineStart: 1, pageLineEnd: TRICKY.split('\n').length, totalLines: TRICKY.split('\n').length, bytes: TRICKY.length, hasMore: false, nextPage: null }, + } as any); + // Verbatim body present in full, including the inner ```python fence, + // the indented code line, and the trailing-space + tab line. + expect(out).toContain(TRICKY); + expect(out).toContain(' return x*2 # indented'); + expect(out).toContain('\ttab-line '); + expect(out).not.toMatch(/more lines\)/); // no truncation marker + }); + + test('formatter renders paginated + edge shapes without the _Formatter error_ crash', () => { + const out2 = formatFileRead({ path: 'big.md', content: 'line 1 ...\nline 2 ...', metadata: { totalLines: 4000, bytes: 90000 }, pagination: { paginated: true, page: 1, pageLineStart: 1, pageLineEnd: 1800, totalLines: 4000, bytes: 90000, hasMore: true, nextPage: "vault.read(path='big.md', page=2)" }, warning: 'Large file …', } as any); - expect(out).toContain('Pagination'); - expect(out).toContain('page=2'); - expect(out).not.toContain('Formatter error'); + expect(out2).toContain('Pagination'); + expect(out2).toContain('page=2'); + expect(out2).not.toContain('Formatter error'); + // non-text passthrough must not crash + expect(() => formatFileRead({ path: 'x.bin', content: { binary: true } } as any)).not.toThrow(); }); });