@mzzsfy/dsh-shell-select 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ // config 设置 schema 与 argv 组装:BDD 场景见 docs/progress/shell-select-plan.md「模块 config」。
2
+ // schemastery schema 走真实官方包验证默认值与校验行为。
3
+
4
+ import { test } from 'node:test'
5
+ import assert from 'node:assert/strict'
6
+ import { Config, defaultConfig, entryById, requireEntry, buildArgv, KINDS, RESOLVED_AUTO } from '../src/config.mjs'
7
+
8
+ test('schema 应用出厂默认:三客户端 + 默认 pwsh', () => {
9
+ const applied = Config({})
10
+ assert.deepEqual(applied, defaultConfig())
11
+ assert.equal(applied.default, 'pwsh')
12
+ assert.deepEqual(applied.shells.map((entry) => entry.id), ['pwsh', 'git-bash', 'cmd'])
13
+ for (const entry of applied.shells) {
14
+ assert.equal(entry.path, '')
15
+ assert.ok(KINDS.includes(entry.kind))
16
+ }
17
+ })
18
+
19
+ test('schema 拒绝未知 kind;default 跨字段一致性由 validate hook 与 requireEntry 兜底', () => {
20
+ assert.throws(() => Config({ shells: [{ id: 'x', name: 'X', kind: 'fish', path: '' }], default: 'x' }))
21
+ })
22
+
23
+ test('pwsh argv:非交互形 + 编码前缀', () => {
24
+ const argv = buildArgv({ kind: 'pwsh', path: 'C:\\pf\\pwsh.exe' }, 'Get-Item .')
25
+ assert.deepEqual(argv, [
26
+ 'C:\\pf\\pwsh.exe',
27
+ '-NoLogo', '-NoProfile', '-NonInteractive', '-Command',
28
+ '[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false); $OutputEncoding = [System.Text.UTF8Encoding]::new($false); Get-Item .',
29
+ ])
30
+ })
31
+
32
+ test('bash argv:-c 直传', () => {
33
+ assert.deepEqual(buildArgv({ kind: 'bash', path: 'C:\\Git\\bin\\bash.exe' }, 'ls -la'), [
34
+ 'C:\\Git\\bin\\bash.exe', '-c', 'ls -la',
35
+ ])
36
+ })
37
+
38
+ test('bash argv:login 条目走 -lc 登录壳(profile 注入 PATH)', () => {
39
+ assert.deepEqual(buildArgv({ kind: 'bash', path: 'C:\\msys64\\usr\\bin\\bash.exe', login: true }, 'uname -a'), [
40
+ 'C:\\msys64\\usr\\bin\\bash.exe', '-lc', 'uname -a',
41
+ ])
42
+ })
43
+
44
+ test('bash argv:login=false 与缺省等价(-c)', () => {
45
+ assert.deepEqual(buildArgv({ kind: 'bash', path: 'C:\\b.exe', login: false }, 'x'), ['C:\\b.exe', '-c', 'x'])
46
+ })
47
+
48
+ test('login 显式配置与自定义 args 模板互斥时模板优先', () => {
49
+ assert.deepEqual(buildArgv({ kind: 'bash', path: 'C:\\b.exe', login: true, args: ['-x'] }, 'hi'), ['C:\\b.exe', '-x', 'hi'])
50
+ })
51
+
52
+ test('schema 往返:login 布尔保留,旧配置缺省落 false', () => {
53
+ const applied = Config({ shells: [
54
+ { id: 'm', name: 'MSYS2', kind: 'bash', path: 'C:\\msys64\\usr\\bin\\bash.exe', login: true },
55
+ { id: 'g', name: 'Git Bash', kind: 'bash', path: '' },
56
+ ], default: 'm' })
57
+ assert.equal(applied.shells[0].login, true)
58
+ assert.equal(applied.shells[1].login, false)
59
+ })
60
+
61
+ test('cmd argv:/d /s /c 忽略 AutoRun', () => {
62
+ assert.deepEqual(buildArgv({ kind: 'cmd', path: 'C:\\S32\\cmd.exe' }, 'dir'), [
63
+ 'C:\\S32\\cmd.exe', '/d', '/s', '/c', 'dir',
64
+ ])
65
+ })
66
+
67
+ test('wsl argv:--exec 绕过默认 shell', () => {
68
+ assert.deepEqual(buildArgv({ kind: 'wsl', path: 'C:\\S32\\wsl.exe' }, 'uname -a'), [
69
+ 'C:\\S32\\wsl.exe', '--exec', 'bash', '-c', 'uname -a',
70
+ ])
71
+ })
72
+
73
+ test('自定义 args 模板:{command} 占位替换', () => {
74
+ const argv = buildArgv({ kind: 'bash', path: 'C:\\x\\fish.exe', args: ['--login', '-c', '{command}'] }, 'echo hi')
75
+ assert.deepEqual(argv, ['C:\\x\\fish.exe', '--login', '-c', 'echo hi'])
76
+ })
77
+
78
+ test('自定义 args 模板:无占位则追加末项', () => {
79
+ const argv = buildArgv({ kind: 'bash', path: 'C:\\x\\sh.exe', args: ['-s'] }, 'echo hi')
80
+ assert.deepEqual(argv, ['C:\\x\\sh.exe', '-s', 'echo hi'])
81
+ })
82
+
83
+ test('entryById 按 id 取条目', () => {
84
+ const shells = [
85
+ { id: 'a', name: 'A', kind: 'bash', path: '' },
86
+ { id: 'b', name: 'B', kind: 'cmd', path: '' },
87
+ ]
88
+ assert.equal(entryById(shells, 'b'), shells[1])
89
+ assert.equal(entryById(shells, 'zz'), undefined)
90
+ })
91
+
92
+ test('requireEntry:缺省用 default,default 缺失报配置指引', () => {
93
+ const shells = [
94
+ { id: 'a', name: 'A', kind: 'bash', path: '' },
95
+ { id: 'b', name: 'B', kind: 'cmd', path: '' },
96
+ ]
97
+ assert.equal(requireEntry(shells, undefined, 'b'), shells[1])
98
+ assert.equal(requireEntry(shells, 'a', 'b'), shells[0])
99
+ assert.throws(() => requireEntry(shells, undefined, undefined), /default.*shell-select|shell-select.*default/is)
100
+ assert.throws(() => requireEntry(shells, 'zz', 'b'), /zz/)
101
+ })
102
+
103
+ test('kind 常量与自动解析标记', () => {
104
+ assert.deepEqual(KINDS, ['pwsh', 'bash', 'cmd', 'wsl'])
105
+ assert.equal(RESOLVED_AUTO, '')
106
+ })
@@ -0,0 +1,33 @@
1
+ // deny 顶层字段:schema 往返与 updateConfig 落盘回读。
2
+ // BDD 场景见 docs/progress/feat-shell-select-deny.md 场景 7。
3
+
4
+ import { test } from 'node:test'
5
+ import assert from 'node:assert/strict'
6
+ import { readFileSync } from 'node:fs'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { dirname, join } from 'node:path'
9
+ import { Config, defaultConfig } from '../src/config.mjs'
10
+
11
+ const here = dirname(fileURLToPath(import.meta.url))
12
+ const executorSource = readFileSync(join(here, '..', 'src', 'executor.mjs'), 'utf8')
13
+
14
+ test('schema 默认:deny 空数组,旧配置反序列化补默认', () => {
15
+ const config = defaultConfig()
16
+ assert.deepEqual(config.deny, [])
17
+ })
18
+
19
+ test('schema 往返:deny 保留;allow 无消费者(deny 绝对,无豁免语义)', () => {
20
+ const config = Config({ deny: ['format '], allow: ['git .*'] })
21
+ assert.deepEqual(config.deny, ['format '])
22
+ // schemastery 透传未知键,僵尸键靠"无读取方"守卫:matchDeny/toSection 均只触 deny
23
+ const denylistSource = readFileSync(join(here, '..', 'src', 'denylist.mjs'), 'utf8')
24
+ const clientSource = readFileSync(join(here, '..', 'src', 'client.js'), 'utf8')
25
+ assert.doesNotMatch(denylistSource, /allow/)
26
+ assert.doesNotMatch(clientSource, /allowText|section\.allow/)
27
+ assert.doesNotMatch(executorSource, /current\.allow|patch\.allow/)
28
+ })
29
+
30
+ test('updateConfig 携带 deny(wholesale replace 防静默重置,同 login 教训)', () => {
31
+ assert.match(executorSource, /deny: Array\.isArray\(patch\.deny\) \? patch\.deny : current\.deny/)
32
+ assert.doesNotMatch(executorSource, /patch\.allow/)
33
+ })
@@ -0,0 +1,26 @@
1
+ // tool.mjs DenyError → 模型可见 blocked 标记(场景 8):源级守卫
2
+ // (registerShellTool 工厂形态,node:test 下不便直拉 dsh-tools 全链)。
3
+
4
+ import { test } from 'node:test'
5
+ import assert from 'node:assert/strict'
6
+ import { readFileSync } from 'node:fs'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { dirname, join } from 'node:path'
9
+ import { DenyError } from '../src/denylist.mjs'
10
+
11
+ const here = dirname(fileURLToPath(import.meta.url))
12
+ const source = readFileSync(join(here, '..', 'src', 'tool.mjs'), 'utf8')
13
+
14
+ test('execute 路径 catch DenyError 返回 blocked 标记文本', () => {
15
+ // 前台与后台两分支各自触达 executor 入口,均须拦截转译
16
+ const catches = (source.match(/catch \(error\) \{[\s\S]*?isDenyError/g) ?? []).length
17
+ assert.ok(catches >= 2, `execute 前台/后台两分支应各自 catch DenyError(发现 ${catches} 处)`)
18
+ assert.match(source, /SHELL_COMMAND_BLOCKED/)
19
+ assert.match(source, /blocked by shell-select/)
20
+ })
21
+
22
+ test('DenyError 形态契约:code/name 可供跨层判定', () => {
23
+ const error = new DenyError('p', 'cmd')
24
+ assert.equal(error.code, 'SHELL_COMMAND_BLOCKED')
25
+ assert.equal(error.name, 'DenyError')
26
+ })
@@ -0,0 +1,39 @@
1
+ // matchDeny 纯函数:命令黑名单匹配(deny 绝对:命中即拒,无豁免语义)。
2
+ // BDD 场景见 docs/progress/feat-shell-select-deny.md 场景 1-4/6。
3
+
4
+ import { test } from 'node:test'
5
+ import assert from 'node:assert/strict'
6
+ import { matchDeny, DenyError } from '../src/denylist.mjs'
7
+
8
+ test('deny 命中:抛 DenyError,消息含命中的 pattern', () => {
9
+ const deny = ['format ']
10
+ assert.throws(() => matchDeny('format c: /q', deny), (error) => {
11
+ assert.ok(error instanceof DenyError)
12
+ assert.equal(error.code, 'SHELL_COMMAND_BLOCKED')
13
+ assert.ok(error.message.includes('format '))
14
+ return true
15
+ })
16
+ })
17
+
18
+ test('精细放行在 deny 模式内用前瞻表达:禁 rm -rf,放行清 node_modules', () => {
19
+ const deny = ['rm -rf\\s+(?!\\S*node_modules)']
20
+ assert.throws(() => matchDeny('rm -rf C:\\Windows', deny), DenyError)
21
+ assert.doesNotThrow(() => matchDeny('rm -rf ./node_modules', deny))
22
+ })
23
+
24
+ test('大小写不敏感:DISKPART 拒绝 diskpart', () => {
25
+ assert.throws(() => matchDeny('diskpart', ['DISKPART']), DenyError)
26
+ })
27
+
28
+ test('空配置不拦:deny 空数组/undefined 照常放行', () => {
29
+ assert.doesNotThrow(() => matchDeny('git status', []))
30
+ assert.doesNotThrow(() => matchDeny('git status', undefined))
31
+ })
32
+
33
+ test('坏正则容错:非法条目跳过,不瘫执行链', () => {
34
+ assert.doesNotThrow(() => matchDeny('anything', ['[bad', '(bad', 'ok']))
35
+ })
36
+
37
+ test('多行命令整文本匹配:第一行干净也挡住后续行命中', () => {
38
+ assert.throws(() => matchDeny('echo start\nformat c:', ['format ']), DenyError)
39
+ })
@@ -0,0 +1,81 @@
1
+ // buildClientEnv:spawn env 构造纯函数。BDD 场景见 docs/feat-shell-select-optim/plan.md S1-S4。
2
+ // 三层并集:内置覆盖集 < 条目 env(用户客户端配置,如 MSYSTEM) < 调用方 env(spec.env+dshEnv)。
3
+ // wsl 追加键 = 条目与调用方全部键(WSLENV 本身除外);调用方显式 WSLENV 优先于继承值。
4
+
5
+ import { test } from 'node:test'
6
+ import assert from 'node:assert/strict'
7
+ import { buildClientEnv, ENV_OVERRIDES } from '../src/executor.mjs'
8
+
9
+ test('pwsh 形:DSH_* 直接并入,不动 WSLENV', () => {
10
+ const env = buildClientEnv('pwsh', { DSH_WORKSPACE: 'C:\\w' })
11
+ assert.equal(env.DSH_WORKSPACE, 'C:\\w')
12
+ assert.equal(env.WSLENV, undefined)
13
+ assert.equal(env.NO_COLOR, '1')
14
+ })
15
+
16
+ test('S1 bash 条目 env 并存:MSYSTEM 注入且内置覆盖集仍在', () => {
17
+ const env = buildClientEnv('bash', undefined, { MSYSTEM: 'MINGW64' })
18
+ assert.equal(env.MSYSTEM, 'MINGW64')
19
+ assert.equal(env.NO_COLOR, '1')
20
+ assert.equal(env.PAGER, 'cat')
21
+ })
22
+
23
+ test('S2 三层优先级:条目压内置,调用方压条目', () => {
24
+ const overridden = buildClientEnv('pwsh', undefined, { NO_COLOR: '0' })
25
+ assert.equal(overridden.NO_COLOR, '0')
26
+ const callerWins = buildClientEnv('pwsh', { NO_COLOR: '2' }, { NO_COLOR: '0' })
27
+ assert.equal(callerWins.NO_COLOR, '2')
28
+ })
29
+
30
+ test('S3 wsl 条目 env 键追加进 WSLENV,与 DSH_* 键并存', () => {
31
+ const env = buildClientEnv('wsl', { DSH_X: '1' }, { MSYSTEM: 'MINGW64' }, { inheritedWslenv: 'WT_SESSION:' })
32
+ assert.equal(env.WSLENV, 'WT_SESSION:MSYSTEM:DSH_X')
33
+ })
34
+
35
+ test('S4 非 wsl 形条目 env 不触发 WSLENV', () => {
36
+ const env = buildClientEnv('bash', undefined, { MSYSTEM: 'MINGW64' }, { inheritedWslenv: 'WT_SESSION:' })
37
+ assert.equal(env.WSLENV, undefined)
38
+ })
39
+
40
+ test('wsl 形:DSH_* 键追加进 WSLENV,继承条目保留,无空段', () => {
41
+ const env = buildClientEnv('wsl', { DSH_WORKSPACE: 'C:\\w' }, undefined, { inheritedWslenv: 'WT_SESSION:WT_PROFILE_ID:' })
42
+ assert.equal(env.DSH_WORKSPACE, 'C:\\w')
43
+ assert.equal(env.WSLENV, 'WT_SESSION:WT_PROFILE_ID:DSH_WORKSPACE')
44
+ })
45
+
46
+ test('wsl 形:无追加键时 WSLENV 原样继承', () => {
47
+ const env = buildClientEnv('wsl', undefined, undefined, { inheritedWslenv: 'WT_SESSION:' })
48
+ assert.equal(env.WSLENV, 'WT_SESSION:')
49
+ })
50
+
51
+ test('wsl 形:调用方显式 WSLENV 优先于继承值', () => {
52
+ const env = buildClientEnv('wsl', { DSH_X: '1', WSLENV: 'A' }, undefined, { inheritedWslenv: 'B' })
53
+ assert.equal(env.WSLENV, 'A:DSH_X')
54
+ })
55
+
56
+ test('条目显式 WSLENV 优先于继承值,追加键去重保留', () => {
57
+ const env = buildClientEnv('wsl', { DSH_X: '1' }, { WSLENV: 'E', MSYSTEM: 'MINGW64' }, { inheritedWslenv: 'WT_SESSION:MSYSTEM:' })
58
+ assert.equal(env.WSLENV, 'E:MSYSTEM:DSH_X')
59
+ })
60
+
61
+ test('继承值已含的追加键不产生重复段', () => {
62
+ const env = buildClientEnv('wsl', { DSH_X: '1' }, { MSYSTEM: 'MINGW64' }, { inheritedWslenv: 'WT:DSH_X:' })
63
+ assert.equal(env.WSLENV, 'WT:DSH_X:MSYSTEM')
64
+ })
65
+
66
+ test('wsl 形:无继承无显式时从空构建,无空条目', () => {
67
+ const env = buildClientEnv('wsl', { DSH_X: '1' }, undefined, { inheritedWslenv: undefined })
68
+ assert.equal(env.WSLENV, 'DSH_X')
69
+ })
70
+
71
+ test('非 wsl 形不受 inheritedWslenv 影响', () => {
72
+ const env = buildClientEnv('bash', { DSH_X: '1' }, undefined, { inheritedWslenv: 'B' })
73
+ assert.equal(env.WSLENV, undefined)
74
+ })
75
+
76
+ test('覆盖集始终在场且可被 dshEnv 覆盖', () => {
77
+ const env = buildClientEnv('cmd', { PAGER: 'more' })
78
+ assert.equal(env.NO_COLOR, '1')
79
+ assert.equal(env.PAGER, 'more')
80
+ assert.deepEqual(Object.keys(ENV_OVERRIDES).sort(), ['GIT_PAGER', 'NO_COLOR', 'PAGER'])
81
+ })
@@ -0,0 +1,136 @@
1
+ // executor 官方 seam 集成(桩 ctx):BDD 场景见本文件与 README「官方工具兼容」。
2
+ // 背景:preset 注入的官方 tool-pwsh 经 cordis 服务代理(ctx.shell)调用执行器,
3
+ // 代理把方法 this 重定向到阴影对象;执行器公开面必须与官方 PwshLocalExecutor
4
+ // 同构地使用公有字段/方法(#私有在阴影 receiver 下触发品牌检查错误)。
5
+
6
+ import { test } from 'node:test'
7
+ import assert from 'node:assert/strict'
8
+ import ShellSelectExecutor from '../src/executor.mjs'
9
+ import { Config } from '../src/config.mjs'
10
+
11
+ function stubReader(text) {
12
+ return { readFrom: () => ({ text, lossy: false, nextOffset: text.length }) }
13
+ }
14
+
15
+ function stubSpawn(stdoutText = '', stderrText = '', exitCode = 0) {
16
+ const calls = []
17
+ const spawn = (spec) => {
18
+ calls.push(spec)
19
+ return {
20
+ collected: {
21
+ stdout: stubReader(stdoutText),
22
+ stderr: stubReader(stderrText),
23
+ },
24
+ done: Promise.resolve({ exitCode, signal: null }),
25
+ terminate: () => true,
26
+ }
27
+ }
28
+ return { spawn, calls }
29
+ }
30
+
31
+ function stubCtx({ sandboxMode = 'danger-full-access', spawn = stubSpawn() } = {}) {
32
+ const registered = { tools: [], sections: [], promptSections: [], routes: [] }
33
+ const settingsImpl = {
34
+ installSection: (ctx, ns, schema, base, hooks) => {
35
+ registered.sections.push({ ns, schema, base, hooks })
36
+ settingsImpl._source = () => Config(base ?? {})
37
+ },
38
+ }
39
+ const ctx = {
40
+ reflect: { provide: () => {} },
41
+ logger: { warn: () => {} },
42
+ get(service) {
43
+ if (service === 'webServer') return { register: (item) => registered.routes.push(item.path) }
44
+ return undefined
45
+ },
46
+ subprocess: spawn,
47
+ sandbox: {
48
+ confine: (argv, policy) => ({
49
+ argv: ['WRAPPED', ...argv],
50
+ enforcement: 'full',
51
+ denialSignatures: ['file access denied'],
52
+ runnerFailureRules: [],
53
+ _policy: policy,
54
+ }),
55
+ },
56
+ sandboxPolicy: {
57
+ defaultMode: sandboxMode,
58
+ resolve: () => ({ mode: sandboxMode, roots: [] }),
59
+ },
60
+ tools: {
61
+ register: (definition) => {
62
+ registered.tools.push(definition)
63
+ return () => registered.tools.pop()
64
+ },
65
+ },
66
+ systemPrompt: {
67
+ section: (section) => registered.promptSections.push(section),
68
+ getSectionOrder: (key) => `order:${key}`,
69
+ },
70
+ shellEnv: { collect: () => ({ DSH_TEST: '1' }) },
71
+ settings: settingsImpl,
72
+ effect: (fn) => fn(),
73
+ }
74
+ return { ctx, registered }
75
+ }
76
+
77
+ /** cordis 阴影 receiver 模拟:服务方法被以非实例 receiver 调用(Reflect.get 语义)。 */
78
+ function shadow(executor) {
79
+ return Object.create(executor)
80
+ }
81
+
82
+ test('官方 run seam:存在且走默认客户端,spec 已解析形态(danger 直跑)', async () => {
83
+ const spawn = stubSpawn('seam-out', '')
84
+ const { ctx } = stubCtx({ spawn })
85
+ const executor = new ShellSelectExecutor(ctx, {})
86
+ assert.equal(typeof executor.run, 'function')
87
+ const result = await executor.run(executor.resolve({ command: 'Get-Date', workdir: process.cwd() }))
88
+ assert.equal(spawn.calls.length, 1)
89
+ assert.equal(spawn.calls[0].argv[0], executor.entryFor('pwsh').path)
90
+ assert.equal(result.stdout.text, 'seam-out')
91
+ assert.deepEqual(result.sandbox, { mode: 'danger-full-access', denied: false })
92
+ })
93
+
94
+ test('官方 start seam:存在且返回 ShellProcess 形态句柄', async () => {
95
+ const spawn = stubSpawn('bg', '')
96
+ const { ctx } = stubCtx({ spawn })
97
+ const executor = new ShellSelectExecutor(ctx, {})
98
+ assert.equal(typeof executor.start, 'function')
99
+ const proc = executor.start(executor.resolve({ command: 'sleep 1', workdir: process.cwd() }))
100
+ assert.equal(proc.status, 'running')
101
+ await proc.done
102
+ assert.equal(proc.status, 'completed')
103
+ })
104
+
105
+ test('cordis 阴影 receiver:公开面调用不触发私有品牌错误', () => {
106
+ const { ctx } = stubCtx()
107
+ const executor = new ShellSelectExecutor(ctx, {})
108
+ const mirrored = shadow(executor)
109
+ // 官方 tool-pwsh 的调用面:resolve/run/start + apply 期 sandboxMode
110
+ assert.doesNotThrow(() => mirrored.resolve({ command: 'x' }))
111
+ assert.equal(typeof mirrored.run, 'function')
112
+ assert.equal(typeof mirrored.start, 'function')
113
+ assert.equal(mirrored.entryFor(undefined).id, 'pwsh')
114
+ assert.ok(Array.isArray(mirrored.listShells().shells))
115
+ assert.equal(mirrored.config.default, 'pwsh')
116
+ assert.equal(mirrored.sandboxMode, 'danger-full-access')
117
+ })
118
+
119
+ test('cordis 阴影 receiver:受限模式 run 全链路(confine + 结果分类)', async () => {
120
+ const spawn = stubSpawn('', 'file access denied here', 1)
121
+ const { ctx } = stubCtx({ spawn, sandboxMode: 'read-only' })
122
+ const executor = new ShellSelectExecutor(ctx, {})
123
+ const mirrored = shadow(executor)
124
+ const result = await mirrored.run(mirrored.resolve({ command: 'dir', workdir: process.cwd() }))
125
+ assert.equal(spawn.calls[0].argv[0], 'WRAPPED')
126
+ assert.equal(result.sandbox.denied, true)
127
+ assert.equal(result.sandbox.mode, 'read-only')
128
+ })
129
+
130
+ test('sandboxMode:透出部署默认模式,策略缺席时 undefined', () => {
131
+ const { ctx } = stubCtx({ sandboxMode: 'workspace-write' })
132
+ const executor = new ShellSelectExecutor(ctx, {})
133
+ assert.equal(executor.sandboxMode, 'workspace-write')
134
+ const bare = new ShellSelectExecutor({ ...ctx, sandboxPolicy: undefined }, {})
135
+ assert.equal(bare.sandboxMode, undefined)
136
+ })