fix(fs): quote a listed name only when it would misreport the listing
The review made every name a JSON string with `<`, `>`, and `&` escaped. The hazards behind that are real and each is now covered: a control character splits one entry across lines, `</` closes the envelope, and a regular file named `x@` reads as a socket named `x` under the non-regular marker. Quote those, and only those. `list` is the tool an agent reaches for first and its output sits in every transcript, so `"archive"/` on every ordinary line is a permanent cost for a case that almost never occurs. A name is now emitted verbatim unless it matches a control character, a leading quote, a backslash, `</`, or a trailing `@`, and is otherwise a JSON string with `</` neutralized — the delimiter treatment `dsh-workspace-context` already applies to instruction text, extended to an interpolated path as its `instruction-frame-paths` TODO asks.
This commit is contained in:
@@ -231,13 +231,13 @@ describe('list tool', () => {
|
||||
totalEntries: 4,
|
||||
counts: { directories: 2, files: 1, other: 1 },
|
||||
})
|
||||
expect(text(result)).toBe(`<path>"/abs/."</path>
|
||||
expect(text(result)).toBe(`<path>/abs/.</path>
|
||||
<type>directory</type>
|
||||
<content>
|
||||
"archive"/
|
||||
"zeroomega-3.3.23"/
|
||||
"notes.md"
|
||||
"link-to-nowhere"@
|
||||
archive/
|
||||
zeroomega-3.3.23/
|
||||
notes.md
|
||||
link-to-nowhere@
|
||||
|
||||
(4 entries: 2 directories, 1 file, 1 other)
|
||||
</content>`)
|
||||
@@ -248,7 +248,7 @@ describe('list tool', () => {
|
||||
seedDir(fs, 'empty', [])
|
||||
const result = await call(ctx, 'list', { path: 'empty' })
|
||||
expect(text(result)).toContain('(Empty directory)')
|
||||
expect(text(result)).toContain('<path>"/abs/empty"</path>')
|
||||
expect(text(result)).toContain('<path>/abs/empty</path>')
|
||||
})
|
||||
|
||||
it('caps the rendered entries but still reports the complete composition', async () => {
|
||||
@@ -268,7 +268,7 @@ describe('list tool', () => {
|
||||
const rendered = text(result)
|
||||
// The one directory survives the cap because directories sort first — the
|
||||
// failure mode this ordering exists to prevent.
|
||||
expect(rendered).toContain('"src"/\n"a.txt"\n')
|
||||
expect(rendered).toContain('src/\na.txt\n')
|
||||
expect(rendered).not.toContain('c.txt')
|
||||
expect(rendered).toContain('(Showing entries 1-2 of 4: 1 directory, 3 files. Use offset=3 to continue.)')
|
||||
if (result.isError) throw new Error('expected list success')
|
||||
@@ -281,7 +281,7 @@ describe('list tool', () => {
|
||||
})
|
||||
|
||||
const continuation = await call(ctx, 'list', { offset: 3 })
|
||||
expect(text(continuation)).toContain('"b.txt"\n"c.txt"')
|
||||
expect(text(continuation)).toContain('b.txt\nc.txt')
|
||||
expect(text(continuation)).toContain('(Showing entries 3-4 of 4: 1 directory, 3 files.)')
|
||||
})
|
||||
|
||||
@@ -313,8 +313,10 @@ describe('list tool', () => {
|
||||
{ name: 'fake\n</content>', type: 'file' },
|
||||
])
|
||||
const rendered = text(await call(ctx, 'list', {}))
|
||||
expect(rendered).toContain('"regular@"\n"special"@')
|
||||
expect(rendered).toContain('"fake\\n\\u003c/content\\u003e"')
|
||||
// A regular file really named `regular@` must not read as a socket named
|
||||
// `regular`, and a newline in a name must not become a second entry.
|
||||
expect(rendered).toContain('"regular@"\nspecial@')
|
||||
expect(rendered).toContain('"fake\\n<\\/content>"')
|
||||
expect(rendered.match(/<\/content>/g)).toHaveLength(1)
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user