@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.
@@ -16,6 +16,45 @@ test('干净退出:仅 stdout,无标记', () => {
16
16
  assert.equal(text, 'hello\n')
17
17
  })
18
18
 
19
+ test('S20h shell 客户端标记:首标记行落,官方退出锚保持末行', () => {
20
+ // BDD:Given 调用经 git-bash 客户端执行且非零退出,When 渲染模型可见文本,Then 首行标记 [shell: git-bash],[exit code] 仍居末(官方 parseExitStatus 锚不被破坏)
21
+ const text = renderResult({
22
+ stdout: { text: 'out\n', truncated: false },
23
+ stderr: { text: '', truncated: false },
24
+ exitCode: 2, signal: null, timedOut: false, sandbox: undefined,
25
+ }, NO_ESCALATION, 'git-bash')
26
+ assert.equal(text, 'out\n[shell: git-bash]\n[exit code: 2]')
27
+ })
28
+
29
+ test('S20h shell 标记:干净成功仅标记行,退出锚缺省形态不变', () => {
30
+ // BDD:Given exit 0 成功调用,When 渲染,Then 输出尾追 [shell: id] 单行,无退出标记
31
+ const text = renderResult({
32
+ stdout: { text: 'hello\n', truncated: false },
33
+ stderr: { text: '', truncated: false },
34
+ exitCode: 0, signal: null, timedOut: false, sandbox: undefined,
35
+ }, NO_ESCALATION, 'pwsh')
36
+ assert.equal(text, 'hello\n[shell: pwsh]')
37
+ })
38
+
39
+ test('S20h shell 标记:先于超时/信号标记,退出锚仍居末', () => {
40
+ const text = renderResult({
41
+ stdout: { text: '', truncated: false },
42
+ stderr: { text: '', truncated: false },
43
+ exitCode: null, signal: 'SIGTERM', timedOut: true, timeoutMs: 1000, sandbox: undefined,
44
+ }, NO_ESCALATION, 'pwsh')
45
+ assert.equal(text, '(no output)\n[shell: pwsh]\n[timed out after 1000ms]\n[killed by signal: SIGTERM]')
46
+ })
47
+
48
+ test('S20h shell 标记:未传 id 不加标记(渲染面与官方逐字同构)', () => {
49
+ // BDD:Given 调用方未携带客户端事实,When 渲染,Then 文本与官方 pwsh 工具逐字一致
50
+ const text = renderResult({
51
+ stdout: { text: 'hello\n', truncated: false },
52
+ stderr: { text: '', truncated: false },
53
+ exitCode: 0, signal: null, timedOut: false, sandbox: undefined,
54
+ }, NO_ESCALATION)
55
+ assert.equal(text, 'hello\n')
56
+ })
57
+
19
58
  test('stderr 段 + 非零退出标记次序', () => {
20
59
  const text = renderResult({
21
60
  stdout: { text: 'out\n', truncated: false },
@@ -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
+ // 轮询上界,不在套件内重复真实等待。