refactor: apply repository naming contract

Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
Tianyi Cui
2026-08-13 00:36:22 +08:00
parent 101df7cf58
commit a2d0f7f411
3281 changed files with 21730 additions and 21592 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/shell/shell-env/README.md
README.md: b056289bd58e960e9fd36ee84ed20579e7bde42e
README.zh.md: 3ced6d63525db8a15f9202311c9ca9e32335cab8

View File

@@ -0,0 +1,51 @@
# @deepseek-ai/dsh-shell-env
English | [中文](README.zh.md)
The tool-independent shell environment plugin: owns the `ctx.shellEnv` registry of trusted, per-execution `DSH_*` variables that the model-facing shell tools (`dsh-tool-bash`, `dsh-tool-pwsh`) collect into every shell call's environment. Built-in shell facts (`DSH_HOME`, `DSH_SHELL=1`, `DSH_SESSION_ID`) are owned by the registry itself; other plugins register additional enumerable facts with effect-scoped disposal, and duplicate ownership or undeclared runtime keys fail loudly.
The package root exports the Cordis plugin contract (`name`, `inject`, `Config`, `apply`) plus the `ShellEnvRegistry` service class and its contributor types; consumers use `ctx.shellEnv` after loading this plugin.
## Config
```yaml
- id: shell-env
name: '@deepseek-ai/dsh-shell-env'
config:
dshHome: C:\Users\me\.dsh # default: $DSH_HOME, then ~/.dsh
```
## Managed environment
Every foreground and background model shell call receives a newly collected trusted `DSH_*` environment. `DSH_HOME` is the absolute Harness home resolved by [`@deepseek-ai/dsh-home-paths`](../../util/home-paths/README.md) (`dshHome` config, then ambient `$DSH_HOME`, then `~/.dsh`) and `DSH_SHELL=1` identifies the managed child. Agent calls additionally receive `DSH_SESSION_ID=agent.session.header.id`; when the active persistence seam locates a JSONL artifact they also receive `DSH_SESSION_JSONL=<absolute target path>`. The JSONL path is a location hint: it may not exist before the first flush or contain the current buffered turn, and it is not an authorization credential.
`ctx.shellEnv` owns collection. Other plugins can register an effect-scoped contributor with a stable name, declared keys/descriptions, and `resolve(execution: ToolExecution)`; duplicate ownership and undeclared runtime keys fail loudly, while `list()` enumerates declarations without executing providers. Harness built-ins reserve `DSH_HOME`, `DSH_SHELL`, and `DSH_SESSION_ID`; this plugin's persistence translator owns `DSH_SESSION_JSONL` by reading the backend-neutral `sessionPersistence.locate()` seam.
```ts
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-shell-env'
export const inject = ['shellEnv']
export function apply(ctx: Context): void {
ctx.shellEnv.register({
name: 'deployment-region',
variables: { DSH_DEPLOYMENT_REGION: { description: 'Current deployment region.' } },
resolve: execution => execution.agent === undefined ? {} : { DSH_DEPLOYMENT_REGION: 'cn-north' },
})
}
```
The overlay is computed from the current `ToolExecution` and passed through the dedicated `ShellExecRequest.dshEnv` channel. The local executors remove all inherited `DSH_*` before merging that snapshot, so nested harnesses and concurrent parent/child agents cannot leak stale identities. `process.env` is never modified. The shell tools' descriptions teach the generic `$DSH_*` convention rather than naming persistence-specific variables or adding a permanent system-prompt section.
## Model Experience
Indirectly, through the shell tools (`dsh-tool-bash`, `dsh-tool-pwsh`), which collect this registry's managed `DSH_*` snapshot into every shell-tool call.
#### KV Cache effect
No direct invalidation; the named consumers own any request-prefix changes.
## Known Limitations and Deferred Work
- **`list()` enumerates contributor-declared variables only** — registry-owned built-ins (`DSH_HOME`, `DSH_SHELL`, `DSH_SESSION_ID`) are not included, so diagnostics, prompt, or UI code must not treat `list()` as an exhaustive environment catalog.

View File

@@ -0,0 +1,51 @@
# @deepseek-ai/dsh-shell-env
[English](README.md) | 中文
工具无关的 shell 环境插件:拥有 `ctx.shellEnv` 注册表,管理受信任的、每次执行收集的 `DSH_*` 变量,供模型可见的 shell 工具(`dsh-tool-bash``dsh-tool-pwsh`)收集进每次 shell 调用的环境。内置 shell 事实(`DSH_HOME``DSH_SHELL=1``DSH_SESSION_ID`归注册表自身所有其他插件可以注册额外的可枚举事实注册随插件纤维fiber释放重复所有权或未声明的运行时键会响亮失败。
包根导出 Cordis 插件约定(`name``inject``Config``apply`)以及 `ShellEnvRegistry` 服务类及其 contributor 类型;消费方在加载本插件后使用 `ctx.shellEnv`
## Config
```yaml
- id: shell-env
name: '@deepseek-ai/dsh-shell-env'
config:
dshHome: C:\Users\me\.dsh # default: $DSH_HOME, then ~/.dsh
```
## Managed environment
每次前台与后台模型 shell 调用都会收到一份新收集的受信任 `DSH_*` 环境。`DSH_HOME` 是由 [`@deepseek-ai/dsh-home-paths`](../../util/home-paths/README.md) 解析的 Harness 主目录绝对路径(`dshHome` 配置,然后环境变量 `$DSH_HOME`,然后 `~/.dsh``DSH_SHELL=1` 标识受管理的子进程。带 agent智能体的调用额外收到 `DSH_SESSION_ID=agent.session.header.id`;当活动的持久化 seam 定位到 JSONL 工件时,它们还会收到 `DSH_SESSION_JSONL=<绝对目标路径>`。JSONL 路径只是位置提示:首次 flush 之前它可能不存在,也不一定包含当前缓冲中的轮次,并且它不是授权凭据。
`ctx.shellEnv` 负责收集。其他插件可以注册一个受 effect 作用域约束的 contributor带有稳定名称、已声明的键/描述以及 `resolve(execution: ToolExecution)`;重复所有权与未声明的运行时键会响亮失败,而 `list()` 只枚举声明、不执行 provider。Harness 内置键保留 `DSH_HOME``DSH_SHELL``DSH_SESSION_ID`;本插件的持久化翻译器通过读取与后端无关的 `sessionPersistence.locate()` seam 拥有 `DSH_SESSION_JSONL`
```ts
import type { Context } from '@deepseek-ai/cordis'
import type {} from '@deepseek-ai/dsh-shell-env'
export const inject = ['shellEnv']
export function apply(ctx: Context): void {
ctx.shellEnv.register({
name: 'deployment-region',
variables: { DSH_DEPLOYMENT_REGION: { description: 'Current deployment region.' } },
resolve: execution => execution.agent === undefined ? {} : { DSH_DEPLOYMENT_REGION: 'cn-north' },
})
}
```
覆盖层根据当前 `ToolExecution` 计算,并通过专用的 `ShellExecRequest.dshEnv` 通道传递。本地执行器在合并该快照前移除所有继承的 `DSH_*`,因此嵌套 harness 与并发的父子 agent 无法泄漏陈旧身份。`process.env` 永不被修改。shell 工具的描述只教授通用的 `$DSH_*` 约定,而不是点名持久化相关的变量或添加常驻的 system-prompt 段落。
## Model Experience
通过 shell 工具(`dsh-tool-bash``dsh-tool-pwsh`)间接产生影响;这些工具会把该注册表的受管 `DSH_*` 快照收集进每次 shell 工具调用。
#### KV Cache effect
不会直接导致缓存失效;任何请求前缀变更均由上述消费方负责。
## Known Limitations and Deferred Work
- **`list()` 只枚举 contributor 声明的变量** — 注册表自有的内置键(`DSH_HOME``DSH_SHELL``DSH_SESSION_ID`不包含在内因此诊断、prompt 或 UI 代码不得把 `list()` 当作完整的环境目录。

View File

@@ -0,0 +1,55 @@
{
"name": "@deepseek-ai/dsh-shell-env",
"description": "Tool-independent managed DSH_* shell environment registry",
"version": "0.0.1-rc.2",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/shell/shell-env"
},
"type": "module",
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.d.ts"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-shell": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-home-paths": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"dependencies": {
"@deepseek-ai/schemastery": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-agent": "workspace:^",
"@deepseek-ai/dsh-shell": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-home-paths": "workspace:^",
"@deepseek-ai/dsh-session-persistence": "workspace:^",
"@deepseek-ai/dsh-tools": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -0,0 +1,217 @@
/**
* Tool-independent shell environment plugin: owns the `ctx.shellEnv` registry of
* trusted, per-execution `DSH_*` variables consumed by the model-facing shell
* tools (`dsh-tool-bash`, `dsh-tool-pwsh`). Built-in shell facts are owned by
* the registry itself while plugins can register additional, enumerable facts
* with effect-scoped disposal.
*
* @module @deepseek-ai/dsh-shell-env
*/
import { Service, type Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { DSH_ENV_PREFIX } from '@deepseek-ai/dsh-shell'
import type { DshEnvironment, DshEnvironmentKey } from '@deepseek-ai/dsh-shell'
import { DSH_HOME_ENV, resolveDshHome } from '@deepseek-ai/dsh-home-paths'
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
import type {} from '@deepseek-ai/dsh-session-persistence'
declare module '@deepseek-ai/cordis' {
interface Context {
shellEnv: ShellEnvRegistry
}
}
export const name = 'shell-env'
export const inject: string[] = []
/** Plugin config (all optional — the built-in facts resolve without defaults). */
export interface Config {
/** DeepSeek Harness home directory exposed as `DSH_HOME`; defaults to `$DSH_HOME` or `~/.dsh`. */
dshHome?: string
}
/** Runtime configuration schema for the shell-env plugin. */
export const Config: z<Config> = z.object({
dshHome: z.string(),
})
/** Model-visible metadata for one managed `DSH_*` environment variable. */
export interface BashEnvVariable {
/** Concise description of the environment fact represented by the variable. */
description: string
}
/**
* A plugin contribution to the managed environment of each model shell call.
* Declared keys make ownership conflicts detectable before the first command;
* `resolve` computes only the values available for the current execution.
*/
export interface BashEnvContributor {
/** Stable contributor name used in diagnostics and duplicate detection. */
name: string
/** Complete set of `DSH_*` keys this contributor may return. */
variables: Readonly<Record<DshEnvironmentKey, BashEnvVariable>>
/**
* Resolve this contributor's available values for one tool execution.
* @param execution - the shell tool execution and its optional calling agent.
* @returns a partial map containing only keys declared in {@link variables}.
*/
resolve(execution: ToolExecution): Readonly<Partial<Record<DshEnvironmentKey, string>>>
}
/** An enumerable declaration returned by {@link ShellEnvRegistry.list}. */
export interface BashEnvVariableInfo extends BashEnvVariable {
/** Contributor that owns the variable. */
contributor: string
/** Declared `DSH_*` environment variable name. */
key: DshEnvironmentKey
}
const DSH_SHELL_KEY = `${DSH_ENV_PREFIX}SHELL` as const
const DSH_SESSION_ID_KEY = `${DSH_ENV_PREFIX}SESSION_ID` as const
const DSH_SESSION_JSONL_KEY = `${DSH_ENV_PREFIX}SESSION_JSONL` as const
const RESERVED_BASH_ENV_KEYS = new Set<DshEnvironmentKey>([
DSH_HOME_ENV,
DSH_SHELL_KEY,
DSH_SESSION_ID_KEY,
])
const BASH_ENV_KEY_SUFFIX = /^[A-Z][A-Z0-9_]*$/
/**
* Registry (`ctx.shellEnv`) for trusted, per-execution `DSH_*` variables.
* The namespace is rebuilt for every model shell call: ambient `DSH_*` values
* are discarded by the executor, then the registry's current snapshot is
* injected. Built-in shell facts remain owned by the registry itself while
* plugins can register additional, enumerable facts with effect-scoped
* disposal.
*/
export class ShellEnvRegistry extends Service {
private readonly contributors = new Map<string, BashEnvContributor>()
private readonly keyOwners = new Map<DshEnvironmentKey, string>()
private readonly dshHome: string
/**
* Create and install the `ctx.shellEnv` service.
* @param ctx - Cordis context that owns the service and registrations.
* @param config - home-directory configuration for the built-in variables.
*/
constructor(ctx: Context, config: Config = {}) {
super(ctx, 'shellEnv')
this.dshHome = resolveDshHome(config.dshHome)
}
/**
* Register one environment contributor. Names and keys are unique; built-in
* keys are reserved. Registration is disposed with the calling plugin fiber.
* @param contributor - declared key ownership and per-execution resolver.
* @returns the disposer that unregisters the contribution.
*/
register(contributor: BashEnvContributor): () => void {
const dispose = this.ctx.effect(function* (this: ShellEnvRegistry) {
if (contributor.name.trim().length === 0) {
throw new Error('bash env contributor name must be non-empty')
}
if (this.contributors.has(contributor.name)) {
throw new Error(`bash env contributor "${contributor.name}" is already registered`)
}
const variables = Object.entries(contributor.variables) as [DshEnvironmentKey, BashEnvVariable][]
for (const [key, variable] of variables) {
if (!key.startsWith(DSH_ENV_PREFIX)
|| !BASH_ENV_KEY_SUFFIX.test(key.slice(DSH_ENV_PREFIX.length))) {
throw new Error(`bash env contributor "${contributor.name}" declared invalid key "${key}"`)
}
if (RESERVED_BASH_ENV_KEYS.has(key)) {
throw new Error(`bash env contributor "${contributor.name}" cannot own reserved key "${key}"`)
}
if (variable.description.trim().length === 0) {
throw new Error(`bash env contributor "${contributor.name}" must describe "${key}"`)
}
const owner = this.keyOwners.get(key)
if (owner !== undefined) {
throw new Error(`bash env key "${key}" is already owned by contributor "${owner}"; contributor "${contributor.name}" cannot also own it`)
}
}
this.contributors.set(contributor.name, contributor)
for (const [key] of variables) this.keyOwners.set(key, contributor.name)
yield () => {
this.contributors.delete(contributor.name)
for (const [key] of variables) this.keyOwners.delete(key)
}
}.bind(this), 'bashEnv.register()')
return () => void dispose()
}
/**
* Build the trusted `DSH_*` snapshot for one shell tool execution.
* @param execution - the current tool execution.
* @returns an immutable environment overlay containing built-ins and current contributions.
*/
collect(execution: ToolExecution): DshEnvironment {
const values: Record<DshEnvironmentKey, string> = {
[DSH_HOME_ENV]: this.dshHome,
[DSH_SHELL_KEY]: '1',
}
if (execution.agent !== undefined) {
values[DSH_SESSION_ID_KEY] = execution.agent.session.header.id
}
for (const contributor of [...this.contributors.values()].sort((left, right) => left.name.localeCompare(right.name))) {
const resolved = contributor.resolve(execution)
for (const [rawKey, value] of Object.entries(resolved)) {
const key = rawKey as DshEnvironmentKey
if (!Object.hasOwn(contributor.variables, key)) {
throw new Error(`bash env contributor "${contributor.name}" returned undeclared key "${key}"`)
}
if (typeof value !== 'string') {
throw new Error(`bash env contributor "${contributor.name}" returned a non-string value for "${key}"`)
}
values[key] = value
}
}
return Object.freeze(Object.fromEntries(Object.entries(values).sort(([left], [right]) => left.localeCompare(right))))
}
// TODO(bash-env-list-builtins): Include registry-owned built-ins before diagnostics,
// prompt, or UI code treats list() as an exhaustive environment catalog.
/**
* Enumerate plugin-contributed variables without executing their resolvers.
* @returns declarations sorted by environment variable name.
*/
list(): BashEnvVariableInfo[] {
return [...this.contributors.values()]
.flatMap(contributor => Object.entries(contributor.variables).map(([key, variable]) => ({
contributor: contributor.name,
description: variable.description,
key: key as DshEnvironmentKey,
})))
.sort((left, right) => left.key.localeCompare(right.key))
}
}
/**
* Load the shell-env plugin: register the `ctx.shellEnv` service and the
* shell-agnostic persistence contributor (`DSH_SESSION_JSONL`).
* @param ctx - Cordis context that owns the service and registrations.
* @param config - home-directory configuration for the built-in variables.
*/
export function apply(ctx: Context, config: Config = {}): void {
const registry = new ShellEnvRegistry(ctx, config)
registry.register({
name: 'session-persistence',
variables: {
[DSH_SESSION_JSONL_KEY]: {
description: 'Absolute target path of the current session JSONL when the active persistence backend provides one.',
},
},
resolve(execution) {
const agent = execution.agent
if (agent === undefined) return {}
const location = ctx.get('sessionPersistence')?.locate(agent.session.header)
return location?.kind === 'jsonl' ? { [DSH_SESSION_JSONL_KEY]: location.path } : {}
},
})
}

View File

@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-shell-env`.
* @module @deepseek-ai/dsh-shell-env/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-shell-env'
/** Cordis companion plugin name. */
export const name = 'shell-env-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: the environment registry validates ownership and collected values at each
* registration/collection; it publishes no independent snapshot that a companion could cross-check.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */

View File

@@ -0,0 +1,238 @@
/**
* Registry tests for `@deepseek-ai/dsh-shell-env`: built-in facts, contributor
* ownership and validation, collection ordering, effect-scoped disposal, and
* the explicit disposer contract.
*/
import { homedir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/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 { ShellEnvRegistry } from '@deepseek-ai/dsh-shell-env'
import * as BashEnvPlugin from '@deepseek-ai/dsh-shell-env'
const testToolSignal = new AbortController().signal
afterEach(() => vi.unstubAllEnvs())
function execution(sessionId?: string): ToolExecution {
return {
signal: testToolSignal,
token: Symbol('bash-env-test') as ToolExecution['token'],
callId: CallId('bash-env-call'),
rootCallId: CallId('bash-env-call'),
name: 'bash',
arguments: { command: 'true' },
...(sessionId === undefined
? {}
: { agent: { session: { header: { version: 0, id: sessionId, createdAt: 0 } } } as Agent }),
}
}
describe('ShellEnvRegistry', () => {
it('collects unconditional shell facts and the current agent session id', () => {
const ctx = new Context()
const registry = new ShellEnvRegistry(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 ShellEnvRegistry(new Context())
expect(fromEnvironment.collect(execution()).DSH_HOME).toBe(resolve('./ambient-dsh-home'))
vi.stubEnv('DSH_HOME', undefined)
const fromDefault = new ShellEnvRegistry(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 ShellEnvRegistry(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 ShellEnvRegistry(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 ShellEnvRegistry(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 ShellEnvRegistry(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 ShellEnvRegistry(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 ShellEnvRegistry(ctx, { dshHome: './test-dsh-home' })
const fiber = await ctx.plugin({
inject: ['shellEnv'],
apply(inner: Context) {
inner.shellEnv.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 ShellEnvRegistry(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')
})
it('the plugin registers the service and the persistence contributor on load', async () => {
const ctx = new Context()
await ctx.plugin(BashEnvPlugin)
expect(ctx.shellEnv).toBeInstanceOf(ShellEnvRegistry)
expect(ctx.shellEnv.list()).toEqual([
{
contributor: 'session-persistence',
description: 'Absolute target path of the current session JSONL when the active persistence backend provides one.',
key: 'DSH_SESSION_JSONL',
},
])
})
it('the persistence contributor resolves DSH_SESSION_JSONL only for a jsonl backend', async () => {
const ctx = new Context()
await ctx.plugin(BashEnvPlugin)
ctx.provide('sessionPersistence', {
locate: () => ({ kind: 'jsonl' as const, path: 'C:\\sessions\\s.jsonl' }),
})
expect(ctx.shellEnv.collect(execution('sess-p')).DSH_SESSION_JSONL).toBe('C:\\sessions\\s.jsonl')
})
it('the persistence contributor omits the variable for a non-jsonl backend', async () => {
const ctx = new Context()
await ctx.plugin(BashEnvPlugin)
ctx.provide('sessionPersistence', {
locate: () => ({ kind: 'sqlite' as const, path: 'C:\\sessions\\s.db' }),
})
expect(ctx.shellEnv.collect(execution('sess-p'))).not.toHaveProperty('DSH_SESSION_JSONL')
})
it('the persistence contributor omits the variable without a persistence backend', async () => {
const ctx = new Context()
await ctx.plugin(BashEnvPlugin)
expect(ctx.shellEnv.collect(execution('sess-p'))).not.toHaveProperty('DSH_SESSION_JSONL')
})
})

View File

@@ -0,0 +1,36 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": [
"src"
],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../shell/shell"
},
{
"path": "../../util/home-paths"
},
{
"path": "../../core/tools"
},
{
"path": "../../session/session-persistence"
},
{
"path": "../../runtime-diagnostics/invariants"
}
]
}