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
#	packages/host/directory-picker-browse/README.i18n.yaml
#	packages/host/directory-picker-browse/package.json
#	pnpm-lock.yaml
This commit is contained in:
creatixchu
2026-07-29 04:12:44 +08:00
16 changed files with 155 additions and 35 deletions

View File

@@ -130,6 +130,29 @@ describe('directory-picker-browse client half', () => {
}
})
it('rolls back the zh dictionary when a rival already owns the namespace en slot', async () => {
const b = await bench()
b.declare()
const locale = b.ctx.get('locale') as LocaleService
const disposeRival = locale.register('directory-browser', 'en', { 'browser.title': 'rival' })
const rejections: unknown[] = []
const onUnhandled = (reason: unknown): void => { rejections.push(reason) }
// cordis re-raises the apply throw as a late rejection (installFailLoud's contract).
process.on('unhandledRejection', onUnhandled)
try {
const fiber = b.ctx.plugin({ inject: [...inject], apply })
await expect(fiber.await()).rejects.toThrow(/already has locale/)
// The zh registration rolled back with the failure: once the rival
// leaves, a fresh registrant owns the whole namespace again.
disposeRival()
const disposeZh = locale.register('directory-browser', 'zh', { 'browser.title': '空闲' })
disposeZh()
} finally {
await new Promise(resolve => setTimeout(resolve, 0))
process.off('unhandledRejection', onUnhandled)
}
})
it('registers the dialog dictionaries and binds this package namespace', async () => {
const b = await bench()
b.declare()

View File

@@ -7,7 +7,8 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { DirectoryPickerError } from '@deepseek-ai/dsh-host-directory-picker'
import type { DirectoryPickerBrowseCapability } from '@deepseek-ai/dsh-host-directory-picker'
import BrowseDirectoryPicker, { fullyQualified } from '../src/index.ts'
import BrowseDirectoryPicker, { boundedInsert, fullyQualified } from '../src/index.ts'
import type { ListingCandidate } from '../src/index.ts'
let root: string
let capability: DirectoryPickerBrowseCapability
@@ -21,6 +22,13 @@ beforeAll(async () => {
await writeFile(join(root, 'notes.txt'), 'not a directory')
await symlink(join(root, 'projects'), join(root, 'linked'), 'junction')
await symlink(join(root, 'gone'), join(root, 'broken'), 'junction')
try {
await symlink(join(root, 'notes.txt'), join(root, 'file-link'))
} catch {
// Windows denies unprivileged file symlinks; the file-link row only
// feeds the POSIX lanes' coverage of the symlink-to-file arm, and every
// assertion below expects it to be filtered out anyway.
}
const ctx = new Context()
const fiber = ctx.plugin(BrowseDirectoryPicker)
@@ -63,11 +71,31 @@ describe('BrowseDirectoryPicker', () => {
const exact = await bounded.list(join(root, 'projects'))
expect(exact.entries.map(entry => entry.name)).toEqual(['harness'])
expect(exact.truncated).toBe(false)
// A level that fits the window but exceeds the bound (two rows, bound
// one): the in-window extra row proves the cut without any eviction.
await mkdir(join(root, 'projects', 'harness', 'a'))
await mkdir(join(root, 'projects', 'harness', 'b'))
const inWindow = await bounded.list(join(root, 'projects', 'harness'))
expect(inWindow.entries.map(entry => entry.name)).toEqual(['a'])
expect(inWindow.truncated).toBe(true)
} finally {
await fiber.dispose()
}
})
it('boundedInsert keeps the window name-sorted and bounded, reporting evictions', () => {
const candidate = (name: string): ListingCandidate => ({ name, isDirectory: true, isSymbolicLink: false })
const window: ListingCandidate[] = []
expect(boundedInsert(window, candidate('m'), 2)).toBe(false)
expect(boundedInsert(window, candidate('z'), 2)).toBe(false)
// A smaller name lands in place and pushes the current largest out.
expect(boundedInsert(window, candidate('a'), 2)).toBe(true)
expect(window.map(entry => entry.name)).toEqual(['a', 'm'])
// A name beyond the window's tail enters last and leaves immediately.
expect(boundedInsert(window, candidate('t'), 2)).toBe(true)
expect(window.map(entry => entry.name)).toEqual(['a', 'm'])
})
it('reports the ancestry as jump-target crumbs ending at the listed directory', async () => {
const listing = await capability.list(join(root, 'projects'))
const tail = listing.crumbs.at(-1)!