merge master and address permission settings review

This commit is contained in:
Yichen Jiang
2026-07-31 13:24:30 +08:00
89 changed files with 1336 additions and 295 deletions

View File

@@ -14,8 +14,11 @@ import { SlotsService, type SessionId } from '@deepseek-ai/dsh-client-runtime/cl
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
import type { CommandDecoration } from '@deepseek-ai/dsh-client-ui-command/client'
import type { PermissionSelect } from '@deepseek-ai/dsh-permission/client'
import { PermissionRow } from '../src/client/PermissionRow.tsx'
import {
PermissionRow, type PermissionRowInjected,
} from '../src/client/PermissionRow.tsx'
import { apply, inject } from '../src/client/index.ts'
import { accessEn } from '../src/client/locales.ts'
const sid = (k: string): SessionId => k as SessionId
@@ -32,6 +35,7 @@ async function bench() {
const ctx = new Context()
await ctx.plugin(SlotsService)
const locale = new LocaleService(ctx)
locale.setLocale('en')
ctx.provide('locale', locale)
ctx.slots.register({
name: 'root',
@@ -96,9 +100,10 @@ describe('ui-permission browser plugin', () => {
expect(c.ui.kind).toBe('popupSelect')
const row = b.permissionRow()!
expect(row.options).toEqual({ id: 'permission', order: -20 })
const injected = row.inject?.()
expect(injected?.controller).toBeDefined()
expect(typeof injected?.useSnapshot).toBe('function')
const injected = row.inject?.() as PermissionRowInjected | undefined
expect(injected?.hooks.permission).toBeDefined()
expect(typeof injected?.load).toBe('function')
expect(typeof injected?.select).toBe('function')
})
it('availability follows the projection key; options mark the current value active and exclude custom', async () => {
@@ -116,7 +121,14 @@ describe('ui-permission browser plugin', () => {
expect(again.find(option => option.id === 'workspace-write')?.active).toBe(true)
expect(again.find(option => option.id === 'read-only')?.detail).toBe('Reads only.')
// Kebab-case names title-case; non-kebab host-configured names pass through.
expect(again.map(option => option.label)).toEqual(['Read Only', 'Workspace Write', 'Danger Full Access'])
expect(again.map(option => option.label)).toEqual(['Read Only', 'Workspace Write', 'Full access'])
expect(again.find(option => option.id === 'danger-full-access')?.confirmation).toEqual({
title: 'Enable Full access?',
description: accessEn['confirm.description'],
acknowledgeLabel: 'I understand the risks and want to continue',
cancelLabel: 'Cancel',
confirmLabel: 'Enable Full access',
})
b.values.set(sid('s1'), { ...SELECT, options: [{ value: 'plain', name: 'Ask Every Time' }] })
const passthrough = await c.ui.options(proj, new AbortController().signal)
expect(passthrough[0]?.label).toBe('Ask Every Time')

View File

@@ -10,12 +10,13 @@ import { PermissionSettingsController } from '../src/client/settings-store.ts'
afterEach(cleanup)
const SCHEMA = {
uid: 4,
uid: 5,
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 } },
3: { type: 'const', value: 'danger-full-access' },
4: { type: 'union', list: [1, 2, 3] },
5: { type: 'object', dict: { defaultPreset: 4 } },
},
}
@@ -46,8 +47,9 @@ function mount(controller: PermissionSettingsController) {
return render(
<PermissionRow
{...runtime}
controller={controller}
useSnapshot={bindSnapshotSelector(controller.store)}
load={() => controller.load()}
select={preset => controller.select(preset)}
usePermission={bindSnapshotSelector(controller.store)}
t={t}
/>,
)
@@ -78,6 +80,27 @@ describe('PermissionRow', () => {
expect(mutate).toHaveBeenCalledOnce()
})
it('requires explicit acknowledgement before saving Full access', async () => {
const mutate = vi.fn(() => Promise.resolve(ok(view('danger-full-access', 1))))
const controller = new PermissionSettingsController({
settings: {
describe: () => Promise.resolve(ok({ writable: true, namespaces: [view('read-only')] })),
mutate,
} as never,
})
mount(controller)
fireEvent.click(await screen.findByRole('button', { name: 'Read Only' }))
fireEvent.click(screen.getByRole('menuitem', { name: 'Full access' }))
expect(mutate).not.toHaveBeenCalled()
const dialog = screen.getByRole('dialog', { name: 'Enable Full access?' })
const enable = screen.getByRole('button', { name: 'Enable Full access' })
expect((enable as HTMLButtonElement).disabled).toBe(true)
fireEvent.click(screen.getByRole('checkbox'))
fireEvent.click(enable)
await waitFor(() => { expect(mutate).toHaveBeenCalledOnce() })
expect(dialog.isConnected).toBe(false)
})
it('hides an unavailable namespace and disables a read-only provider', async () => {
const absent = new PermissionSettingsController({
settings: {