mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/feat/directory-picker' into feat/workspace-directory-browser
# Conflicts: # .agents/notes/implemented/architecture/2026-07-28-directory-picker-capability-seam.i18n.yaml
This commit is contained in:
@@ -138,6 +138,11 @@ function asError(reason: unknown): Error {
|
||||
return reason instanceof Error ? reason : new Error(String(reason))
|
||||
}
|
||||
|
||||
/* v8 ignore start -- a close failure of an abandoned handle has no consumer, and forcing one needs a filesystem torn down mid-request. */
|
||||
/** Swallow the close failure of a handle its caller already departed. */
|
||||
function swallowCloseFailure(): void {}
|
||||
/* v8 ignore stop */
|
||||
|
||||
/** Message text of an unknown thrown value. */
|
||||
function messageOf(error: unknown): string {
|
||||
/* v8 ignore next -- node:fs rejects with Error instances; the String arm only satisfies the unknown narrowing. */
|
||||
@@ -149,13 +154,19 @@ function messageOf(error: unknown): string {
|
||||
* non-directories and broken/cyclic links (skipped silently — the browser
|
||||
* shows what can be entered, and a broken link cannot).
|
||||
*/
|
||||
async function directoryRow(parent: string, name: string, isDirectory: boolean, isSymbolicLink: boolean): Promise<DirectoryEntry | null> {
|
||||
async function directoryRow(
|
||||
parent: string, name: string, isDirectory: boolean, isSymbolicLink: boolean, signal: AbortSignal | undefined,
|
||||
): Promise<DirectoryEntry | null> {
|
||||
const path = join(parent, name)
|
||||
let enterable = isDirectory
|
||||
if (!enterable && isSymbolicLink) {
|
||||
try {
|
||||
enterable = (await stat(path)).isDirectory()
|
||||
// The probe races the caller too: a symlink target on a stalled
|
||||
// network filesystem must not keep a departed caller's request alive.
|
||||
enterable = (await raceAbort(stat(path), signal)).isDirectory()
|
||||
} catch {
|
||||
/* v8 ignore next 2 -- an abort landing mid-probe needs a stalled stat; the per-candidate check in list covers the settled path. */
|
||||
if (signal?.aborted) throw asError(signal.reason)
|
||||
// Broken or cyclic symlink: stat is the probe, failure means "not enterable".
|
||||
return null
|
||||
}
|
||||
@@ -231,8 +242,10 @@ export default class BrowseDirectoryPicker extends DirectoryPicker {
|
||||
const level = await raceAbort(opening, signal).catch((error: unknown) => {
|
||||
// The abandoned open can still mint a handle after the abort won;
|
||||
// close it so a departed caller cannot leak a descriptor. (A lost
|
||||
// race against opendir's own rejection has nothing to close.)
|
||||
void opening.then(async (dir) => { await dir.close() }, () => {
|
||||
// race against opendir's own rejection has nothing to close, and
|
||||
// the close's own failure is swallowed — the request already
|
||||
// returned, so a cleanup error has no consumer.)
|
||||
void opening.then(dir => dir.close().catch(swallowCloseFailure), () => {
|
||||
// Already rejected: raceAbort surfaced or swallowed it.
|
||||
})
|
||||
throw error
|
||||
@@ -248,10 +261,18 @@ export default class BrowseDirectoryPicker extends DirectoryPicker {
|
||||
if (boundedInsert(window, candidate, keep)) evicted = true
|
||||
}
|
||||
} finally {
|
||||
// Manual read() never auto-closes; close on every exit, the aborted
|
||||
// one included (its abandoned read settles against the closed handle
|
||||
// and raceAbort already swallowed that settlement).
|
||||
await level.close()
|
||||
// Manual read() never auto-closes; close on every exit. The aborted
|
||||
// exit must not await it — Node queues close behind any in-flight
|
||||
// read, so awaiting would chain the departed caller back onto the
|
||||
// very stall the abort escaped (the abandoned read's settlement is
|
||||
// already swallowed by raceAbort).
|
||||
const closing = level.close()
|
||||
/* v8 ignore next 3 -- an abort between open and close needs a stalled read; the abandoned-close arm has no observable outcome. */
|
||||
if (signal?.aborted) {
|
||||
closing.catch(swallowCloseFailure)
|
||||
} else {
|
||||
await closing
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// An abort is the caller's own reason, not an unreadable directory.
|
||||
@@ -261,7 +282,10 @@ export default class BrowseDirectoryPicker extends DirectoryPicker {
|
||||
const entries: DirectoryEntry[] = []
|
||||
let truncated = evicted
|
||||
for (const candidate of window) {
|
||||
const row = await directoryRow(target, candidate.name, candidate.isDirectory, candidate.isSymbolicLink)
|
||||
// A caller that departed between reads and probes stops before the
|
||||
// next probe (each probe's own await is raced inside directoryRow).
|
||||
signal?.throwIfAborted()
|
||||
const row = await directoryRow(target, candidate.name, candidate.isDirectory, candidate.isSymbolicLink, signal)
|
||||
if (row === null) continue
|
||||
if (entries.length === this.config.maxEntries) {
|
||||
truncated = true
|
||||
|
||||
@@ -95,8 +95,13 @@ describe('BrowseDirectoryPicker', () => {
|
||||
// own and there is nothing to close.
|
||||
await expect(capability.list(join(root, 'no-such-dir'), gone.signal)).rejects.toThrow('caller left')
|
||||
await new Promise(resolve => setTimeout(resolve, 10))
|
||||
// A live signal changes nothing about ordinary failures.
|
||||
// A live signal leaves a normal listing untouched — the reads and the
|
||||
// symlink probes race it without ever losing.
|
||||
const live = new AbortController()
|
||||
const complete = await capability.list(root, live.signal)
|
||||
expect(complete.truncated).toBe(false)
|
||||
expect(complete.entries.map(entry => entry.name)).toContain('linked')
|
||||
// A live signal changes nothing about ordinary failures.
|
||||
const missing = join(root, 'no-such-dir')
|
||||
const failure = await capability.list(missing, live.signal).catch((error: unknown) => error)
|
||||
expect(failure).toBeInstanceOf(DirectoryPickerError)
|
||||
|
||||
Reference in New Issue
Block a user