Three ui-conversation spec conflicts resolve to master's SlotTestRuntime rewrites. Adaptation to the new outward session face: ISession gains the command verb (the composer chip and the /permission picker submit through it), FixtureSession grows the matching fail-loud stub plus the waitingApproval summary default, and the picker reads the projection through projections.faceOf (the ProjectionsFace shape) instead of the retired store getter.
117 lines
5.1 KiB
TypeScript
117 lines
5.1 KiB
TypeScript
/**
|
|
* ui-permission browser half on a real cordis Context with fake command/
|
|
* sessions faces: the plugin registers the hostBacked /permission popup
|
|
* contribution; options flatten the session's permissions projection with
|
|
* the current value active and `custom` excluded; availability follows the
|
|
* projection key's presence; a pick submits the /permission line through
|
|
* Session.command and surfaces rejection/unmatched as thrown errors; fiber
|
|
* disposal removes the contribution (HMR safety).
|
|
*/
|
|
import { Context } from 'cordis'
|
|
import { describe, expect, it } from 'vitest'
|
|
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import type { CommandContribution } from '@deepseek-ai/dsh-client-ui-command/client'
|
|
import type { PermissionSelect } from '@deepseek-ai/dsh-permission/client'
|
|
import { apply, inject } from '../src/client/index.ts'
|
|
|
|
const sid = (k: string): SessionId => k as SessionId
|
|
|
|
const SELECT: PermissionSelect = {
|
|
options: [
|
|
{ value: 'read-only', name: 'read-only', description: 'Reads only.' },
|
|
{ value: 'workspace-write', name: 'workspace-write' },
|
|
{ value: 'danger-full-access', name: 'danger-full-access' },
|
|
],
|
|
currentValue: 'workspace-write',
|
|
}
|
|
|
|
async function bench() {
|
|
const ctx = new Context()
|
|
let contribution: CommandContribution | undefined
|
|
ctx.provide('command', {
|
|
register(c: CommandContribution) {
|
|
contribution = c
|
|
return () => { contribution = undefined }
|
|
},
|
|
})
|
|
const values = new Map<SessionId, PermissionSelect>()
|
|
const commands: string[] = []
|
|
let commandResult: { ok: boolean; matched?: boolean } = { ok: true, matched: true }
|
|
const session = (id: SessionId) => ({
|
|
projections: {
|
|
faceOf: (key: string) => ({
|
|
getSnapshot: () => (key === 'permissions' ? values.get(id) : undefined),
|
|
subscribe: () => () => {},
|
|
}),
|
|
},
|
|
command: (line: string) => {
|
|
commands.push(line)
|
|
return Promise.resolve(commandResult.ok
|
|
? { ok: true as const, value: { matched: commandResult.matched ?? true } }
|
|
: { ok: false as const, error: { code: 'internal', message: 'boom' } })
|
|
},
|
|
})
|
|
ctx.provide('sessions', {
|
|
binding: (id: SessionId) => (values.has(id) ? { sessionId: id, session: session(id) } : undefined),
|
|
})
|
|
const fiber = ctx.plugin({ inject: [...inject], apply })
|
|
await fiber.await()
|
|
return {
|
|
ctx, fiber, values, commands,
|
|
setResult: (r: { ok: boolean; matched?: boolean }) => { commandResult = r },
|
|
contribution: () => contribution,
|
|
}
|
|
}
|
|
|
|
describe('ui-permission browser plugin', () => {
|
|
it('registers the hostBacked /permission popup contribution', async () => {
|
|
const b = await bench()
|
|
const c = b.contribution()!
|
|
expect(c.name).toBe('permission')
|
|
expect(c.hostBacked).toBe(true)
|
|
expect(c.ui.kind).toBe('popupSelect')
|
|
})
|
|
|
|
it('availability follows the projection key; options mark the current value active and exclude custom', async () => {
|
|
const b = await bench()
|
|
const c = b.contribution()!
|
|
const proj = { sessionId: sid('s1') }
|
|
expect(c.available(proj)).toBe(false)
|
|
b.values.set(sid('s1'), { ...SELECT, options: [...SELECT.options, { value: 'custom', name: 'Custom' }], currentValue: 'custom' })
|
|
expect(c.available(proj)).toBe(true)
|
|
const options = await c.ui.options(proj, new AbortController().signal)
|
|
expect(options.map(option => option.id)).toEqual(['read-only', 'workspace-write', 'danger-full-access'])
|
|
expect(options.every(option => option.active !== true)).toBe(true)
|
|
b.values.set(sid('s1'), SELECT)
|
|
const again = await c.ui.options(proj, new AbortController().signal)
|
|
expect(again.find(option => option.id === 'workspace-write')?.active).toBe(true)
|
|
expect(again.find(option => option.id === 'read-only')?.detail).toBe('Reads only.')
|
|
// A projection that vanished between availability and open throws.
|
|
expect(() => c.ui.options({ sessionId: sid('ghost') }, new AbortController().signal))
|
|
.toThrow(/not available on this host/)
|
|
})
|
|
|
|
it('a pick submits the /permission line; rejection and unmatched throw', async () => {
|
|
const b = await bench()
|
|
const c = b.contribution()!
|
|
const proj = { sessionId: sid('s1') }
|
|
b.values.set(sid('s1'), SELECT)
|
|
await c.ui.onSelect({ id: 'danger-full-access', label: 'danger-full-access' }, proj)
|
|
expect(b.commands).toEqual(['/permission danger-full-access'])
|
|
b.setResult({ ok: false })
|
|
await expect(c.ui.onSelect({ id: 'read-only', label: 'read-only' }, proj)).rejects.toThrow(/permission switch failed/)
|
|
b.setResult({ ok: true, matched: false })
|
|
await expect(c.ui.onSelect({ id: 'read-only', label: 'read-only' }, proj)).rejects.toThrow(/no \/permission command/)
|
|
// An unmaterialized session throws before any submit.
|
|
await expect(c.ui.onSelect({ id: 'read-only', label: 'read-only' }, { sessionId: sid('ghost') }))
|
|
.rejects.toThrow(/not materialized/)
|
|
})
|
|
|
|
it('disposal removes the contribution (HMR safety)', async () => {
|
|
const b = await bench()
|
|
expect(b.contribution()).toBeDefined()
|
|
await b.fiber.dispose()
|
|
expect(b.contribution()).toBeUndefined()
|
|
})
|
|
})
|