A business package imported the Remote assembly for `ctx.remote` and the Connection plugin for the wire types it passes around. The assembly now re-exports the carrier's Client-facing types, so a business package names one package. The re-export is type-only: the carrier's runtime values keep their own module edge, since inlining them here would duplicate the carrier inside the assembly bundle. Four surfaces that had no Remote assembly dependency declare one now.
158 lines
6.2 KiB
TypeScript
158 lines
6.2 KiB
TypeScript
// @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-api-remotes/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: 5,
|
|
refs: {
|
|
1: { type: 'const', value: 'read-only' },
|
|
2: { type: 'const', value: 'workspace-write' },
|
|
3: { type: 'const', value: 'danger-full-access' },
|
|
4: { type: 'union', list: [1, 2, 3] },
|
|
5: { type: 'object', dict: { defaultPreset: 4 } },
|
|
},
|
|
}
|
|
|
|
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}
|
|
load={() => controller.load()}
|
|
select={preset => controller.select(preset)}
|
|
usePermission={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, hasDocument: false, 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: 'Read Only' }))
|
|
expect(mutate).not.toHaveBeenCalled()
|
|
fireEvent.click(button)
|
|
fireEvent.click(screen.getByRole('menuitem', { name: 'Workspace Write' }))
|
|
await screen.findByRole('button', { name: 'Workspace Write' })
|
|
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, hasDocument: false, 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()
|
|
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
|
|
expect(screen.queryByRole('dialog', { name: 'Enable Full access?' })).toBeNull()
|
|
fireEvent.click(screen.getByRole('button', { name: 'Read Only' }))
|
|
fireEvent.click(screen.getByRole('menuitem', { name: 'Full access' }))
|
|
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: {
|
|
describe: () => Promise.resolve(ok({ writable: true, hasDocument: false, 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, hasDocument: 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, hasDocument: false, 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')
|
|
})
|
|
})
|