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.
257 lines
9.4 KiB
TypeScript
257 lines
9.4 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import type { SettingsNamespaceView } from '@deepseek-ai/dsh-api-remotes/client'
|
|
import {
|
|
PermissionSettingsController, permissionDefaultOf, refreshPermissionIfLoaded,
|
|
} from '../src/client/settings-store.ts'
|
|
|
|
const SCHEMA = {
|
|
uid: 6,
|
|
refs: {
|
|
1: { type: 'const', value: 'read-only' },
|
|
2: { type: 'const', meta: { description: 'Workspace' }, value: 'workspace-write' },
|
|
3: { type: 'union', list: [1, 2] },
|
|
6: { type: 'object', dict: { defaultPreset: 3 } },
|
|
},
|
|
}
|
|
|
|
function view(defaultPreset: string, revision = 0, schema: SettingsNamespaceView['schema'] = SCHEMA): SettingsNamespaceView {
|
|
return {
|
|
ns: 'permission',
|
|
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 } }
|
|
}
|
|
|
|
describe('permission settings store', () => {
|
|
it('derives dynamic options and host labels from the descriptor schema', () => {
|
|
expect(permissionDefaultOf(view('read-only'))).toEqual({
|
|
currentValue: 'read-only',
|
|
options: [
|
|
{ id: 'read-only', label: 'Read Only' },
|
|
{ id: 'workspace-write', label: 'Workspace' },
|
|
],
|
|
})
|
|
const single = {
|
|
uid: 2,
|
|
refs: {
|
|
1: { type: 'const', meta: { description: '' }, value: 'read-only' },
|
|
2: { type: 'object', dict: { defaultPreset: 1 } },
|
|
},
|
|
}
|
|
expect(permissionDefaultOf(view('read-only', 0, single))).toEqual({
|
|
currentValue: 'read-only',
|
|
options: [{ id: 'read-only', label: 'Read Only' }],
|
|
})
|
|
const undescribed = {
|
|
uid: 2,
|
|
refs: {
|
|
1: { type: 'const', meta: { description: 7 }, value: 'read-only' },
|
|
2: { type: 'object', dict: { defaultPreset: 1 } },
|
|
},
|
|
}
|
|
expect(permissionDefaultOf(view('read-only', 0, undescribed)).options)
|
|
.toEqual([{ id: 'read-only', label: 'Read Only' }])
|
|
})
|
|
|
|
it('rejects malformed values and dynamic enums at the wire boundary', () => {
|
|
expect(() => permissionDefaultOf({ ...view('read-only'), value: {} })).toThrow(/no defaultPreset value/)
|
|
expect(() => permissionDefaultOf(view('read-only', 0, {
|
|
uid: 1, refs: { 1: { type: 'object', dict: {} } },
|
|
}))).toThrow(/no defaultPreset field/)
|
|
expect(() => permissionDefaultOf(view('read-only', 0, {
|
|
uid: 2,
|
|
refs: {
|
|
1: { type: 'union' },
|
|
2: { type: 'object', dict: { defaultPreset: 1 } },
|
|
},
|
|
}))).toThrow(/does not advertise/)
|
|
expect(() => permissionDefaultOf(view('read-only', 0, {
|
|
uid: 4,
|
|
refs: {
|
|
1: { type: 'string' },
|
|
2: { type: 'const', value: 1 },
|
|
3: { type: 'union', list: [1, 2] },
|
|
4: { type: 'object', dict: { defaultPreset: 3 } },
|
|
},
|
|
}))).toThrow(/does not advertise/)
|
|
expect(() => permissionDefaultOf(view('missing'))).toThrow(/does not advertise/)
|
|
})
|
|
|
|
it('loads and writes defaultPreset with optimistic concurrency', async () => {
|
|
const describe = vi.fn(() => Promise.resolve(ok({
|
|
writable: true,
|
|
hasDocument: false,
|
|
namespaces: [view('read-only', 4)],
|
|
})))
|
|
const mutate = vi.fn(() => Promise.resolve(ok(view('workspace-write', 5))))
|
|
const controller = new PermissionSettingsController({
|
|
settings: { describe, mutate } as never,
|
|
})
|
|
await controller.load()
|
|
expect(controller.store.getSnapshot()).toMatchObject({
|
|
status: 'ready',
|
|
writable: true,
|
|
currentValue: 'read-only',
|
|
revision: 4,
|
|
})
|
|
await controller.select('workspace-write')
|
|
expect(mutate).toHaveBeenCalledWith({
|
|
ns: 'permission',
|
|
ops: [{ op: 'set', path: ['defaultPreset'], value: 'workspace-write' }],
|
|
expectedRevision: 4,
|
|
})
|
|
expect(controller.store.getSnapshot()).toMatchObject({
|
|
status: 'ready',
|
|
currentValue: 'workspace-write',
|
|
revision: 5,
|
|
})
|
|
})
|
|
|
|
it('hides the row when the namespace is absent and contains write failures', async () => {
|
|
const describe = vi.fn(() => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [] })))
|
|
const controller = new PermissionSettingsController({
|
|
settings: { describe, mutate: vi.fn() } as never,
|
|
})
|
|
await controller.load()
|
|
expect(controller.store.getSnapshot().status).toBe('unavailable')
|
|
|
|
const failing = new PermissionSettingsController({
|
|
settings: {
|
|
describe: () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('read-only')] })),
|
|
mutate: () => Promise.resolve({
|
|
rpcId: 'test',
|
|
result: {
|
|
ok: false as const,
|
|
error: { code: 'settings-conflict', message: 'stale', details: {} },
|
|
},
|
|
}),
|
|
} as never,
|
|
})
|
|
await failing.load()
|
|
await failing.select('workspace-write')
|
|
expect(failing.store.getSnapshot()).toMatchObject({ status: 'error', error: 'stale' })
|
|
})
|
|
|
|
it('contains read failures, no-ops without a writable view, and ignores stale responses', async () => {
|
|
const first = Promise.withResolvers<ReturnType<typeof ok<{
|
|
writable: boolean
|
|
namespaces: SettingsNamespaceView[]
|
|
}>>>()
|
|
const describe = vi.fn()
|
|
.mockImplementationOnce(() => first.promise)
|
|
.mockResolvedValueOnce(ok({ writable: false, hasDocument: false, namespaces: [view('read-only', 2)] }))
|
|
const mutate = vi.fn()
|
|
const controller = new PermissionSettingsController({
|
|
settings: { describe, mutate } as never,
|
|
})
|
|
const stale = controller.load()
|
|
await controller.load()
|
|
first.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('workspace-write', 1)] }))
|
|
await stale
|
|
expect(controller.store.getSnapshot()).toMatchObject({
|
|
currentValue: 'read-only',
|
|
writable: false,
|
|
revision: 2,
|
|
})
|
|
await controller.select('workspace-write')
|
|
expect(mutate).not.toHaveBeenCalled()
|
|
|
|
const rejected = new PermissionSettingsController({
|
|
settings: {
|
|
describe: () => Promise.resolve({
|
|
rpcId: 'test',
|
|
result: { ok: false as const, error: { code: 'internal', message: 'offline', details: {} } },
|
|
}),
|
|
mutate,
|
|
} as never,
|
|
})
|
|
await rejected.select('workspace-write')
|
|
await rejected.load()
|
|
expect(rejected.store.getSnapshot()).toMatchObject({ status: 'error', error: 'offline' })
|
|
|
|
const thrown = new PermissionSettingsController({
|
|
settings: {
|
|
// Promise consumers must contain unknown rejection values from a
|
|
// transport implementation, including non-Error legacy clients.
|
|
// oxlint-disable-next-line typescript/prefer-promise-reject-errors
|
|
describe: () => Promise.reject('disconnected'),
|
|
mutate,
|
|
} as never,
|
|
})
|
|
await thrown.load()
|
|
expect(thrown.store.getSnapshot()).toMatchObject({ status: 'error', error: 'disconnected' })
|
|
})
|
|
|
|
it('disposal suppresses in-flight reads and writes, and loaded invalidations refetch', async () => {
|
|
const read = Promise.withResolvers<ReturnType<typeof ok<{
|
|
writable: boolean
|
|
namespaces: SettingsNamespaceView[]
|
|
}>>>()
|
|
const describe = vi.fn(() => read.promise)
|
|
const idle = new PermissionSettingsController({ settings: { describe, mutate: vi.fn() } as never })
|
|
refreshPermissionIfLoaded(idle)
|
|
expect(describe).not.toHaveBeenCalled()
|
|
const loading = idle.load()
|
|
idle.dispose()
|
|
read.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('read-only')] }))
|
|
await loading
|
|
expect(idle.store.getSnapshot().status).toBe('loading')
|
|
|
|
const rejectedRead = Promise.withResolvers<ReturnType<typeof ok<{
|
|
writable: boolean
|
|
namespaces: SettingsNamespaceView[]
|
|
}>>>()
|
|
const disposedRead = new PermissionSettingsController({
|
|
settings: { describe: () => rejectedRead.promise, mutate: vi.fn() } as never,
|
|
})
|
|
const reading = disposedRead.load()
|
|
disposedRead.dispose()
|
|
rejectedRead.reject(new Error('late read'))
|
|
await reading
|
|
expect(disposedRead.store.getSnapshot().status).toBe('loading')
|
|
|
|
const mutation = Promise.withResolvers<ReturnType<typeof ok<SettingsNamespaceView>>>()
|
|
const activeDescribe = vi.fn(() => Promise.resolve(ok({
|
|
writable: true,
|
|
hasDocument: false,
|
|
namespaces: [view('read-only')],
|
|
})))
|
|
const active = new PermissionSettingsController({
|
|
settings: {
|
|
describe: activeDescribe,
|
|
mutate: () => mutation.promise,
|
|
} as never,
|
|
})
|
|
await active.load()
|
|
refreshPermissionIfLoaded(active)
|
|
await vi.waitFor(() => { expect(activeDescribe).toHaveBeenCalledTimes(2) })
|
|
const saving = active.select('workspace-write')
|
|
active.dispose()
|
|
mutation.resolve(ok(view('workspace-write', 1)))
|
|
await saving
|
|
expect(active.store.getSnapshot().status).toBe('saving')
|
|
|
|
const rejectedMutation = Promise.withResolvers<ReturnType<typeof ok<SettingsNamespaceView>>>()
|
|
const disposedWrite = new PermissionSettingsController({
|
|
settings: {
|
|
describe: () => Promise.resolve(ok({ writable: true, hasDocument: false, namespaces: [view('read-only')] })),
|
|
mutate: () => rejectedMutation.promise,
|
|
} as never,
|
|
})
|
|
await disposedWrite.load()
|
|
const writing = disposedWrite.select('workspace-write')
|
|
disposedWrite.dispose()
|
|
rejectedMutation.reject(new Error('late write'))
|
|
await writing
|
|
expect(disposedWrite.store.getSnapshot().status).toBe('saving')
|
|
})
|
|
})
|