mirror of
https://github.com/aaronsb/obsidian-mcp-plugin.git
synced 2026-07-22 06:45:14 +00:00
fix(formatter): emit verbatim content in default vault.read (PR #205 blocking review)
formatFileRead truncated the string branch to a 50-line preview; since raw defaults false, the DEFAULT vault.read was that truncated view — an agent reading any >50-line file could not derive a byte-matching edit.window oldText without raw:true, the exact #133 friction ADR-203 retires. The data layer was already byte-faithful; the presentation default undercut it. - string branch: emit full content verbatim, no 50-line slice, no wrapping code fence (body may contain fences — a wrapper would corrupt round-trip) - add non-text/binary passthrough guard (closes a pre-existing latent formatter-error path; reviewer non-blocking note) - test now asserts the FORMATTED (raw:false) output is byte-exact, not just not.toThrow make check green: 0 errors, baseline 5 warnings, 244/244.
This commit is contained in:
parent
f3dac1847e
commit
1f83465d3b
2 changed files with 42 additions and 21 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue