fix(workspace): keep the default Workspace path language-neutral
The first-use directory name and its stored title came from the Client's startup language, so one product decision carried two incompatible requirements: a path addressed by shell commands, tool arguments, @path references, and session logs, and a label a reader reads. The controller now owns the fixed `default-workspace` directory name and the registry titles the Workspace after that same segment, so `workspace.initializeDefault` takes no request at all — WorkspaceInitializeDefaultRequest and its name validation are gone, and the registry resolver returns a path instead of a path and title. No language reaches the Host. Browser consumers label a Workspace through the new `workspaceDisplayTitle`: a title still equal to the automatic one reads as the localized default name from the `common` namespace, every other title reads verbatim. The Workspace browser resolves it once at the store read, so rows, hover card, search meta, and the rename and delete dialogs inherit it; the hero picker and the Conversation chip resolve it too. Confirming a rename stores the text on screen, which is what stops a row following the language.
This commit is contained in:
1 parent
5e25475857
commit
ff1c7d415f
53 files changed
+297
-195
No files matched your search
@@ -4,36 +4,36 @@ import { defaultWorkspaceDirectory, validateDocumentsDirectory } from '../src/de
|
||||
|
||||
describe('system Documents directory', () => {
|
||||
it.each([
|
||||
['darwin', '/Users/a/文档/\n', '/Users/a/文档/deepseek-harness/默认工作区', 'osascript'],
|
||||
['win32', 'D:\\Redirected Documents\r\n', 'D:\\Redirected Documents\\deepseek-harness\\默认工作区', 'powershell.exe'],
|
||||
['linux', '/home/a/My Documents\n', '/home/a/My Documents/deepseek-harness/默认工作区', 'xdg-user-dir'],
|
||||
['darwin', '/Users/a/文档/\n', '/Users/a/文档/deepseek-harness/default-workspace', 'osascript'],
|
||||
['win32', 'D:\\Redirected Documents\r\n', 'D:\\Redirected Documents\\deepseek-harness\\default-workspace', 'powershell.exe'],
|
||||
['linux', '/home/a/My Documents\n', '/home/a/My Documents/deepseek-harness/default-workspace', 'xdg-user-dir'],
|
||||
] as const)('uses the %s account directory and preserves spaces and Unicode', async (platform, stdout, path, command) => {
|
||||
const run = vi.fn<NativeCommandRunner>(async () => ({ stdout, stderr: '' }))
|
||||
const signal = new AbortController().signal
|
||||
await expect(defaultWorkspaceDirectory('默认工作区', undefined, signal, { platform, run })).resolves.toBe(path)
|
||||
await expect(defaultWorkspaceDirectory(undefined, signal, { platform, run })).resolves.toBe(path)
|
||||
expect(run).toHaveBeenCalledWith(command, expect.any(Array), signal)
|
||||
})
|
||||
|
||||
it.each(['Default workspace', '默认工作区', 'default-workspace'])('uses the configured directory with name %s', async (folder) => {
|
||||
it('uses the configured directory without a system lookup', async () => {
|
||||
const run = vi.fn<NativeCommandRunner>()
|
||||
await expect(defaultWorkspaceDirectory(folder, '/documents', new AbortController().signal, { platform: 'linux', run }))
|
||||
.resolves.toBe(`/documents/deepseek-harness/${folder}`)
|
||||
await expect(defaultWorkspaceDirectory('/documents', new AbortController().signal, { platform: 'linux', run }))
|
||||
.resolves.toBe('/documents/deepseek-harness/default-workspace')
|
||||
expect(run).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['', '\r\n', '/home/a\n'])('rejects an unavailable XDG directory %j', async (stdout) => {
|
||||
const run: NativeCommandRunner = async () => ({ stdout, stderr: '' })
|
||||
await expect(defaultWorkspaceDirectory('Default workspace', undefined, new AbortController().signal, { platform: 'linux', home: '/home/a', run }))
|
||||
await expect(defaultWorkspaceDirectory(undefined, new AbortController().signal, { platform: 'linux', home: '/home/a', run }))
|
||||
.rejects.toThrow('unavailable')
|
||||
})
|
||||
|
||||
it('propagates lookup failure and cancellation', async () => {
|
||||
const run: NativeCommandRunner = async () => { throw new Error('lookup denied') }
|
||||
await expect(defaultWorkspaceDirectory('Default workspace', undefined, new AbortController().signal, { platform: 'darwin', run }))
|
||||
await expect(defaultWorkspaceDirectory(undefined, new AbortController().signal, { platform: 'darwin', run }))
|
||||
.rejects.toThrow('lookup denied')
|
||||
await expect(defaultWorkspaceDirectory('Default workspace', '/documents', AbortSignal.abort(), { platform: 'linux' }))
|
||||
await expect(defaultWorkspaceDirectory('/documents', AbortSignal.abort(), { platform: 'linux' }))
|
||||
.rejects.toThrow()
|
||||
await expect(defaultWorkspaceDirectory('Default workspace', undefined, new AbortController().signal, { platform: 'freebsd' }))
|
||||
await expect(defaultWorkspaceDirectory(undefined, new AbortController().signal, { platform: 'freebsd' }))
|
||||
.rejects.toThrow('unavailable')
|
||||
})
|
||||
|
||||
|
||||
@@ -176,18 +176,18 @@ describe('ClientWorkspaceModel', () => {
|
||||
const model = modelFor(remote)
|
||||
baseline(model)
|
||||
const signal = new AbortController().signal
|
||||
await expect(model.initializeDefault({ directoryName: '默认工作区', title: '默认工作区' }, signal)).resolves.toMatchObject({ ok: true })
|
||||
expect(remote.initializeDefault).toHaveBeenCalledWith({ directoryName: '默认工作区', title: '默认工作区' }, signal)
|
||||
await expect(model.initializeDefault(signal)).resolves.toMatchObject({ ok: true })
|
||||
expect(remote.initializeDefault).toHaveBeenCalledWith(signal)
|
||||
expect(model.getSnapshot().items.map(item => item.workspaceId)).toEqual(['default'])
|
||||
const before = model.getSnapshot()
|
||||
remote.initializeDefault.mockResolvedValueOnce(remoteOk(undefined))
|
||||
await expect(model.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' }))
|
||||
await expect(model.initializeDefault())
|
||||
.resolves.toEqual({ ok: true, value: undefined })
|
||||
expect(model.getSnapshot()).toBe(before)
|
||||
remote.initializeDefault.mockResolvedValueOnce(workspaceError(
|
||||
new RemoteError('gateway/bad-request', 'choose a folder', {}),
|
||||
))
|
||||
await expect(model.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' })).resolves.toMatchObject({ ok: false })
|
||||
await expect(model.initializeDefault()).resolves.toMatchObject({ ok: false })
|
||||
expect(model.getSnapshot()).toBe(before)
|
||||
})
|
||||
|
||||
|
||||
@@ -318,8 +318,7 @@ describe('WorkspaceController', () => {
|
||||
const controller = new WorkspaceController(client.ctx, model)
|
||||
const before = model.getSnapshot()
|
||||
mock.remote.workspace.initializeDefault.mockResolvedValueOnce({ ok: true, value: undefined })
|
||||
await expect(controller.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' }))
|
||||
.resolves.toBeUndefined()
|
||||
await expect(controller.initializeDefault()).resolves.toBeUndefined()
|
||||
expect(model.getSnapshot()).toBe(before)
|
||||
})
|
||||
|
||||
@@ -331,7 +330,7 @@ describe('WorkspaceController', () => {
|
||||
|
||||
expect(controller.list).toBe(model)
|
||||
expect(client.ctx.workspaces.list).toBe(model)
|
||||
await expect(controller.initializeDefault({ directoryName: '默认工作区', title: '默认工作区' }, new AbortController().signal)).resolves.toMatchObject({ workspaceId: 'default' })
|
||||
await expect(controller.initializeDefault(new AbortController().signal)).resolves.toMatchObject({ workspaceId: 'default' })
|
||||
await expect(controller.create({ path: '/work/created' })).resolves.toMatchObject({ workspaceId: 'created' })
|
||||
await expect(controller.rename(wid('one'), 'renamed')).resolves.toMatchObject({ title: 'renamed' })
|
||||
await expect(controller.insertBefore(wid('one'))).resolves.toBeUndefined()
|
||||
@@ -344,7 +343,6 @@ describe('WorkspaceController', () => {
|
||||
await expect(controller.unpinSession(sid('session'))).resolves.toBeUndefined()
|
||||
await expect(controller.delete(wid('one'))).resolves.toBeUndefined()
|
||||
// Each command crosses the wire as one positional request object.
|
||||
expect(mock.log.requests('workspace/initializeDefault')).toEqual([{ directoryName: '默认工作区', title: '默认工作区' }])
|
||||
expect(mock.log.requests('workspace/create')).toEqual([{ path: '/work/created' }])
|
||||
expect(mock.log.requests('workspace/rename')).toEqual([{ workspaceId: 'one', title: 'renamed' }])
|
||||
expect(mock.log.requests('workspace/insertBefore')).toEqual([{ workspaceId: 'one' }])
|
||||
@@ -364,7 +362,7 @@ describe('WorkspaceController', () => {
|
||||
const missingSession = new RemoteError('session/not-found', 'missing session', { sessionId: sid('session') })
|
||||
|
||||
mock.remote.workspace.initializeDefault.mockResolvedValueOnce(err(new RemoteError('gateway/internal', 'directory denied', {})))
|
||||
await expect(controller.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' })).rejects.toBeInstanceOf(WorkspaceCreateError)
|
||||
await expect(controller.initializeDefault()).rejects.toBeInstanceOf(WorkspaceCreateError)
|
||||
|
||||
mock.remote.workspace.create.mockResolvedValueOnce(err(new RemoteError('workspace/invalid-path', 'missing path', { path: '/missing' })))
|
||||
const create = controller.create({ path: '/missing' })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { existsSync, mkdirSync, mkdtempSync, realpathSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { basename, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
|
||||
@@ -10,6 +10,7 @@ import { RemoteError } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import WorkspaceRegistry from '@deepseek-ai/dsh-workspace'
|
||||
import type { WorkspaceId } from '@deepseek-ai/dsh-workspace/types'
|
||||
import WorkspaceController from '../src/index.ts'
|
||||
import { DEFAULT_WORKSPACE_DIRECTORY } from '../src/default-workspace.ts'
|
||||
import { WorkspaceFeed } from '../src/feed.ts'
|
||||
import type { WorkspaceFollowFrame } from '../src/types.ts'
|
||||
import { MemoryStorageBackend } from '../../../storage/storage-domain/tests/helpers/memory-backend.ts'
|
||||
@@ -450,53 +451,30 @@ describe('WorkspaceController follow', () => {
|
||||
describe('first-use Remote', () => {
|
||||
it('reuses an initialized Workspace without looking up system Documents', async () => {
|
||||
const { controller, ctx, root } = await harness({ systemDocuments: true })
|
||||
const workspace = await ctx.workspaceRegistry.initializeDefault(async () => ({ path: root, title: 'Existing default' }))
|
||||
const workspace = await ctx.workspaceRegistry.initializeDefault(async () => root)
|
||||
const signal = AbortSignal.abort()
|
||||
await expect(controller.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' }, signal))
|
||||
.resolves.toMatchObject({ workspace: { workspaceId: workspace!.id, path: root, title: 'Existing default' } })
|
||||
await expect(controller.initializeDefault(signal))
|
||||
.resolves.toMatchObject({ workspace: { workspaceId: workspace!.id, path: root, title: basename(root) } })
|
||||
})
|
||||
|
||||
it.each(['', ' ', '.', '..', '../outside', 'nested/name', 'nested\\name', 'C:outside', 'name\0', '.. ', 'tail.'])(
|
||||
'rejects invalid directory name %j before invoking the registry', async (directoryName) => {
|
||||
const { controller, ctx } = await harness()
|
||||
const initialize = vi.spyOn(ctx.workspaceRegistry, 'initializeDefault').mockResolvedValue(undefined)
|
||||
await expect(controller.initializeDefault({ directoryName, title: 'Default workspace' }, new AbortController().signal))
|
||||
.rejects.toMatchObject({ code: 'gateway/bad-request' })
|
||||
expect(initialize).not.toHaveBeenCalled()
|
||||
},
|
||||
)
|
||||
|
||||
it('rejects a blank initial title before invoking the registry', async () => {
|
||||
const { controller, ctx } = await harness()
|
||||
const initialize = vi.spyOn(ctx.workspaceRegistry, 'initializeDefault').mockResolvedValue(undefined)
|
||||
await expect(controller.initializeDefault({ directoryName: 'default-workspace', title: ' ' }, new AbortController().signal))
|
||||
.rejects.toMatchObject({ code: 'gateway/bad-request' })
|
||||
expect(initialize).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses the requested display title independently of the directory name', async () => {
|
||||
const { controller, root } = await harness()
|
||||
const result = await controller.initializeDefault({ directoryName: 'default-workspace', title: 'Default workspace' }, new AbortController().signal)
|
||||
expect(result?.workspace).toMatchObject({ path: join(root, 'deepseek-harness', 'default-workspace'), title: 'Default workspace' })
|
||||
})
|
||||
|
||||
it('returns a durable Workspace without allocating a Session', async () => {
|
||||
it('returns a durable Workspace named after its fixed directory without allocating a Session', async () => {
|
||||
const { controller, ctx, root } = await harness()
|
||||
const signal = new AbortController().signal
|
||||
const result = await controller.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' }, signal)
|
||||
expect(result!.workspace.path).toBe(join(root, 'deepseek-harness', 'Default workspace'))
|
||||
const result = await controller.initializeDefault(signal)
|
||||
expect(result!.workspace.path).toBe(join(root, 'deepseek-harness', DEFAULT_WORKSPACE_DIRECTORY))
|
||||
expect(result!.workspace.title).toBe(DEFAULT_WORKSPACE_DIRECTORY)
|
||||
expect(existsSync(result!.workspace.path)).toBe(true)
|
||||
expect(ctx.sessions.list()).toEqual([])
|
||||
expect(await controller.initializeDefault({ directoryName: '默认工作区', title: '默认工作区' }, signal)).toEqual(result)
|
||||
expect(await controller.initializeDefault(signal)).toEqual(result)
|
||||
})
|
||||
|
||||
it('skips ineligible first use and propagates preparation failures', async () => {
|
||||
const { controller, ctx, root } = await harness()
|
||||
await ctx.workspaceRegistry.create(root)
|
||||
await expect(controller.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' }, new AbortController().signal))
|
||||
await expect(controller.initializeDefault(new AbortController().signal))
|
||||
.resolves.toBeUndefined()
|
||||
vi.spyOn(ctx.workspaceRegistry, 'initializeDefault').mockRejectedValueOnce(new Error('permission denied'))
|
||||
await expect(controller.initializeDefault({ directoryName: 'Default workspace', title: 'Default workspace' }, new AbortController().signal))
|
||||
await expect(controller.initializeDefault(new AbortController().signal))
|
||||
.rejects.toThrow('permission denied')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user