feat(permission): add new-session default setting

This commit is contained in:
Yichen Jiang
2026-07-31 12:48:19 +08:00
parent 4a061f33d0
commit ee74b5b07a
51 changed files with 1452 additions and 226 deletions
@@ -0,0 +1,127 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
import type { SettingsNamespaceView } from '@deepseek-ai/dsh-client-connection/client'
import { PermissionRow, type PermissionRowProps } from '../src/client/PermissionRow.tsx'
import { en } from '../src/client/locales.ts'
import { PermissionSettingsController } from '../src/client/settings-store.ts'
afterEach(cleanup)
const SCHEMA = {
uid: 4,
refs: {
1: { type: 'const', value: 'read-only' },
2: { type: 'const', value: 'workspace-write' },
3: { type: 'union', list: [1, 2] },
4: { type: 'object', dict: { defaultPreset: 3 } },
},
}
function view(defaultPreset: string, revision = 0): SettingsNamespaceView {
return {
ns: 'permission',
schema: SCHEMA,
value: { defaultPreset },
base: { defaultPreset: 'read-only' },
applies: 'live',
secrets: [],
revision,
}
}
function ok<T>(value: T) {
return { rpcId: 'test', result: { ok: true as const, value } }
}
const dictionary: Record<string, string> = en
const t: PermissionRowProps['t'] = key => dictionary[key] ?? key
const runtime = {
useSessions: (() => { throw new Error('unused') }) as never,
useWorkspaces: (() => { throw new Error('unused') }) as never,
}
function mount(controller: PermissionSettingsController) {
return render(
<PermissionRow
{...runtime}
controller={controller}
useSnapshot={bindSnapshotSelector(controller.store)}
t={t}
/>,
)
}
describe('PermissionRow', () => {
it('loads the descriptor, opens the menu, and selects a new default', async () => {
const mutate = vi.fn(() => Promise.resolve(ok(view('workspace-write', 1))))
const controller = new PermissionSettingsController({
settings: {
describe: () => Promise.resolve(ok({ writable: true, namespaces: [view('read-only')] })),
mutate,
} as never,
})
mount(controller)
const button = await screen.findByRole('button', { name: 'Read Only' })
expect(button.getAttribute('aria-expanded')).toBe('false')
fireEvent.click(button)
expect(button.getAttribute('aria-expanded')).toBe('true')
fireEvent.keyDown(document, { key: 'Escape' })
await waitFor(() => { expect(button.getAttribute('aria-expanded')).toBe('false') })
fireEvent.click(button)
fireEvent.click(button)
expect(button.getAttribute('aria-expanded')).toBe('false')
fireEvent.click(button)
fireEvent.click(screen.getByRole('menuitem', { name: 'Workspace Write' }))
await screen.findByRole('button', { name: 'Workspace Write' })
expect(mutate).toHaveBeenCalledOnce()
})
it('hides an unavailable namespace and disables a read-only provider', async () => {
const absent = new PermissionSettingsController({
settings: {
describe: () => Promise.resolve(ok({ writable: true, namespaces: [] })),
mutate: vi.fn(),
} as never,
})
const rendered = mount(absent)
await waitFor(() => { expect(rendered.container.textContent).toBe('') })
rendered.unmount()
const readonly = new PermissionSettingsController({
settings: {
describe: () => Promise.resolve(ok({ writable: false, namespaces: [view('read-only')] })),
mutate: vi.fn(),
} as never,
})
mount(readonly)
expect((await screen.findByRole('button', { name: 'Read Only' })).hasAttribute('disabled')).toBe(true)
})
it('shows loading and a contained write error', async () => {
const describe = Promise.withResolvers<ReturnType<typeof ok<{
writable: boolean
namespaces: SettingsNamespaceView[]
}>>>()
const controller = new PermissionSettingsController({
settings: {
describe: () => describe.promise,
mutate: () => Promise.resolve({
rpcId: 'test',
result: {
ok: false as const,
error: { code: 'settings-conflict', message: 'changed elsewhere', details: {} },
},
}),
} as never,
})
mount(controller)
expect((await screen.findByRole('button', { name: 'Loading' })).hasAttribute('disabled')).toBe(true)
describe.resolve(ok({ writable: true, namespaces: [view('read-only')] }))
const button = await screen.findByRole('button', { name: 'Read Only' })
fireEvent.click(button)
fireEvent.click(screen.getByRole('menuitem', { name: 'Workspace Write' }))
expect((await screen.findByRole('alert')).textContent).toBe('changed elsewhere')
})
})