import { homedir } from 'node:os' import { join, resolve } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import { Context } from 'cordis' import { CallId } from '@deepseek-ai/dsh-llm' import type { Agent } from '@deepseek-ai/dsh-agent' import type { ToolExecution } from '@deepseek-ai/dsh-tools' import { BashEnvRegistry } from '@deepseek-ai/dsh-tool-bash' afterEach(() => vi.unstubAllEnvs()) function execution(sessionId?: string): ToolExecution { return { token: Symbol('bash-env-test') as ToolExecution['token'], callId: CallId('bash-env-call'), name: 'bash', arguments: { command: 'true' }, ...(sessionId === undefined ? {} : { agent: { session: { header: { version: 0, id: sessionId, createdAt: 0 } } } as Agent }), } } describe('BashEnvRegistry', () => { it('collects unconditional shell facts and the current agent session id', () => { const ctx = new Context() const registry = new BashEnvRegistry(ctx, { dshHome: './test-dsh-home' }) expect(registry.collect(execution())).toEqual({ DSH_HOME: resolve('./test-dsh-home'), DSH_SHELL: '1', }) expect(registry.collect(execution('session-a'))).toEqual({ DSH_HOME: resolve('./test-dsh-home'), DSH_SESSION_ID: 'session-a', DSH_SHELL: '1', }) }) it('resolves DSH_HOME from the ambient override or the user-home default', () => { vi.stubEnv('DSH_HOME', './ambient-dsh-home') const fromEnvironment = new BashEnvRegistry(new Context()) expect(fromEnvironment.collect(execution()).DSH_HOME).toBe(resolve('./ambient-dsh-home')) vi.stubEnv('DSH_HOME', undefined) const fromDefault = new BashEnvRegistry(new Context()) expect(fromDefault.collect(execution()).DSH_HOME).toBe(join(homedir(), '.dsh')) }) it('collects declared contributor variables and omits unavailable values', () => { const ctx = new Context() const registry = new BashEnvRegistry(ctx, { dshHome: './test-dsh-home' }) registry.register({ name: 'optional-session-fact', variables: { DSH_SESSION_OPTIONAL: { description: 'Optional session-scoped test fact.' }, }, resolve: exec => exec.agent === undefined ? {} : { DSH_SESSION_OPTIONAL: exec.agent.session.header.id }, }) registry.register({ name: 'always-available-fact', variables: { DSH_ALWAYS_AVAILABLE: { description: 'Always-available test fact.' }, }, resolve: () => ({ DSH_ALWAYS_AVAILABLE: 'yes' }), }) expect(registry.collect(execution())).not.toHaveProperty('DSH_SESSION_OPTIONAL') expect(registry.collect(execution()).DSH_ALWAYS_AVAILABLE).toBe('yes') expect(registry.collect(execution('session-b')).DSH_SESSION_OPTIONAL).toBe('session-b') expect(registry.list()).toEqual([ { contributor: 'always-available-fact', description: 'Always-available test fact.', key: 'DSH_ALWAYS_AVAILABLE', }, { contributor: 'optional-session-fact', description: 'Optional session-scoped test fact.', key: 'DSH_SESSION_OPTIONAL', }, ]) }) it('rejects duplicate variable ownership at registration time', () => { const ctx = new Context() const registry = new BashEnvRegistry(ctx, { dshHome: './test-dsh-home' }) registry.register({ name: 'first', variables: { DSH_SHARED: { description: 'First owner.' } }, resolve: () => ({ DSH_SHARED: 'first' }), }) expect(() => registry.register({ name: 'second', variables: { DSH_SHARED: { description: 'Second owner.' } }, resolve: () => ({ DSH_SHARED: 'second' }), })).toThrow(/DSH_SHARED.*first.*second|DSH_SHARED.*second.*first/) }) it('rejects duplicate contributor names and malformed declarations', () => { const registry = new BashEnvRegistry(new Context(), { dshHome: './test-dsh-home' }) registry.register({ name: 'declared', variables: { DSH_DECLARED: { description: 'Declared fact.' } }, resolve: () => ({}), }) expect(() => registry.register({ name: 'declared', variables: { DSH_ANOTHER: { description: 'Another fact.' } }, resolve: () => ({}), })).toThrow(/already registered/) expect(() => registry.register({ name: ' ', variables: { DSH_BLANK_NAME: { description: 'Blank owner.' } }, resolve: () => ({}), })).toThrow(/name must be non-empty/) expect(() => registry.register({ name: 'invalid-key', variables: { dsh_invalid: { description: 'Invalid key.' } } as unknown as Record<'DSH_INVALID', { description: string }>, resolve: () => ({}), })).toThrow(/invalid key/) expect(() => registry.register({ name: 'reserved-key', variables: { DSH_HOME: { description: 'Reserved key.' } }, resolve: () => ({}), })).toThrow(/reserved key/) expect(() => registry.register({ name: 'blank-description', variables: { DSH_BLANK_DESCRIPTION: { description: ' ' } }, resolve: () => ({}), })).toThrow(/must describe/) }) it('rejects undeclared variables returned by a contributor', () => { const ctx = new Context() const registry = new BashEnvRegistry(ctx, { dshHome: './test-dsh-home' }) registry.register({ name: 'drifted-provider', variables: { DSH_DECLARED: { description: 'Declared fact.' } }, resolve: () => ({ DSH_UNDECLARED: 'bad' }), }) expect(() => registry.collect(execution())).toThrow(/drifted-provider.*DSH_UNDECLARED/) }) it('rejects non-string values returned by a contributor', () => { const registry = new BashEnvRegistry(new Context(), { dshHome: './test-dsh-home' }) registry.register({ name: 'wrong-value-type', variables: { DSH_STRING: { description: 'String fact.' } }, resolve: () => ({ DSH_STRING: 42 }) as unknown as Record<'DSH_STRING', string>, }) expect(() => registry.collect(execution())).toThrow(/wrong-value-type.*non-string.*DSH_STRING/) }) it('removes an effect-scoped contributor when its plugin is disposed', async () => { const ctx = new Context() const registry = new BashEnvRegistry(ctx, { dshHome: './test-dsh-home' }) const fiber = await ctx.plugin({ inject: ['bashEnv'], apply(inner: Context) { inner.bashEnv.register({ name: 'temporary', variables: { DSH_TEMPORARY: { description: 'Temporary fact.' } }, resolve: () => ({ DSH_TEMPORARY: 'present' }), }) }, }) expect(registry.collect(execution()).DSH_TEMPORARY).toBe('present') await fiber.dispose() expect(registry.collect(execution())).not.toHaveProperty('DSH_TEMPORARY') }) it('returns an explicit contributor disposer', () => { const registry = new BashEnvRegistry(new Context(), { dshHome: './test-dsh-home' }) const dispose = registry.register({ name: 'explicit-disposal', variables: { DSH_EXPLICIT_DISPOSAL: { description: 'Explicitly disposed fact.' } }, resolve: () => ({ DSH_EXPLICIT_DISPOSAL: 'present' }), }) expect(registry.collect(execution()).DSH_EXPLICIT_DISPOSAL).toBe('present') dispose() expect(registry.collect(execution())).not.toHaveProperty('DSH_EXPLICIT_DISPOSAL') }) })