@mzzsfy/dsh-shell-select 0.3.1 → 0.4.1

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,169 @@
1
+ // 渲染守卫:全量执行 client.js 工厂(React 桩),实际调用注册出的 toolview
2
+ // 组件——纯文本/LOGIC 提取守卫抓不住的渲染期未定义引用(model 丢失事故)在此暴露。
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
+
10
+ const here = dirname(fileURLToPath(import.meta.url))
11
+ const source = readFileSync(join(here, '..', 'src', 'client.js'), 'utf8')
12
+
13
+ function reactStub() {
14
+ // 函数组件即时执行(模拟渲染语义):组件体内的未定义引用/坏访问当场抛出
15
+ const createElement = (type, props, ...children) => {
16
+ if (typeof type === 'function') return type(props ?? {})
17
+ return { type, props, children }
18
+ }
19
+ return {
20
+ createElement,
21
+ useState: (init) => [typeof init === 'function' ? init() : init, () => {}],
22
+ useEffect: () => {},
23
+ }
24
+ }
25
+
26
+ // 加载 client.js 并执行 apply,返回 slots 注册面
27
+ function loadClient() {
28
+ const React = reactStub()
29
+ const registrations = []
30
+ const slots = {
31
+ inject: (_name, factory) => factory(),
32
+ register: (meta, component) => registrations.push({ meta, component }),
33
+ }
34
+ const document = {
35
+ documentElement: { lang: 'zh-CN' },
36
+ createElement: () => ({ setAttribute: () => {}, remove: () => {}, style: {} }),
37
+ head: { appendChild: () => {} },
38
+ }
39
+ let loaded
40
+ const window = { __ModuleLoader__: { load: (definition) => { loaded = definition } } }
41
+ new Function('window', 'document', 'navigator', source)(window, document, {}, undefined)
42
+ assert.notEqual(loaded, undefined, 'client.js 未调用 __ModuleLoader__.load')
43
+ const ctx = {
44
+ effect(fn) {
45
+ return fn()
46
+ },
47
+ slots,
48
+ }
49
+ loaded.factory((name) => {
50
+ if (name === 'react') return React
51
+ // 其余模块(primitives 等)按缺席处理:抛错触发 client 内 try/catch 降级
52
+ throw new Error('module not available: ' + name)
53
+ }).apply(ctx)
54
+ return registrations
55
+ }
56
+
57
+ function toolviewOf(registrations, key) {
58
+ const view = registrations.find((item) => item.meta.name === 'tool.call.toolview' && item.meta.key === key)
59
+ assert.notEqual(view, undefined, `缺少 key=${key} 的 toolview 注册`)
60
+ return view.component
61
+ }
62
+
63
+ // 已定调用块(官方形态:kind 字段在)
64
+ function settledBlock(argsRaw, text) {
65
+ return {
66
+ kind: 'tool',
67
+ callId: 'c1',
68
+ call: { callId: 'c1', name: 'shell', argsRaw },
69
+ content: [{ type: 'text', text }],
70
+ }
71
+ }
72
+
73
+ test('渲染守卫:settled terminal 调用组件执行不抛(引用完整)', () => {
74
+ const component = toolviewOf(loadClient(), 'shell')
75
+ const element = component({
76
+ block: settledBlock(JSON.stringify({ command: 'git status', description: 'status' }), 'On branch main\n[exit code: 0]'),
77
+ cwd: 'C:\\repo',
78
+ })
79
+ assert.notEqual(element, undefined)
80
+ })
81
+
82
+ test('渲染守卫:running 块组件执行不抛', () => {
83
+ const component = toolviewOf(loadClient(), 'pwsh')
84
+ const element = component({
85
+ block: { callId: 'c2', name: 'pwsh', argsRaw: JSON.stringify({ command: 'Start-Sleep 1', description: 'wait' }), time: Date.now() - 65_000 },
86
+ cwd: 'C:\\repo',
87
+ })
88
+ assert.notEqual(element, undefined)
89
+ })
90
+
91
+ test('渲染守卫:运行中卡展开后可见命令全文与实时时长', () => {
92
+ const component = toolviewOf(loadClient(), 'shell')
93
+ // 默认收起:运行中折叠行 = 黄点 + 描述 + 时长,不含卡体
94
+ const element = component({
95
+ block: { callId: 'c3', name: 'shell', argsRaw: JSON.stringify({ command: 'npm run build', description: 'build' }), time: Date.now() - 65_000 },
96
+ cwd: 'C:\\repo',
97
+ })
98
+ assert.doesNotMatch(JSON.stringify(element), /sls-tv__body/)
99
+ assert.match(JSON.stringify(element), /1:05/)
100
+ })
101
+
102
+ test('渲染守卫:后台 ack 块(默认收起行 + 任务号徽标)组件执行不抛', () => {
103
+ const component = toolviewOf(loadClient(), 'bash')
104
+ const element = component({
105
+ block: settledBlock(JSON.stringify({ command: 'npm test', description: 'tests', run_in_background: true }), 'started background job bg-1'),
106
+ cwd: 'C:\\repo',
107
+ })
108
+ assert.notEqual(element, undefined)
109
+ })
110
+
111
+ test('设置卡注册:settings.section 单卡 id shell-select', () => {
112
+ const registrations = loadClient()
113
+ const card = registrations.find((item) => item.meta.name === 'settings.section')
114
+ assert.notEqual(card, undefined)
115
+ assert.equal(card.meta.id, 'shell-select')
116
+ })
117
+
118
+ test('图标名守卫:0.1.7 primitives 字重后缀名在场,尺寸后缀旧名不在首选位', () => {
119
+ // 0.1.7 图标改名:尺寸从名字移除(IconApiOutline14 → IconApiOutlineRegular/Medium),
120
+ // 旧名 miss 使 IconApi 落淡灰 fallback 被当成"空白 icon"(实测事故)
121
+ assert.match(source, /'IconApiOutlineRegular'/)
122
+ assert.match(source, /'IconInspectOutlineRegular'/)
123
+ assert.doesNotMatch(source, /createElementOf\('IconApiOutline14'/)
124
+ })
125
+
126
+ test('名册单飞守卫:失败复位 promise,下次渲染重试(宿主启动窗口 404 不永久缺徽章)', () => {
127
+ // 旧实现 catch 静默吞错且 promise 不复位:boot 窗口期 404 后所有卡永久缺客户端徽章
128
+ assert.doesNotMatch(source, /catch\(\(\) => \{ \}\)/)
129
+ assert.match(source, /clientCatalogPromise = null/)
130
+ })
131
+
132
+ test('后台卡头部守卫:状态点缺省渲染(bg pill 即标识),非法 StateDot state 不再传入', () => {
133
+ // StateDot 合法 state 仅 done/warning/ongoing/error/idle;'none' 是编造值
134
+ assert.match(source, /dot: undefined/)
135
+ assert.match(source, /meta\.dot !== undefined \? TOOLVIEW_ICONS\.StateDot/)
136
+ assert.doesNotMatch(source, /dot: 'none'/)
137
+ })
138
+
139
+ test('hooks 恒序守卫:组件内 hook 调用必须全部位于第一个提前 return 之前', () => {
140
+ // 真实 React 的 hooks 链表按调用序对位:提前 return 之后的 hook 使不同
141
+ // 块形态(generic/terminal)的 hook 数量分叉 → "Rendered fewer hooks
142
+ // than expected" → 行组件树整棵卸载(shell 卡全消失事故)。React 桩无
143
+ // hooks 链表,渲染守卫抓不住,必须源码结构断言。
144
+ const extract = (name) => {
145
+ const begin = source.indexOf(`function ${name}(`)
146
+ assert.ok(begin >= 0, `缺函数 ${name}`)
147
+ const beginLine = source.slice(0, begin).split('\n').length
148
+ const body = source.slice(begin)
149
+ const end = body.indexOf('\n function ', 1)
150
+ const scope = end >= 0 ? body.slice(0, end) : body
151
+ const hookLines = [...scope.matchAll(/\b(?:useState|useEffect|useRef|useMemo|useCallback)\(/g)]
152
+ .map((m) => beginLine + scope.slice(0, m.index).split('\n').length - 1)
153
+ return { hookLines, scopeLines: beginLine + scope.split('\n').length }
154
+ }
155
+ const earlyReturnLine = (scope, beginLine) => {
156
+ const m = /return h\(/.exec(scope)
157
+ return m === null ? Number.POSITIVE_INFINITY : beginLine + scope.slice(0, m.index).split('\n').length - 1
158
+ }
159
+ for (const name of ['ShellToolRow', 'GenericShellRow', 'CopyButton', 'RunningDuration', 'ShellSelectApp']) {
160
+ const { hookLines } = extract(name)
161
+ const scopeBody = source.slice(source.indexOf(`function ${name}(`))
162
+ const end = scopeBody.indexOf('\n function ', 1)
163
+ const scope = end >= 0 ? scopeBody.slice(0, end) : scopeBody
164
+ const limit = earlyReturnLine(scope, source.slice(0, source.indexOf(`function ${name}(`)).split('\n').length)
165
+ for (const line of hookLines) {
166
+ assert.ok(line < limit, `${name} 的 hook 在第 ${line} 行,位于提前 return(第 ${limit} 行)之后:hooks 恒序被破坏`)
167
+ }
168
+ }
169
+ })
@@ -1,12 +1,12 @@
1
- // 条目级 env 与 wsl distro:schema 默认、argv 形、entryFor 透传。BDD 场景见 docs/feat-shell-select-optim/plan.md S5-S10。
1
+ // 条目级 env 与 wsl distro:schema 默认、argv 形、entryFor 透传。BDD 场景见 docs/feat-shell-select-optim/plan.md S5-S10。
2
2
 
3
3
  import { test } from 'node:test'
4
4
  import assert from 'node:assert/strict'
5
5
  import ShellSelectExecutor from '../src/executor.mjs'
6
- import { Config, buildArgv, defaultConfig } from '../src/config.mjs'
6
+ import { resolveConfig, buildArgv, defaultConfig, entryById, requireEntry } from '../src/config.mjs'
7
7
 
8
8
  test('S5 schema 默认:条目 env 空对象、distro 空串,旧配置反序列化补默认', () => {
9
- const config = Config({ shells: [{ id: 'w', name: 'WSL', kind: 'wsl' }], default: 'w' })
9
+ const config = resolveConfig({ shells: [{ id: 'w', name: 'WSL', kind: 'wsl' }], default: 'w' })
10
10
  const entry = config.shells[0]
11
11
  assert.deepEqual(entry.env, {})
12
12
  assert.equal(entry.distro, '')
@@ -32,7 +32,7 @@ test('S10 args 模板条目接管 argv,distro 不出现', () => {
32
32
 
33
33
  test('S6 entryFor 透传 env 与 distro;spawnSpec env 含条目键', () => {
34
34
  const cmdPath = process.execPath
35
- const config = Config({
35
+ const config = resolveConfig({
36
36
  shells: [{ id: 'c', name: 'CMD', kind: 'cmd', path: cmdPath, env: { MSYSTEM: 'MINGW64' } }, { id: 'p', name: 'pwsh', kind: 'pwsh' }],
37
37
  default: 'c',
38
38
  })
@@ -45,19 +45,24 @@ test('S6 entryFor 透传 env 与 distro;spawnSpec env 含条目键', () => {
45
45
  assert.equal(spawned.env.NO_COLOR, '1')
46
46
  })
47
47
 
48
- test('S7 updateConfig 接受 env/distro 字段并落盘回读', async () => {
49
- const section = Config({
48
+ test('S7 web 行写路径接受 env/distro 字段并落盘', async () => {
49
+ const { SECTION_NS, buildFaces } = await import('../src/web-routes.mjs')
50
+ const section = resolveConfig({
50
51
  shells: [
51
52
  { id: 'p', name: 'pwsh', kind: 'pwsh' },
52
53
  { id: 'w', name: 'WSL', kind: 'wsl', distro: 'Debian', env: { LANG: 'C.UTF-8' } },
53
54
  ],
54
55
  default: 'p',
55
56
  })
56
- const executor = new ShellSelectExecutor(stubCtxFor(section), section)
57
- const next = await executor.updateConfig({})
57
+ const replaced = []
58
+ const ctx = { logger: { warn: () => {} } }
59
+ const settings = { describe: () => [{ ns: SECTION_NS, value: section }], replace: async (ns, next) => { replaced.push({ ns, next }) } }
60
+ const faces = buildFaces(ctx, settings)
61
+ const next = await faces.updateConfig({})
58
62
  const wsl = next.shells.find((entry) => entry.id === 'w')
59
63
  assert.equal(wsl.distro, 'Debian')
60
64
  assert.deepEqual(wsl.env, { LANG: 'C.UTF-8' })
65
+ assert.equal(replaced[0].ns, SECTION_NS)
61
66
  })
62
67
 
63
68
  function stubCtxFor(config) {
@@ -3,10 +3,11 @@
3
3
 
4
4
  import { test } from 'node:test'
5
5
  import assert from 'node:assert/strict'
6
- import { Config, defaultConfig, entryById, requireEntry, buildArgv, KINDS, RESOLVED_AUTO } from '../src/config.mjs'
6
+ import { resolveConfig, resolveConfigStrict, coerceServiceable, defaultConfig, entryById, requireEntry, buildArgv, KINDS, RESOLVED_AUTO } from '../src/config.mjs'
7
+ import { assertServiceableConfig } from '../src/config.mjs'
7
8
 
8
9
  test('schema 应用出厂默认:三客户端 + 默认 pwsh', () => {
9
- const applied = Config({})
10
+ const applied = resolveConfig({})
10
11
  assert.deepEqual(applied, defaultConfig())
11
12
  assert.equal(applied.default, 'pwsh')
12
13
  assert.deepEqual(applied.shells.map((entry) => entry.id), ['pwsh', 'git-bash', 'cmd'])
@@ -17,7 +18,97 @@ test('schema 应用出厂默认:三客户端 + 默认 pwsh', () => {
17
18
  })
18
19
 
19
20
  test('schema 拒绝未知 kind;default 跨字段一致性由 validate hook 与 requireEntry 兜底', () => {
20
- assert.throws(() => Config({ shells: [{ id: 'x', name: 'X', kind: 'fish', path: '' }], default: 'x' }))
21
+ assert.throws(() => resolveConfig({ shells: [{ id: 'x', name: 'X', kind: 'fish', path: '' }], default: 'x' }))
22
+ })
23
+
24
+ test('落盘坏形态防御:volatile 字段被序列化成对象时降级默认,行构造不炸', () => {
25
+ // 事故:原生设置页/ref 序列化把 shells 落盘为 {},冷启动 Config 抛
26
+ // "expected array but got [object Object]" → 行死 → settings 无节 →
27
+ // 设置页 "no longer configurable" 死锁。类型不符的键必须降级默认。
28
+ const poisoned = {
29
+ shells: {},
30
+ deny: { 0: 'rm' },
31
+ default: 42,
32
+ timeoutMs: 'forever',
33
+ cwd: ['C:\\'],
34
+ good: 'ignored-by-schema',
35
+ }
36
+ const applied = resolveConfig(poisoned)
37
+ assert.deepEqual(applied, defaultConfig())
38
+ // 合法值原样保留,清洗不做过度剥离
39
+ const valid = resolveConfig({ shells: [{ id: 'pwsh', kind: 'pwsh' }], default: 'pwsh', deny: ['rm -rf'], cwd: 'C:\\tmp', timeoutMs: 5000 })
40
+ assert.equal(valid.shells.length, 1)
41
+ assert.equal(valid.default, 'pwsh')
42
+
43
+ // 执行面 strict:同款坏形态必须炸(宿主 resolve 产物须先解箱;静默降级
44
+ // 曾让行 config 永不生效,0.2.0-rc.2 真机实证)。宽容路径(上方)保留。
45
+ assert.throws(() => resolveConfigStrict(poisoned), /形态非法.*shells/)
46
+ // 合法输入与 undefined 缺省:strict 与宽容同产出
47
+ assert.deepEqual(resolveConfigStrict(valid), valid)
48
+ assert.deepEqual(resolveConfigStrict(undefined), defaultConfig())
49
+ assert.deepEqual(valid.deny, ['rm -rf'])
50
+ assert.equal(valid.cwd, 'C:\\tmp')
51
+ assert.equal(valid.timeoutMs, 5000)
52
+ // 非对象整体(null/数组/标量)= 无配置,出厂默认
53
+ for (const junk of [null, 'x', 42, ['shells']]) {
54
+ assert.deepEqual(resolveConfig(junk), defaultConfig())
55
+ }
56
+ })
57
+
58
+ // --- coerceServiceable:任意用户输入产出可服务配置(插件不因配置死) ---
59
+
60
+ test('coerce:悬空 default 归 shells[0],空/全非法 shells 回落出厂默认', () => {
61
+ const { config, warnings } = coerceServiceable({ shells: [{ id: 'pwsh', kind: 'pwsh' }], default: 'nope' })
62
+ assert.equal(config.default, 'pwsh')
63
+ assert.ok(warnings.some((line) => line.includes('default')))
64
+ const emptied = coerceServiceable({ shells: [{ id: 'x', kind: 'fish' }], default: 'x' })
65
+ assert.deepEqual(emptied.config.shells.map((entry) => entry.id), ['pwsh', 'git-bash', 'cmd'])
66
+ assert.equal(emptied.config.default, 'pwsh')
67
+ assert.ok(emptied.warnings.length >= 2)
68
+ })
69
+
70
+ test('coerce:预算字段非正数降默认,deny 剔除不可编译条目,合法输入零告警原样', () => {
71
+ const { config, warnings } = coerceServiceable({
72
+ shells: [{ id: 'pwsh', kind: 'pwsh' }],
73
+ default: 'pwsh',
74
+ timeoutMs: -5,
75
+ graceMs: Number.MAX_SAFE_INTEGER,
76
+ deny: ['rm -rf', '(unclosed', '', 42],
77
+ })
78
+ assert.equal(config.timeoutMs, undefined)
79
+ assert.equal(config.graceMs, undefined)
80
+ assert.deepEqual(config.deny, ['rm -rf'])
81
+ assert.equal(warnings.length, 3)
82
+ const clean = coerceServiceable({ shells: [{ id: 'pwsh', kind: 'pwsh' }], default: 'pwsh' })
83
+ assert.equal(clean.warnings.length, 0)
84
+ // 产物必过可服务校验:任意输入不产出会炸挂载的配置
85
+ assertServiceableConfig(resolveConfig(clean.config))
86
+ assertServiceableConfig(resolveConfig(config))
87
+ })
88
+
89
+ test('冻结输入防御:volatile ref 深冻结快照喂入不改写不抛(挂载路径)', () => { // 事故:cosmokit createVolatile 的 snapshot 对 volatile 值逐层 Object.freeze,
90
+ // 行 Config ref .get() 产物是深冻结对象;sanitizeConfigEntry 引用透传,
91
+ // schemastery resolve 原地改写入参(填默认)撞冻结 env 抛
92
+ // "Cannot assign to read only property 'MSYSTEM'" → 执行器挂载失败,
93
+ // 每次启动回滚官方 pwsh 链,shell 工具永不注册。
94
+ // 契约:resolveConfig 对输入不可变(与 unwrapConfig 输出 clone 对称)。
95
+ const frozenSection = Object.freeze({
96
+ shells: Object.freeze([
97
+ Object.freeze({ id: 'pwsh', kind: 'pwsh', path: '', args: Object.freeze([]), env: Object.freeze({}) }),
98
+ Object.freeze({ id: 'git-bash', kind: 'bash', path: '', args: Object.freeze([]), login: true, distro: '', env: Object.freeze({ MSYSTEM: 'MINGW64' }) }),
99
+ ]),
100
+ default: 'git-bash',
101
+ })
102
+ const applied = resolveConfig(frozenSection)
103
+ assert.equal(applied.default, 'git-bash')
104
+ assert.deepEqual(applied.shells.map((entry) => entry.id), ['pwsh', 'git-bash'])
105
+ assert.deepEqual(applied.shells[1].env, { MSYSTEM: 'MINGW64' })
106
+ // 二轮 resolve(行重载/快照回灌常态)同样幂等
107
+ const again = resolveConfig(frozenSection)
108
+ assert.deepEqual(again, applied)
109
+ // 输出与冻结输入脱耦:改输出不 affects 输入,反复解析结果稳定
110
+ applied.shells[1].env.MSYSTEM = 'TERM'
111
+ assert.deepEqual(resolveConfig(frozenSection).shells[1].env, { MSYSTEM: 'MINGW64' })
21
112
  })
22
113
 
23
114
  test('pwsh argv:非交互形 + 编码前缀', () => {
@@ -50,7 +141,7 @@ test('login 显式配置与自定义 args 模板互斥时模板优先', () => {
50
141
  })
51
142
 
52
143
  test('schema 往返:login 布尔保留,旧配置缺省落 false', () => {
53
- const applied = Config({ shells: [
144
+ const applied = resolveConfig({ shells: [
54
145
  { id: 'm', name: 'MSYS2', kind: 'bash', path: 'C:\\msys64\\usr\\bin\\bash.exe', login: true },
55
146
  { id: 'g', name: 'Git Bash', kind: 'bash', path: '' },
56
147
  ], default: 'm' })
@@ -1,4 +1,4 @@
1
- // deny 顶层字段:schema 往返与 updateConfig 落盘回读。
1
+ // deny 顶层字段:schema 往返与 updateConfig 落盘回读。
2
2
  // BDD 场景见 docs/progress/feat-shell-select-deny.md 场景 7。
3
3
 
4
4
  import { test } from 'node:test'
@@ -6,10 +6,10 @@ import assert from 'node:assert/strict'
6
6
  import { readFileSync } from 'node:fs'
7
7
  import { fileURLToPath } from 'node:url'
8
8
  import { dirname, join } from 'node:path'
9
- import { Config, defaultConfig } from '../src/config.mjs'
9
+ import { resolveConfig, defaultConfig } from '../src/config.mjs'
10
10
 
11
11
  const here = dirname(fileURLToPath(import.meta.url))
12
- const executorSource = readFileSync(join(here, '..', 'src', 'executor.mjs'), 'utf8')
12
+ const webRoutesSource = readFileSync(join(here, '..', 'src', 'web-routes.mjs'), 'utf8')
13
13
 
14
14
  test('schema 默认:deny 空数组,旧配置反序列化补默认', () => {
15
15
  const config = defaultConfig()
@@ -17,17 +17,17 @@ test('schema 默认:deny 空数组,旧配置反序列化补默认', () => {
17
17
  })
18
18
 
19
19
  test('schema 往返:deny 保留;allow 无消费者(deny 绝对,无豁免语义)', () => {
20
- const config = Config({ deny: ['format '], allow: ['git .*'] })
20
+ const config = resolveConfig({ deny: ['format '], allow: ['git .*'] })
21
21
  assert.deepEqual(config.deny, ['format '])
22
22
  // schemastery 透传未知键,僵尸键靠"无读取方"守卫:matchDeny/toSection 均只触 deny
23
23
  const denylistSource = readFileSync(join(here, '..', 'src', 'denylist.mjs'), 'utf8')
24
24
  const clientSource = readFileSync(join(here, '..', 'src', 'client.js'), 'utf8')
25
25
  assert.doesNotMatch(denylistSource, /allow/)
26
26
  assert.doesNotMatch(clientSource, /allowText|section\.allow/)
27
- assert.doesNotMatch(executorSource, /current\.allow|patch\.allow/)
27
+ assert.doesNotMatch(webRoutesSource, /current\.allow|patch\.allow/)
28
28
  })
29
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/)
30
+ test('updateConfig 携带 deny(wholesale replace 防静默重置,同 login 教训;写路径在 web 行)', () => {
31
+ assert.match(webRoutesSource, /deny: Array\.isArray\(patch\.deny\) \? patch\.deny : current\.deny/)
32
+ assert.doesNotMatch(webRoutesSource, /patch\.allow/)
33
33
  })
@@ -1,4 +1,4 @@
1
- // executor 官方 seam 集成(桩 ctx):BDD 场景见本文件与 README「官方工具兼容」。
1
+ // executor 官方 seam 集成(桩 ctx):BDD 场景见本文件与 README「官方工具兼容」。
2
2
  // 背景:preset 注入的官方 tool-pwsh 经 cordis 服务代理(ctx.shell)调用执行器,
3
3
  // 代理把方法 this 重定向到阴影对象;执行器公开面必须与官方 PwshLocalExecutor
4
4
  // 同构地使用公有字段/方法(#私有在阴影 receiver 下触发品牌检查错误)。
@@ -6,7 +6,7 @@
6
6
  import { test } from 'node:test'
7
7
  import assert from 'node:assert/strict'
8
8
  import ShellSelectExecutor from '../src/executor.mjs'
9
- import { Config } from '../src/config.mjs'
9
+ import { resolveConfig } from '../src/config.mjs'
10
10
 
11
11
  function stubReader(text) {
12
12
  return { readFrom: () => ({ text, lossy: false, nextOffset: text.length }) }
@@ -33,7 +33,7 @@ function stubCtx({ sandboxMode = 'danger-full-access', spawn = stubSpawn() } = {
33
33
  const settingsImpl = {
34
34
  installSection: (ctx, ns, schema, base, hooks) => {
35
35
  registered.sections.push({ ns, schema, base, hooks })
36
- settingsImpl._source = () => Config(base ?? {})
36
+ settingsImpl._source = () => resolveConfig(base ?? {})
37
37
  },
38
38
  }
39
39
  const ctx = {