fix(fs-search): respect platform path separators
Group paths using node:path.sep so POSIX backslashes remain filename characters while Windows continues to treat them as separators.
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import { sep } from 'node:path'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { GenericCallView } from '@deepseek-ai/dsh-tools'
|
||||
import type { SpillRef } from '@deepseek-ai/dsh-spill'
|
||||
@@ -112,16 +113,25 @@ export interface GlobSample {
|
||||
|
||||
/** Remove the displayed search-root prefix before choosing a top-level group. */
|
||||
function relativeToSearchRoot(path: string, root: string): string {
|
||||
if (root === '.') return path.replace(/^\.[\\/]/, '')
|
||||
const trimmedRoot = root.replace(/[\\/]+$/, '')
|
||||
if (trimmedRoot.length === 0) return path.replace(/^[\\/]+/, '')
|
||||
if (root === '.') return path.startsWith(`.${sep}`) ? path.slice(2) : path
|
||||
let rootEnd = root.length
|
||||
while (rootEnd > 0 && root[rootEnd - 1] === sep) rootEnd -= 1
|
||||
const trimmedRoot = root.slice(0, rootEnd)
|
||||
if (trimmedRoot.length === 0) return stripLeadingSeparators(path)
|
||||
if (path === trimmedRoot) return ''
|
||||
if (path.startsWith(`${trimmedRoot}/`) || path.startsWith(`${trimmedRoot}\\`)) {
|
||||
if (path.startsWith(`${trimmedRoot}${sep}`)) {
|
||||
return path.slice(trimmedRoot.length + 1)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
/** Strip only separators recognized by the execution platform. */
|
||||
function stripLeadingSeparators(path: string): string {
|
||||
let start = 0
|
||||
while (path[start] === sep) start += 1
|
||||
return path.slice(start)
|
||||
}
|
||||
|
||||
/**
|
||||
* The leading path segment of one display path — the top-level entry, relative
|
||||
* to the search root, that the path sits under. A path with no separator is its
|
||||
@@ -131,8 +141,8 @@ function relativeToSearchRoot(path: string, root: string): string {
|
||||
* empty group.
|
||||
*/
|
||||
function topLevelSegment(path: string): string {
|
||||
const trimmed = path.replace(/^[\\/]+/, '')
|
||||
const cut = trimmed.search(/[\\/]/)
|
||||
const trimmed = stripLeadingSeparators(path)
|
||||
const cut = trimmed.indexOf(sep)
|
||||
return cut === -1 ? trimmed : trimmed.slice(0, cut)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { join } from 'node:path'
|
||||
import { join, sep } from 'node:path'
|
||||
import { createUserMessage, CallId } from '@deepseek-ai/dsh-llm'
|
||||
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
|
||||
import ToolRegistry, { TOOL_ABORTED_BEFORE_DISPATCH, type ToolExecutionToken } from '@deepseek-ai/dsh-tools'
|
||||
@@ -582,14 +582,28 @@ describe('cross-directory sampling', () => {
|
||||
.toEqual({ items: ['./vendor/a.ts', './src/b.ts'], shown: 2, total: 2 })
|
||||
expect(sampleAcrossTopLevel(['/vendor/a.ts', '/src/b.ts'], 2, '/'))
|
||||
.toEqual({ items: ['/vendor/a.ts', '/src/b.ts'], shown: 2, total: 2 })
|
||||
expect(sampleAcrossTopLevel(['C:\\root\\a\\one', 'C:\\root\\b\\two'], 2, 'C:\\root'))
|
||||
.toEqual({ items: ['C:\\root\\a\\one', 'C:\\root\\b\\two'], shown: 2, total: 2 })
|
||||
const rooted = [
|
||||
['root', 'a', 'one'].join(sep),
|
||||
['root', 'a', 'two'].join(sep),
|
||||
['root', 'b', 'three'].join(sep),
|
||||
]
|
||||
expect(sampleAcrossTopLevel(rooted, 2, 'root'))
|
||||
.toEqual({ items: [rooted[0], rooted[2]], shown: 2, total: 2 })
|
||||
expect(sampleAcrossTopLevel(['other/a.ts'], 1, 'src'))
|
||||
.toEqual({ items: ['other/a.ts'], shown: 1, total: 1 })
|
||||
expect(sampleAcrossTopLevel(['src'], 1, 'src'))
|
||||
.toEqual({ items: ['src'], shown: 1, total: 1 })
|
||||
})
|
||||
|
||||
it.skipIf(process.platform === 'win32')('treats POSIX backslashes as filename characters', () => {
|
||||
const paths = ['old\\one', 'old\\two', 'src/a']
|
||||
expect(sampleAcrossTopLevel(paths, 2)).toEqual({
|
||||
items: ['old\\one', 'old\\two'],
|
||||
shown: 2,
|
||||
total: 3,
|
||||
})
|
||||
})
|
||||
|
||||
it('handles more top-level groups than the JavaScript argument limit', () => {
|
||||
const paths = Array.from({ length: 125_000 }, (_, index) => `dir-${index}/file.txt`)
|
||||
expect(sampleAcrossTopLevel(paths, 100)).toMatchObject({ shown: 100, total: 125_000 })
|
||||
|
||||
Reference in New Issue
Block a user