import { describe, expect, it } from 'vitest' import { Context } from 'cordis' import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session' import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' import type { ApprovalPolicy } from '@deepseek-ai/dsh-user-approval' import PermissionService, { CUSTOM_PRESET, effectivePermissionPreset, PERMISSION_SETTINGS_NAMESPACE, } from '@deepseek-ai/dsh-permission' import type { Config } from '@deepseek-ai/dsh-permission' import { Settings } from '@deepseek-ai/dsh-settings' import type { SettingsNamespace } from '@deepseek-ai/dsh-settings' /** Writable memory provider for the permission/settings lifecycle specs. */ class MemorySettings extends Settings { readonly doc: Record = {} readonly writable = true protected load(): Promise> { return Promise.resolve(structuredClone(this.doc)) } protected persist(ns: SettingsNamespace, section: Record): Promise { this.doc[ns] = structuredClone(section) return Promise.resolve() } } async function mounted(options: { config?: Config bashDefault?: SandboxMode | undefined approvalDefault?: ApprovalPolicy | undefined } = {}): Promise { const ctx = new Context() await ctx.plugin(SessionStore) ctx.provide('bash', { sandboxMode: 'bashDefault' in options ? options.bashDefault : 'workspace-write', resolve() { throw new Error('permission tests do not execute bash') }, run() { throw new Error('permission tests do not execute bash') }, start() { throw new Error('permission tests do not execute bash') }, }) ctx.provide('approval', { config: { policy: 'approvalDefault' in options ? options.approvalDefault : 'ask' } }) await ctx.plugin(PermissionService, options.config ?? {}) return ctx } function freshSession(id: string): Session { return Session.create(SessionId(id)) } async function mountedStore(options: { approvalDefault?: ApprovalPolicy | undefined } = {}): Promise { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(MemorySettings) ctx.provide('bash', { sandboxMode: 'workspace-write', resolve() { throw new Error('permission tests do not execute bash') }, run() { throw new Error('permission tests do not execute bash') }, start() { throw new Error('permission tests do not execute bash') }, }) ctx.provide('approval', { config: { policy: 'approvalDefault' in options ? options.approvalDefault : 'ask' }, }) await ctx.plugin(PermissionService, {}) return ctx } describe('effectivePermissionPreset', () => { it('folds to the last event, or undefined without one', () => { const session = freshSession('sess-fold') expect(effectivePermissionPreset(session.events)).toBeUndefined() session.append('permission/preset', { preset: 'danger-full-access' }) session.append('permission/preset', { preset: 'workspace-write' }) expect(effectivePermissionPreset(session.events)).toBe('workspace-write') // The backward scan steps over non-preset events to the latest selection. session.append('sandbox/mode', { mode: 'read-only' }) expect(effectivePermissionPreset(session.events)).toBe('workspace-write') }) }) describe('PermissionService', () => { it('advertises the preset table in declaration order and resolves bundles', async () => { const ctx = await mounted() expect(ctx.permission.names).toEqual(['workspace-write', 'danger-full-access']) expect(ctx.permission.resolve('danger-full-access')).toMatchObject({ sandbox: 'danger-full-access', approval: 'never' }) expect(() => ctx.permission.resolve('plan')).toThrow(/unknown preset "plan"/) }) it('current() derives from the effective knobs: composition defaults hit workspace-write, a switch hits its preset', async () => { const ctx = await mounted() const session = freshSession('sess-current') expect(ctx.permission.current(session.events)).toBe('workspace-write') ctx.permission.set(session, 'danger-full-access') expect(ctx.permission.current(session.events)).toBe('danger-full-access') }) it('a knob state matching no table entry derives custom — a state, not an error', async () => { const ctx = await mounted() const session = freshSession('sess-custom') session.append('sandbox/mode', { mode: 'read-only' }) expect(ctx.permission.current(session.events)).toBe(CUSTOM_PRESET) ctx.permission.set(session, 'danger-full-access') expect(ctx.permission.current(session.events)).toBe('danger-full-access') expect(() => ctx.permission.resolve(CUSTOM_PRESET)).toThrow(/unknown preset/) }) it('composition defaults outside the table still derive custom when an explicit new-session default is configured', async () => { const ctx = await mounted({ approvalDefault: 'never', config: { defaultPreset: 'workspace-write' }, }) const session = freshSession('sess-defaults-custom') expect(ctx.permission.current(session.events)).toBe(CUSTOM_PRESET) }) it('the fold breaks bundle ties; a stale fold no longer matching falls back to table order', async () => { const ctx = await mounted({ config: { presets: { 'workspace-write': { sandbox: 'workspace-write', approval: 'ask' }, agentish: { sandbox: 'workspace-write', approval: 'ask' }, 'danger-full-access': { sandbox: 'danger-full-access', approval: 'never' }, } } }) const session = freshSession('sess-tie') ctx.permission.set(session, 'agentish') expect(ctx.permission.current(session.events)).toBe('agentish') session.append('approval/policy', { policy: 'never' }) session.append('sandbox/mode', { mode: 'danger-full-access' }) expect(ctx.permission.current(session.events)).toBe('danger-full-access') }) it('set() writes through: one preset event plus both knob events', async () => { const ctx = await mounted() const session = freshSession('sess-set') ctx.permission.set(session, 'danger-full-access') expect(session.events.map(e => [e.type, e.data])).toEqual([ ['permission/preset', { preset: 'danger-full-access' }], ['sandbox/mode', { mode: 'danger-full-access' }], ['approval/policy', { policy: 'never' }], ]) }) it('set() to the current preset is a no-op when the knobs already match (clicks are not switches)', async () => { const ctx = await mounted() const session = freshSession('sess-noop') ctx.permission.set(session, 'workspace-write') expect(session.events).toHaveLength(0) }) it('re-asserting a preset from a drifted (custom) state re-records the choice and repairs the knob', async () => { const ctx = await mounted() const session = freshSession('sess-drift') ctx.permission.set(session, 'danger-full-access') // Re-selecting from a drifted state records the choice and repairs only // the changed knob. session.append('sandbox/mode', { mode: 'read-only' }) ctx.permission.set(session, 'danger-full-access') const tail = session.events.slice(4) expect(tail.map(e => [e.type, e.data])).toEqual([ ['permission/preset', { preset: 'danger-full-access' }], ['sandbox/mode', { mode: 'danger-full-access' }], ]) }) it('rejects composition over a non-confining executor at load', async () => { await expect(mounted({ bashDefault: undefined })) .rejects.toThrow(/does not confine/) }) it('optionOf() presents shipped labels/descriptions, falls back to the raw key, and fixes custom', async () => { const ctx = await mounted() expect(ctx.permission.optionOf('danger-full-access')).toEqual({ value: 'danger-full-access', name: 'danger-full-access', description: 'Full file access without approval prompts.' }) expect(ctx.permission.optionOf('custom')).toEqual({ value: 'custom', name: 'Custom', description: 'Current sandbox and approval settings do not match a preset.' }) const bare = await mounted({ config: { presets: { plain: { sandbox: 'workspace-write', approval: 'ask' } } } }) expect(bare.permission.optionOf('plain')).toEqual({ value: 'plain', name: 'plain' }) expect(() => ctx.permission.optionOf('plan')).toThrow(/unknown preset/) }) it('rejects a table entry named custom (reserved for the derived state)', async () => { await expect(mounted({ config: { presets: { custom: { sandbox: 'read-only', approval: 'ask' } } } })) .rejects.toThrow(/reserved for the derived not-a-preset state/) }) it('requires an explicit default when composition defaults match no preset', async () => { await expect(mounted({ approvalDefault: 'never' })) .rejects.toThrow(/configure defaultPreset explicitly/) }) it('reads a schema-less approval stand-in as the ask default', async () => { const ctx = await mounted({ approvalDefault: undefined }) const session = freshSession('sess-standin') ctx.permission.set(session, 'workspace-write') expect(session.events).toHaveLength(0) expect(ctx.permission.current(session.events)).toBe('workspace-write') }) }) describe('new-session default', () => { it('pins the current setting into each new session without changing earlier sessions', async () => { const ctx = await mountedStore() const first = ctx.sessions.create(SessionId('first')) expect(first.events.map(event => [event.type, event.data])).toEqual([ ['permission/preset', { preset: 'workspace-write' }], ['sandbox/mode', { mode: 'workspace-write' }], ['approval/policy', { policy: 'ask' }], ]) await ctx.settings.update(PERMISSION_SETTINGS_NAMESPACE, { defaultPreset: 'danger-full-access', }) expect(ctx.permission.defaultPreset).toBe('danger-full-access') const second = ctx.sessions.create(SessionId('second')) expect(ctx.permission.current(first.events)).toBe('workspace-write') expect(ctx.permission.current(second.events)).toBe('danger-full-access') expect(second.events.map(event => event.type)).toEqual([ 'permission/preset', 'sandbox/mode', 'approval/policy', ]) }) it('preserves a seeded legacy session instead of applying the latest user default', async () => { const ctx = await mountedStore() await ctx.settings.update(PERMISSION_SETTINGS_NAMESPACE, { defaultPreset: 'danger-full-access', }) const legacy = freshSession('legacy-source') legacy.append('turn/start', { turn: 1 }) legacy.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) const resumed = ctx.sessions.create(SessionId('legacy-resumed'), { seed: legacy.events }) expect(ctx.permission.current(resumed.events)).toBe('workspace-write') expect(resumed.events.slice(-3).map(event => event.type)).toEqual([ 'permission/preset', 'sandbox/mode', 'approval/policy', ]) }) it('preserves composition defaults when an empty stored session resumes', async () => { const ctx = await mountedStore() await ctx.settings.update(PERMISSION_SETTINGS_NAMESPACE, { defaultPreset: 'danger-full-access', }) const resumed = ctx.sessions.create(SessionId('empty-resumed'), { seed: [] }) expect(ctx.permission.current(resumed.events)).toBe('workspace-write') expect(resumed.events.map(event => event.type)).toEqual([ 'session/end-seed', 'permission/preset', 'sandbox/mode', 'approval/policy', ]) }) it('pins sessions that already exist when the service remounts', async () => { const ctx = new Context() await ctx.plugin(SessionStore) ctx.provide('bash', { sandboxMode: 'workspace-write', resolve() { throw new Error('permission tests do not execute bash') }, run() { throw new Error('permission tests do not execute bash') }, start() { throw new Error('permission tests do not execute bash') }, }) ctx.provide('approval', { config: { policy: 'ask' } }) const existing = ctx.sessions.create(SessionId('existing-before-permission')) expect(existing.events).toEqual([]) await ctx.plugin(PermissionService, {}) expect(existing.events.map(event => event.type)).toEqual([ 'permission/preset', 'sandbox/mode', 'approval/policy', ]) expect(ctx.permission.current(existing.events)).toBe('workspace-write') }) it('fills only missing legacy facts and preserves an unmatched seeded combination', async () => { const ctx = await mountedStore() const partial = freshSession('partial-source') partial.append('sandbox/mode', { mode: 'workspace-write' }) partial.append('approval/policy', { policy: 'ask' }) const resumed = ctx.sessions.create(SessionId('partial-resumed'), { seed: partial.events }) expect(resumed.events.at(-1)).toMatchObject({ type: 'permission/preset', data: { preset: 'workspace-write' }, }) const custom = freshSession('custom-source') custom.append('sandbox/mode', { mode: 'read-only' }) custom.append('approval/policy', { policy: 'never' }) const unmatched = ctx.sessions.create(SessionId('custom-resumed'), { seed: custom.events }) expect(ctx.permission.current(unmatched.events)).toBe(CUSTOM_PRESET) expect(unmatched.events.at(-1)?.type).toBe('session/end-seed') }) it('materializes ask when a legacy seed and approval stand-in omit the policy', async () => { const ctx = await mountedStore({ approvalDefault: undefined }) const partial = freshSession('approval-fallback-source') partial.append('sandbox/mode', { mode: 'workspace-write' }) const resumed = ctx.sessions.create(SessionId('approval-fallback-resumed'), { seed: partial.events }) expect(resumed.events.at(-1)).toMatchObject({ type: 'approval/policy', data: { policy: 'ask' }, }) }) it('rejects a stored default outside the configured preset table', async () => { const ctx = await mountedStore() await expect(ctx.settings.update(PERMISSION_SETTINGS_NAMESPACE, { defaultPreset: 'missing', })).rejects.toThrow() expect(ctx.permission.defaultPreset).toBe('workspace-write') }) })