mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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:
@@ -3,6 +3,8 @@ printf '%s\n' \
|
||||
'archive/a.ts' \
|
||||
'archive/b.ts' \
|
||||
'archive/c.ts' \
|
||||
'old\one' \
|
||||
'old\two' \
|
||||
'src/index.ts' \
|
||||
'docs/guide.md' \
|
||||
'test/spec.ts'
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{"type":"assistant/chunk","seq":9,"time":1785218400010,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
|
||||
{"type":"assistant/message","seq":10,"time":1785218400011,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"tool-call","id":"glob-sampling-call","name":"glob","arguments":"{\"pattern\":\"*\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-pro"},"id":"d8c174b5-2f08-49b3-80d5-a69aabefbd7a"},"usage":{"inputTokens":1,"outputTokens":1}},"sourceEventSeqs":[5,6,7,8,9],"surfaceOp":"append"}
|
||||
{"type":"tool/call","seq":11,"time":1785218400012,"data":{"turn":1,"step":1,"callId":"glob-sampling-call","name":"glob","arguments":"{\"pattern\":\"*\"}"}}
|
||||
{"type":"tool/result","seq":12,"time":1785218400013,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"glob-sampling-call"},"content":[{"type":"tool-result","toolCallId":"glob-sampling-call","content":[{"type":"text","text":"archive/a.ts\nsrc/index.ts\ndocs/guide.md\ntest/spec.ts\n\n(Showing 4 of 6 paths, sampled across 4 of the 4 top-level entries this pattern matched instead of taken in modification-time order. The complete result could not be saved; narrow pattern or path to see more.)"}],"isError":false}],"role":"user","id":"e9711775-0ea5-4383-a562-76a6b49a4742"}},"sourceEventSeqs":[11],"surfaceOp":"append"}
|
||||
{"type":"tool/result","seq":12,"time":1785218400013,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"glob-sampling-call"},"content":[{"type":"tool-result","toolCallId":"glob-sampling-call","content":[{"type":"text","text":"archive/a.ts\nold\\one\nold\\two\nsrc/index.ts\n\n(Showing 4 of 8 paths, sampled across 4 of the 6 top-level entries this pattern matched instead of taken in modification-time order. Narrow path to inspect a specific subtree. The complete result could not be saved; narrow pattern or path to see more.)"}],"isError":false}],"role":"user","id":"e9711775-0ea5-4383-a562-76a6b49a4742"}},"sourceEventSeqs":[11],"surfaceOp":"append"}
|
||||
{"type":"step/end","seq":13,"time":1785218400014,"data":{"turn":1,"step":1}}
|
||||
{"type":"step/start","seq":14,"time":1785218400015,"data":{"turn":1,"step":2}}
|
||||
{"type":"assistant/chunk","seq":15,"time":1785218400016,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"text"}}}
|
||||
|
||||
@@ -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