@mzzsfy/dsh-shell-select 0.3.1 → 0.4.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.
@@ -1,21 +1,19 @@
1
- // preset 接管守卫:本包自带预设(standard 副本 − tool-pwsh 行)与 bundle patch
2
- // 的 agent-presets 覆写形态。漂移守卫对照已安装 dsh 的内置 standard 预设——
3
- // 上游增删行/改表达式即红,升级后按官方实文对表镜像;宿主缺失(无 dsh 本体的
4
- // 最小环境)时 skip,不制造假红。
1
+ // preset 接管守卫(0.1.7 预设架构):本包 patch insert 自带预设声明
2
+ // (preset-shell-select,standard 内联 roster − tool-pwsh 行)并覆写
3
+ // agent-preset-registry 的 default。漂移守卫对照已安装 dsh 的
4
+ // dsh-web-app/presets/standard.patch.yml——上游增删行/改表达式即红,升级后
5
+ // 按官方实文对表镜像;宿主缺失(无 dsh 本体的最小环境)时 skip,不制造假红。
5
6
 
6
7
  import { test } from 'node:test'
7
8
  import assert from 'node:assert/strict'
8
- import { readFileSync, existsSync, mkdirSync, symlinkSync, rmSync, readdirSync } from 'node:fs'
9
+ import { readFileSync, existsSync, readdirSync } from 'node:fs'
9
10
  import { fileURLToPath, pathToFileURL } from 'node:url'
10
11
  import { dirname, join, resolve } from 'node:path'
11
12
  import { createRequire } from 'node:module'
12
- import { tmpdir } from 'node:os'
13
13
  import { execPath } from 'node:process'
14
14
 
15
15
  const here = dirname(fileURLToPath(import.meta.url))
16
16
  const pkgRoot = join(here, '..')
17
- const presetText = readFileSync(join(pkgRoot, 'presets', 'shell-select', 'agent.cordis.yml'), 'utf8')
18
- const metaText = readFileSync(join(pkgRoot, 'presets', 'shell-select', 'preset.yml'), 'utf8')
19
17
  const patchText = readFileSync(join(pkgRoot, 'cordis.patch.yml'), 'utf8')
20
18
 
21
19
  // ── 宿主侧 yaml 能力探测:js-yaml 与官方 entryListSchema 经 dsh 安装位解析 ──
@@ -41,8 +39,6 @@ function locateHarnessDir() {
41
39
  // yaml 能力与宿主解耦:js-yaml 为 devDep(CI 无宿主也可解析本包文件)。
42
40
  // !!js 标签本地同形 Type:tag 必须写完整 URI(tag:yaml.org,2002:js)——文档简写
43
41
  // !!js 展开后按完整 URI 匹配,字面 '!!js' 永不命中(unknown tag,CI 假绿教训)。
44
- // 形态场景恒走 LOCAL_SCHEMA(本机/CI 同路径);漂移守卫在场时改用官方
45
- // entryListSchema 求同(对照面只在有宿主时跑)
46
42
  import yamlStatic from 'js-yaml'
47
43
 
48
44
  const LOCAL_SCHEMA = yamlStatic.JSON_SCHEMA.extend(new yamlStatic.Type('tag:yaml.org,2002:js', {
@@ -68,7 +64,9 @@ const loadYaml = (text) => yamlStatic.load(text, { schema: entryListSchema })
68
64
  // 镜像 loader 的表达式求值形态(new Function + with(ctx),全局可用)
69
65
  const evalExpr = (expr, ctx) => new Function('ctx', `with (ctx) { return (${expr}) }`)(ctx)
70
66
 
71
- // ── 结构对比:展平行树为 id → {name, disabled, config 键集} ──
67
+ // ── 结构对比:展平行树为 id → {name, disabled, isolate, config 全值} ──
68
+ // config 比全值而非键集:键集守不住文本漂移(plan-mode section 曾落后官方一句
69
+ // 而守卫全绿),镜像声明即「standard 全量 − tool-pwsh 逐项同构」,值必须对表
72
70
 
73
71
  function flattenRows(rows, out = new Map()) {
74
72
  for (const row of rows ?? []) {
@@ -76,7 +74,8 @@ function flattenRows(rows, out = new Map()) {
76
74
  out.set(row.id, {
77
75
  name: row.name,
78
76
  disabled: row.disabled === undefined ? undefined : row.disabled.__jsExpr ?? row.disabled,
79
- configKeys: row.config && !Array.isArray(row.config) ? Object.keys(row.config).sort() : undefined,
77
+ isolate: row.isolate === undefined ? undefined : JSON.parse(JSON.stringify(row.isolate)),
78
+ config: row.config === undefined ? undefined : JSON.parse(JSON.stringify(row.config)),
80
79
  })
81
80
  }
82
81
  if (row.group && Array.isArray(row.config)) flattenRows(row.config, out)
@@ -84,18 +83,16 @@ function flattenRows(rows, out = new Map()) {
84
83
  return out
85
84
  }
86
85
 
87
- // ── 场景 ──
86
+ // 本包 patch 解析:insert 的预设声明行 + 各覆写行
87
+ const patchRows = yamlStatic.load(patchText, { schema: LOCAL_SCHEMA })
88
+ const insertRows = patchRows.filter((entry) => entry.insert !== undefined).flatMap((entry) => entry.insert)
89
+ const ourPresetRow = insertRows.find((row) => row.id === 'preset-shell-select')
90
+ const ourPlugins = ourPresetRow?.config?.plugins ?? []
88
91
 
89
- test('预设形态:preset.yml 元数据在场', () => {
90
- const meta = metaText.trim().split('\n').map((line) => line.split(/:(.*)/s)[0])
91
- assert.ok(meta.includes('name'), 'preset.yml 缺 name')
92
- assert.ok(metaText.includes('description:'), 'preset.yml 缺 description')
93
- })
94
-
95
- test('预设形态:agent.cordis.yml 可解析且无 tool-pwsh 行', () => {
96
- // 形态断言恒走本地同形 schema:本机与 CI 同一路径,官方 schema 在场与否不影响本场景
97
- const rows = yamlStatic.load(presetText, { schema: LOCAL_SCHEMA })
98
- assert.ok(Array.isArray(rows), '预设组合必须是行数组')
92
+ test('patch 形态:自带预设声明行(preset-shell-select,0.1.7 内联架构)', () => {
93
+ assert.ok(ourPresetRow, 'patch 缺 preset-shell-select 声明(insert 段)')
94
+ assert.equal(ourPresetRow.name, '@deepseek-ai/dsh-agent-preset')
95
+ assert.equal(ourPresetRow.config.id, 'shell-select')
99
96
  const ids = new Set()
100
97
  const walk = (list) => {
101
98
  for (const row of list) {
@@ -103,17 +100,35 @@ test('预设形态:agent.cordis.yml 可解析且无 tool-pwsh 行', () => {
103
100
  if (row.group && Array.isArray(row.config)) walk(row.config)
104
101
  }
105
102
  }
106
- walk(rows)
103
+ walk(ourPlugins)
107
104
  assert.ok(ids.has('tool-bash'), 'POSIX bash 工具行必须保留')
108
105
  assert.ok(!ids.has('tool-pwsh'), 'tool-pwsh 行必须移除(win32 shell 面由本包接管)')
106
+ assert.ok(!ids.has('persistent-pwsh'), 'persistent-pwsh 行不得进入本包预设')
109
107
  })
110
108
 
111
- test('漂移守卫:与已安装 standard 逐行对表(缺失宿主则 skip)', (t) => {
109
+ test('patch 形态:agent-preset-registry 覆写含平台 default(patch 语义为整体替换 config)', () => {
110
+ const row = patchRows.find((entry) => entry.id === 'agent-preset-registry')
111
+ assert.ok(row, 'patch 缺 agent-preset-registry 覆写行')
112
+ assert.equal(row.name, undefined, 'registry 行是本包上层已 insert 的行,覆写按 id 匹配即无须 name')
113
+ assert.match(row.config.default.__jsExpr, /'shell-select'/)
114
+ assert.match(row.config.default.__jsExpr, /'standard'/)
115
+ })
116
+
117
+ test('patch 表达式求值:default 按平台切换', () => {
118
+ const row = patchRows.find((entry) => entry.id === 'agent-preset-registry')
119
+ const expected = process.platform === 'win32' ? 'shell-select' : 'standard'
120
+ assert.equal(evalExpr(row.config.default.__jsExpr, {}), expected)
121
+ })
122
+
123
+ test('漂移守卫:与已安装 standard 预设逐行对表(缺失宿主则 skip)', (t) => {
112
124
  if (harness === null) return t.skip('未找到 dsh 本体安装,无对照源')
113
- const standardPath = join(harness, 'node_modules', '@deepseek-ai', 'dsh-agent-presets', 'presets', 'standard', 'agent.cordis.yml')
114
- if (!existsSync(standardPath)) return t.skip('dsh 安装位无 standard 预设,无对照源')
115
- const standard = flattenRows(loadYaml(readFileSync(standardPath, 'utf8')))
116
- const ours = flattenRows(loadYaml(presetText))
125
+ const standardPath = join(harness, 'node_modules', '@deepseek-ai', 'dsh-web-app', 'presets', 'standard.patch.yml')
126
+ if (!existsSync(standardPath)) return t.skip('dsh 安装位无 standard 预设声明,无对照源')
127
+ const standardPatch = yamlStatic.load(readFileSync(standardPath, 'utf8'), { schema: LOCAL_SCHEMA })
128
+ const declared = standardPatch.filter((entry) => entry.insert !== undefined).flatMap((entry) => entry.insert).find((row) => row.id === 'preset-standard')
129
+ assert.ok(declared, 'standard.patch.yml 缺 preset-standard 声明')
130
+ const standard = flattenRows(declared.config.plugins)
131
+ const ours = flattenRows(ourPlugins)
117
132
  for (const [id, row] of standard) {
118
133
  if (id === 'tool-pwsh') {
119
134
  assert.ok(!ours.has(id), 'standard 有 tool-pwsh 而本包预设也出现了:镜像被污染')
@@ -131,44 +146,9 @@ test('schema 同构:本地 !!js Type 与官方 entryListSchema 产物 deepEqual(
131
146
  if (harness === null) return t.skip('未找到 dsh 本体安装,无官方 schema 对照')
132
147
  // harness 命中时 entryListSchema 已是官方版:loadYaml=官方路径,与 LOCAL_SCHEMA 逐文本对照。
133
148
  // 本地同形标签的语义漂移曾以 unknown tag 形态炸穿 CI,此处固化为机器断言
134
- for (const [label, text] of [['预设组合', presetText], ['bundle patch', patchText]]) {
135
- assert.deepEqual(
136
- yamlStatic.load(text, { schema: LOCAL_SCHEMA }),
137
- loadYaml(text),
138
- `${label} 双 schema 产物漂移,本地 !!js Type 须与官方 JsExpr 同构`,
139
- )
140
- }
141
- })
142
-
143
- test('patch 形态:agent-presets 覆写带 name 防御且含 default 与 roots', () => {
144
- const patch = yamlStatic.load(patchText, { schema: LOCAL_SCHEMA })
145
- const row = patch.find((entry) => entry.id === 'agent-presets')
146
- assert.ok(row, 'patch 缺 agent-presets 覆写行')
147
- assert.equal(row.name, '@deepseek-ai/dsh-agent-presets', 'name 字段是官方行改名防御,必须钉住包名')
148
- assert.match(row.config.default.__jsExpr, /'shell-select'/)
149
- assert.match(row.config.default.__jsExpr, /'standard'/)
150
- assert.equal(row.config.roots.length, 1)
151
- assert.match(row.config.roots[0].path.__jsExpr, /ctx\.baseUrl/)
152
- })
153
-
154
- test('patch 表达式求值:default 按平台切换,roots 指向本包 presets 目录', () => {
155
- const patch = yamlStatic.load(patchText, { schema: LOCAL_SCHEMA })
156
- const config = patch.find((entry) => entry.id === 'agent-presets').config
157
- const expectedDefault = process.platform === 'win32' ? 'shell-select' : 'standard'
158
- assert.equal(evalExpr(config.default.__jsExpr, { baseUrl: 'file:///any/profile/dir/' }), expectedDefault)
159
- // 合成 profile 布局:baseUrl 锚 <profile>/profiles/web,其下 node_modules/@mzzsfy/dsh-shell-select
160
- // 为符号链接桥(自依赖桥同构;junction 穿透到真实包目录)
161
- const fakeProfile = join(tmpdir(), 'shell-select-preset-probe-' + process.pid)
162
- const linkDir = join(fakeProfile, 'profiles', 'web', 'node_modules', '@mzzsfy')
163
- mkdirSync(linkDir, { recursive: true })
164
- symlinkSync(pkgRoot, join(linkDir, 'dsh-shell-select'), 'junction')
165
- try {
166
- const evaluated = evalExpr(config.roots[0].path.__jsExpr, {
167
- baseUrl: pathToFileURL(join(fakeProfile, 'profiles', 'web') + '/').href,
168
- })
169
- assert.ok(/presets\/$/.test(evaluated), '求值路径必须锚在包内 presets 目录: ' + evaluated)
170
- assert.ok(existsSync(join(evaluated, 'shell-select', 'agent.cordis.yml')), '求值路径必须落在本包 presets 目录(符号链接桥穿透)')
171
- } finally {
172
- rmSync(fakeProfile, { recursive: true, force: true })
173
- }
149
+ assert.deepEqual(
150
+ yamlStatic.load(patchText, { schema: LOCAL_SCHEMA }),
151
+ loadYaml(patchText),
152
+ 'bundle patch 双 schema 产物漂移,本地 !!js Type 须与官方 JsExpr 同构',
153
+ )
174
154
  })
@@ -0,0 +1,397 @@
1
+ // 官方 pwsh 链生命周期探测与运行时接管 BDD(场景编号见 运行时接管-BDD.md):
2
+ // 双行快照 → 四态决策(接管/等待退场/让位/运行时禁用)→ 接线序列(禁用/
3
+ // 退场等待/挂载/回滚)。接管空间判定与 gateway 不同:官方两行带平台表达式,
4
+ // 且真机实证 options.disabled 原始值是 !!js 表达式包装对象(非求值布尔)——
5
+ // 启用中判定必须走 entry.disabled getter 求值结果;宿主默认启用态与用户显式
6
+ // 启用在行树不可区分,行级 false 不作为让位信号——凡求值非 true 即接管空间,
7
+ // 用户意图通道 = 禁用本包。getter 抛错仍让位安全向。禁用只更新启用中的行
8
+ // (0.1.7 混合态常态)。
9
+
10
+ import { test } from 'node:test'
11
+ import assert from 'node:assert/strict'
12
+ import {
13
+ EXIT_POLL_INTERVAL_MS,
14
+ awaitOfficialExit,
15
+ officialChainState,
16
+ officialRowState,
17
+ rollbackDisabledRows,
18
+ runtimeDisableRows,
19
+ takeoverDecision,
20
+ } from '../src/takeover.mjs'
21
+ import { OFFICIAL_ROW_IDS } from '../src/guard-state.mjs'
22
+ import { ShellSelectExecutor } from '../src/executor.mjs'
23
+ import { apply as mainApply } from '../src/index.js'
24
+
25
+ const [TOOL_PWSH, PWSH_SANDBOX] = OFFICIAL_ROW_IDS
26
+
27
+ function loaderOf(entries) {
28
+ const map = new Map(entries.map((entry) => [entry.__id, entry]))
29
+ return {
30
+ resolve: (id) => {
31
+ const bare = [...map.keys()].find((key) => id.endsWith(key))
32
+ if (bare) return map.get(bare)
33
+ throw new Error(`cannot resolve entry ${id}`)
34
+ },
35
+ }
36
+ }
37
+
38
+ // rawDisabled:options.disabled 原始值(默认 undefined);disabled:entry.disabled
39
+ // getter 求值结果;updateLog:entry.update 调用记录(禁用/回滚断言点);
40
+ // exitOnDisable:update(disabled:true) 时模拟宿主「禁用 → fiber 撤清」;
41
+ // updateThrows:update 抛错形态(接线层回滚场景)
42
+ function entryOf(id, { disabled = false, running = false, disabledThrows = false, rawDisabled, updateLog, updateThrows = false, exitOnDisable = true } = {}) {
43
+ const entry = {
44
+ __id: id,
45
+ options: { disabled: rawDisabled },
46
+ get disabled() {
47
+ if (disabledThrows) throw new Error('bad !!js expression')
48
+ return entry.options.disabled === true
49
+ },
50
+ fiber: running ? { uid: 1 } : undefined,
51
+ update: async (patch, quiet, force) => {
52
+ if (updateThrows) throw new Error('update rejected')
53
+ updateLog?.push({ id, patch, quiet, force })
54
+ if (patch?.disabled === true) {
55
+ entry.options.disabled = true
56
+ if (exitOnDisable) entry.fiber = undefined
57
+ }
58
+ if (patch?.disabled === false) entry.options.disabled = false
59
+ return undefined
60
+ },
61
+ }
62
+ return entry
63
+ }
64
+
65
+ // ── 探测层 ────────────────────────────────────────────────────────────────
66
+
67
+ test('探测: loader 缺失或行不存在,按缺席处理(rawDisabled 归一为已禁)', () => {
68
+ const state = officialRowState(undefined, TOOL_PWSH)
69
+ assert.equal(state.present, false)
70
+ assert.equal(state.rawDisabled, true)
71
+ const chain = officialChainState({ resolve: () => { throw new Error('gone') } })
72
+ assert.deepEqual(Object.keys(chain), OFFICIAL_ROW_IDS)
73
+ assert.ok(Object.values(chain).every((row) => row.present === false))
74
+ })
75
+
76
+ test('探测: 三字段映射(disabled 读宿主 getter,rawDisabled 读 options 原值)', () => {
77
+ const chain = officialChainState(loaderOf([entryOf(TOOL_PWSH, { rawDisabled: true, disabled: true })]))
78
+ assert.equal(chain[TOOL_PWSH].disabled, true)
79
+ assert.equal(chain[TOOL_PWSH].rawDisabled, true)
80
+ assert.equal(chain[PWSH_SANDBOX].present, false)
81
+ })
82
+
83
+ test('探测: disabled getter 抛错 → disabledUnreliable(rawDisabled 落 undefined 不误判)', () => {
84
+ const state = officialRowState(loaderOf([entryOf(TOOL_PWSH, { disabledThrows: true })]), TOOL_PWSH)
85
+ assert.equal(state.disabled, false)
86
+ assert.equal(state.disabledUnreliable, true)
87
+ })
88
+
89
+ // ── 决策层(S-1 ~ S-8)───────────────────────────────────────────────────
90
+
91
+ test('S-1 双行全禁停稳 → takeover', () => {
92
+ const chain = officialChainState(loaderOf([
93
+ entryOf(TOOL_PWSH, { rawDisabled: true, disabled: true }),
94
+ entryOf(PWSH_SANDBOX, { rawDisabled: true, disabled: true }),
95
+ ]))
96
+ assert.deepEqual(takeoverDecision(chain), { decision: 'takeover', targets: [] })
97
+ })
98
+
99
+ test('S-2 禁用但仍在退场 → await-exit(单行/双行)', () => {
100
+ const chain = officialChainState(loaderOf([
101
+ entryOf(TOOL_PWSH, { rawDisabled: true, disabled: true }),
102
+ entryOf(PWSH_SANDBOX, { rawDisabled: true, disabled: true, running: true }),
103
+ ]))
104
+ assert.deepEqual(takeoverDecision(chain), { decision: 'await-exit', targets: [] })
105
+ const chain2 = officialChainState(loaderOf([
106
+ entryOf(TOOL_PWSH, { rawDisabled: true, disabled: true, running: true }),
107
+ entryOf(PWSH_SANDBOX, { rawDisabled: true, disabled: true, running: true }),
108
+ ]))
109
+ assert.equal(takeoverDecision(chain2).decision, 'await-exit')
110
+ })
111
+
112
+ test('S-3 平台默认 false(宿主启用态)与 undefined 同为接管空间:runtime-disable', () => {
113
+ // dsh-base 平台表达式在 win32 求值 false,与用户显式启用不可区分,
114
+ // 不作为让位信号(gateway 官方行无 disabled 声明,shell-select 官方行有)
115
+ const tool = entryOf(TOOL_PWSH, { rawDisabled: false })
116
+ const sandbox = entryOf(PWSH_SANDBOX, { rawDisabled: false })
117
+ const chain = officialChainState(loaderOf([tool, sandbox]))
118
+ const { decision, targets } = takeoverDecision(chain)
119
+ assert.equal(decision, 'runtime-disable')
120
+ assert.deepEqual(targets, [tool, sandbox])
121
+ })
122
+
123
+ test('S-4 双行默认启用 → runtime-disable 双目标', () => {
124
+ const tool = entryOf(TOOL_PWSH)
125
+ const sandbox = entryOf(PWSH_SANDBOX)
126
+ const chain = officialChainState(loaderOf([tool, sandbox]))
127
+ const { decision, targets } = takeoverDecision(chain)
128
+ assert.equal(decision, 'runtime-disable')
129
+ assert.deepEqual(targets, [tool, sandbox])
130
+ })
131
+
132
+ test('S-5 混合态(0.1.7 常态:tool-pwsh 宿主已禁 + pwsh-sandbox 默认)→ runtime-disable 仅 sandbox', () => {
133
+ const sandbox = entryOf(PWSH_SANDBOX)
134
+ const chain = officialChainState(loaderOf([
135
+ entryOf(TOOL_PWSH, { rawDisabled: true, disabled: true }),
136
+ sandbox,
137
+ ]))
138
+ const { decision, targets } = takeoverDecision(chain)
139
+ assert.equal(decision, 'runtime-disable')
140
+ assert.deepEqual(targets, [sandbox])
141
+ })
142
+
143
+ test('S-6 任一行 getter 抛错 → yield(双行/混合/显式 false 均让位)', () => {
144
+ for (const broken of [TOOL_PWSH, PWSH_SANDBOX]) {
145
+ const other = broken === TOOL_PWSH ? PWSH_SANDBOX : TOOL_PWSH
146
+ const chain = officialChainState(loaderOf([
147
+ entryOf(broken, { disabledThrows: true }),
148
+ entryOf(other),
149
+ ]))
150
+ assert.equal(takeoverDecision(chain).decision, 'yield')
151
+ }
152
+ })
153
+
154
+ test('S-7 缺席行视为已禁;全缺席 → takeover;缺席+默认 → 仅禁在场行', () => {
155
+ const absentChain = officialChainState({ resolve: () => { throw new Error('gone') } })
156
+ assert.deepEqual(takeoverDecision(absentChain), { decision: 'takeover', targets: [] })
157
+ const sandbox = entryOf(PWSH_SANDBOX)
158
+ const mixed = officialChainState(loaderOf([sandbox]))
159
+ const { decision, targets } = takeoverDecision(mixed)
160
+ assert.equal(decision, 'runtime-disable')
161
+ assert.deepEqual(targets, [sandbox])
162
+ })
163
+
164
+ test('S-8 options.disabled 为 !!js 表达式包装对象(真机形态)→ 按 getter 求值决策,不误让位', () => {
165
+ const expr = { __jsExpr: "process.platform !== 'win32'" }
166
+ const tool = entryOf(TOOL_PWSH, { rawDisabled: expr })
167
+ const sandbox = entryOf(PWSH_SANDBOX, { rawDisabled: expr })
168
+ const chain = officialChainState(loaderOf([tool, sandbox]))
169
+ const { decision, targets } = takeoverDecision(chain)
170
+ assert.equal(decision, 'runtime-disable')
171
+ assert.deepEqual(targets, [tool, sandbox])
172
+ })
173
+
174
+ // ── 原语层 ────────────────────────────────────────────────────────────────
175
+
176
+ test('runtimeDisableRows:逐行 force update;单行失败不中止其余行', async () => {
177
+ const log = []
178
+ const good = entryOf(TOOL_PWSH, { updateLog: log })
179
+ const bad = entryOf(PWSH_SANDBOX, { updateLog: log, updateThrows: true })
180
+ const { disabled, failed } = await runtimeDisableRows([good, bad])
181
+ assert.deepEqual(disabled, [good])
182
+ assert.deepEqual(failed, [bad])
183
+ assert.deepEqual(log.map((call) => call.id), [TOOL_PWSH])
184
+ assert.deepEqual(log[0].patch, { disabled: true })
185
+ assert.equal(log[0].quiet, false)
186
+ assert.equal(log[0].force, true)
187
+ })
188
+
189
+ test('rollbackDisabledRows:update {disabled:false} quiet,失败吞掉不抛', async () => {
190
+ const log = []
191
+ const good = entryOf(TOOL_PWSH, { rawDisabled: true, disabled: true, updateLog: log })
192
+ const bad = entryOf(PWSH_SANDBOX, { rawDisabled: true, disabled: true, updateThrows: true })
193
+ await rollbackDisabledRows([good, bad])
194
+ assert.deepEqual(log, [{ id: TOOL_PWSH, patch: { disabled: false }, quiet: true, force: true }])
195
+ })
196
+
197
+ test('awaitOfficialExit:双行全退场(含收尾间隔)→ true;退场卡死 → false', async () => {
198
+ const delays = []
199
+ const slowDelay = async (ms) => { delays.push(ms) }
200
+ const exited = entryOf(TOOL_PWSH, { rawDisabled: true, disabled: true })
201
+ const exited2 = entryOf(PWSH_SANDBOX, { rawDisabled: true, disabled: true })
202
+ assert.equal(await awaitOfficialExit([exited, exited2], { delay: slowDelay }), true)
203
+ assert.deepEqual(delays, [EXIT_POLL_INTERVAL_MS])
204
+ const stuck = entryOf(TOOL_PWSH, { rawDisabled: true, disabled: true, running: true })
205
+ assert.equal(await awaitOfficialExit([stuck], { rounds: 3, delay: slowDelay }), false)
206
+ assert.equal(delays.length, 4)
207
+ })
208
+
209
+ // ── 接线层(W-1 ~ W-7):主行 apply 桩测 ─────────────────────────────────
210
+
211
+ function stubCtxOf(entries, { updateThrows = false, pluginError } = {}) {
212
+ const log = { warns: [], errors: [], infos: [], plugins: [], listeners: [] }
213
+ const ctx = {
214
+ loader: loaderOf(entries),
215
+ logger: {
216
+ info: (msg) => log.infos.push(msg),
217
+ warn: (msg) => log.warns.push(msg),
218
+ error: (msg) => log.errors.push(msg),
219
+ },
220
+ plugin: async (module, config) => {
221
+ if (pluginError) throw pluginError
222
+ log.plugins.push({ module, config })
223
+ // state=2 对齐宿主 cordis 激活态(挂载校验据它判定激活成功)
224
+ return { dispose: async () => {}, state: 2 }
225
+ },
226
+ on: (event, listener) => {
227
+ log.listeners.push({ event, listener })
228
+ return () => {}
229
+ },
230
+ effect: (fn) => fn(),
231
+ }
232
+ return { ctx, log }
233
+ }
234
+
235
+ // 异步接管序列的落定等待:序列内含退场收尾真实 setTimeout(20ms 级),
236
+ // 轮询至断言条件成立或上界(防序列异常时测试挂死)
237
+ async function untilSettled(probe, rounds = 50) {
238
+ for (let round = 0; round < rounds; round += 1) {
239
+ if (probe()) return true
240
+ await new Promise((resolve) => setTimeout(resolve, 10))
241
+ }
242
+ return probe()
243
+ }
244
+
245
+ test('W-7 POSIX 早退:inactive 旗标,零 update 零挂载', async () => {
246
+ const realPlatform = process.platform
247
+ Object.defineProperty(process, 'platform', { value: 'linux' })
248
+ try {
249
+ const tool = entryOf(TOOL_PWSH)
250
+ const sandbox = entryOf(PWSH_SANDBOX)
251
+ const { ctx, log } = stubCtxOf([tool, sandbox])
252
+ await mainApply(ctx, {})
253
+ assert.equal(log.plugins.length, 0)
254
+ assert.equal(tool.options.disabled, undefined)
255
+ assert.equal(sandbox.options.disabled, undefined)
256
+ } finally {
257
+ Object.defineProperty(process, 'platform', { value: realPlatform })
258
+ }
259
+ })
260
+
261
+ test('W-1 runtime-disable 全链:落定后双行禁用(quiet=false,force=true)→ 退场 → 挂执行器', async () => {
262
+ const realPlatform = process.platform
263
+ Object.defineProperty(process, 'platform', { value: 'win32' })
264
+ try {
265
+ const updateLog = []
266
+ const tool = entryOf(TOOL_PWSH, { updateLog })
267
+ const sandbox = entryOf(PWSH_SANDBOX, { updateLog })
268
+ const { ctx, log } = stubCtxOf([tool, sandbox])
269
+ await mainApply(ctx, { shells: [] })
270
+ await untilSettled(() => log.plugins.length > 0)
271
+ assert.deepEqual(updateLog.map((call) => [call.id, call.patch.disabled, call.force]), [
272
+ [TOOL_PWSH, true, true],
273
+ [PWSH_SANDBOX, true, true],
274
+ ])
275
+ assert.equal(log.plugins.length, 1)
276
+ assert.equal(log.plugins[0].module, ShellSelectExecutor)
277
+ assert.ok(log.warns.some((msg) => msg.includes('接管')))
278
+ } finally {
279
+ Object.defineProperty(process, 'platform', { value: realPlatform })
280
+ }
281
+ })
282
+
283
+ test('W-2 update 失败:回滚已禁行,不挂载,官方保持服务', async () => {
284
+ const realPlatform = process.platform
285
+ Object.defineProperty(process, 'platform', { value: 'win32' })
286
+ try {
287
+ const updateLog = []
288
+ const good = entryOf(TOOL_PWSH, { updateLog })
289
+ const bad = entryOf(PWSH_SANDBOX, { updateLog, updateThrows: true })
290
+ const { ctx, log } = stubCtxOf([good, bad])
291
+ await mainApply(ctx, {})
292
+ await untilSettled(() => updateLog.some((call) => call.id === TOOL_PWSH && call.patch.disabled === false))
293
+ // good 已禁成功 → 回滚启用;bad 未动
294
+ const rollback = updateLog.find((call) => call.id === TOOL_PWSH && call.patch.disabled === false)
295
+ assert.ok(rollback, '回滚调用缺失')
296
+ assert.equal(rollback.quiet, true)
297
+ assert.equal(good.options.disabled, false)
298
+ assert.equal(log.plugins.length, 0)
299
+ assert.ok(log.warns.some((msg) => msg.includes('禁用失败')))
300
+ } finally {
301
+ Object.defineProperty(process, 'platform', { value: realPlatform })
302
+ }
303
+ })
304
+
305
+ test('W-4 挂载失败:官方已退场,回滚启用两行令官方复活', async () => {
306
+ const realPlatform = process.platform
307
+ Object.defineProperty(process, 'platform', { value: 'win32' })
308
+ try {
309
+ const updateLog = []
310
+ const tool = entryOf(TOOL_PWSH, { updateLog })
311
+ const sandbox = entryOf(PWSH_SANDBOX, { updateLog })
312
+ const { ctx, log } = stubCtxOf([tool, sandbox], { pluginError: new Error('service shell conflict') })
313
+ await mainApply(ctx, {})
314
+ await untilSettled(() => log.errors.length > 0)
315
+ const rollbacks = updateLog.filter((call) => call.patch.disabled === false)
316
+ assert.deepEqual(rollbacks.map((call) => call.id).sort(), [PWSH_SANDBOX, TOOL_PWSH].sort())
317
+ assert.equal(tool.options.disabled, false)
318
+ assert.equal(sandbox.options.disabled, false)
319
+ assert.equal(log.plugins.length, 0)
320
+ assert.ok(log.errors.some((msg) => msg.includes('回滚')))
321
+ } finally {
322
+ Object.defineProperty(process, 'platform', { value: realPlatform })
323
+ }
324
+ })
325
+
326
+ test('W-5 getter 抛错 → yield 零动作零挂载(安全向,官方全量服务)', async () => {
327
+ const realPlatform = process.platform
328
+ Object.defineProperty(process, 'platform', { value: 'win32' })
329
+ try {
330
+ const tool = entryOf(TOOL_PWSH, { disabledThrows: true })
331
+ const sandbox = entryOf(PWSH_SANDBOX)
332
+ const { ctx, log } = stubCtxOf([tool, sandbox])
333
+ await mainApply(ctx, {})
334
+ await untilSettled(() => log.infos.length > 0)
335
+ assert.equal(log.plugins.length, 0)
336
+ assert.equal(tool.options.disabled, undefined)
337
+ assert.equal(sandbox.options.disabled, undefined)
338
+ } finally {
339
+ Object.defineProperty(process, 'platform', { value: realPlatform })
340
+ }
341
+ })
342
+
343
+ test('W-6 takeover 直挂:官方已禁停稳,不经 update 直接挂载', async () => {
344
+ const realPlatform = process.platform
345
+ Object.defineProperty(process, 'platform', { value: 'win32' })
346
+ try {
347
+ const updateLog = []
348
+ const tool = entryOf(TOOL_PWSH, { rawDisabled: true, disabled: true, updateLog })
349
+ const sandbox = entryOf(PWSH_SANDBOX, { rawDisabled: true, disabled: true, updateLog })
350
+ const { ctx, log } = stubCtxOf([tool, sandbox])
351
+ await mainApply(ctx, {})
352
+ await new Promise((resolve) => setTimeout(resolve, 0))
353
+ assert.equal(updateLog.length, 0)
354
+ assert.equal(log.plugins.length, 1)
355
+ } finally {
356
+ Object.defineProperty(process, 'platform', { value: realPlatform })
357
+ }
358
+ })
359
+
360
+ test('W-6 await-exit:禁用行退场完成后挂载(真实 setTimeout 微延迟)', async () => {
361
+ const realPlatform = process.platform
362
+ Object.defineProperty(process, 'platform', { value: 'win32' })
363
+ try {
364
+ // 退场在 30ms 后:首轮探测仍 running,轮询兜住
365
+ const tool = entryOf(TOOL_PWSH, { rawDisabled: true, disabled: true, running: true })
366
+ const sandbox = entryOf(PWSH_SANDBOX, { rawDisabled: true, disabled: true })
367
+ setTimeout(() => { tool.fiber = undefined }, 30)
368
+ const { ctx, log } = stubCtxOf([tool, sandbox])
369
+ await mainApply(ctx, {})
370
+ assert.equal(log.plugins.length, 1)
371
+ } finally {
372
+ Object.defineProperty(process, 'platform', { value: realPlatform })
373
+ }
374
+ })
375
+
376
+ // ── volatile 陈旧问题:执行面刷新收口在 web 写路径(updateConfig 直调
377
+ // ctx.shell.refresh),主行不监听宿主事件(真实链路 volatile-only 不重挂行、
378
+ // 事件不达本包,投机监听已移除;挂载路径零监听即正确形态)──
379
+
380
+ test('apply 挂载路径零宿主事件监听(refresh 收口在 web 写路径)', async () => {
381
+ const realPlatform = process.platform
382
+ Object.defineProperty(process, 'platform', { value: 'win32' })
383
+ try {
384
+ const tool = entryOf(TOOL_PWSH, { rawDisabled: true, disabled: true })
385
+ const sandbox = entryOf(PWSH_SANDBOX, { rawDisabled: true, disabled: true })
386
+ const { ctx, log } = stubCtxOf([tool, sandbox])
387
+ await mainApply(ctx, {})
388
+ assert.equal(log.plugins.length, 1)
389
+ assert.equal(log.listeners.length, 0)
390
+ } finally {
391
+ Object.defineProperty(process, 'platform', { value: realPlatform })
392
+ }
393
+ })
394
+
395
+ // runtime 序列的退场超时形态(放弃挂载+告警降级)由 awaitOfficialExit 的 false
396
+ // 分支单测与 await-exit 超时路径共同锁定:接线层超时场景需真实耗尽 50×20ms
397
+ // 轮询上界,不在套件内重复真实等待。