@hyzyn/dsh-tty 0.24.1 → 0.26.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.
- package/README.en.md +65 -13
- package/README.md +51 -13
- package/client.js +464 -192
- package/lib/index.js +164 -9
- package/lib/index.js.map +1 -1
- package/lib/probe.js +11 -6
- package/lib/probe.js.map +1 -1
- package/lib/shell-integration.d.ts +40 -1
- package/lib/shell-integration.js +180 -102
- package/lib/shell-integration.js.map +1 -1
- package/lib/ssh-config.d.ts +1 -1
- package/lib/ssh-config.js +9 -0
- package/lib/ssh-config.js.map +1 -1
- package/lib/ssh.d.ts +32 -2
- package/lib/ssh.js +32 -12
- package/lib/ssh.js.map +1 -1
- package/lib/tmux.d.ts +13 -4
- package/lib/tmux.js +23 -15
- package/lib/tmux.js.map +1 -1
- package/lib/tunnels.d.ts +101 -0
- package/lib/tunnels.js +233 -0
- package/lib/tunnels.js.map +1 -1
- package/package.json +1 -1
- package/scripts/integration.mjs +3 -3
- package/scripts/lib/crash-trace.mjs +164 -0
- package/scripts/preview/harness.js +53 -0
- package/scripts/preview/mock-host.js +12 -1
- package/scripts/preview.mjs +60 -20
- package/scripts/windows-smoke.mjs +76 -0
package/scripts/preview.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync
|
|
|
24
24
|
import { dirname, join, resolve } from 'node:path'
|
|
25
25
|
import { fileURLToPath } from 'node:url'
|
|
26
26
|
import { homedir, tmpdir } from 'node:os'
|
|
27
|
+
import { describeChromeSearch, findChrome, playwrightChromeCandidates } from '../../../scripts/chrome-path.mjs'
|
|
27
28
|
|
|
28
29
|
const require = createRequire(import.meta.url)
|
|
29
30
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
@@ -69,6 +70,7 @@ const SCENARIOS = [
|
|
|
69
70
|
['settings-docker', '设置卡片(docker 与 tty 并排对照)'],
|
|
70
71
|
['docker-stale-book', 'docker 设置卡片:失效的连接簿引用要看得出来'],
|
|
71
72
|
['tunnel-edit', '端口转发:编辑隧道(回填 / 改端口换名 / 停用的不被启用)'],
|
|
73
|
+
['port-warn', '端口转发:保存时宿主报端口占用(警告档,不是成功也不是失败)'],
|
|
72
74
|
['sftp', 'SFTP 单窗体(挂右侧挂载位)'],
|
|
73
75
|
['sftp-dual', 'SFTP 双栏(挂右侧挂载位)'],
|
|
74
76
|
['sftp-fallback', 'SFTP 落点回退(抽屉被容器面板占用 → 对话框)'],
|
|
@@ -150,11 +152,47 @@ function skinCss() {
|
|
|
150
152
|
if (existsSync(file)) candidates.push(file)
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
|
-
|
|
155
|
+
// 基础层**始终**在前:它住在宿主的 base.css / focus.css / corner-shape.css 里,
|
|
156
|
+
// 真皮肤表里没有这些名字(实测 blue-fantasy 一条都不含)。
|
|
157
|
+
if (candidates.length > 0) return BASE_LAYER_CSS + '\n' + readFileSync(candidates[0], 'utf8')
|
|
154
158
|
log('未找到本机 skin token 表,使用内置兜底变量')
|
|
155
|
-
return FALLBACK_SKIN_CSS
|
|
159
|
+
return BASE_LAYER_CSS + '\n' + FALLBACK_SKIN_CSS
|
|
156
160
|
}
|
|
157
161
|
|
|
162
|
+
/**
|
|
163
|
+
* 宿主基础层:圆角阶梯、卡片材质、焦点环、全局超椭圆。
|
|
164
|
+
*
|
|
165
|
+
* 值取自 `@deepseek-ai/dsh-client-ui-theme` 的 `base.css` / `focus.css` / `corner-shape.css`
|
|
166
|
+
* ——**名字与值都必须与真主题逐字一致**:夹具里缺一个名字的代价是「预览是对的、只有真机是错的」
|
|
167
|
+
* (卡片改走 `--dsw-radius-xl` 后,缺了它圆角算成 0,而真机是 20px)。
|
|
168
|
+
* 真皮肤表只有「皮肤」那一层,基础层必须由这里补。
|
|
169
|
+
*
|
|
170
|
+
* 本包 client-src 引用的**每一个** --dsw-* 都必须在这里(或真皮肤表里)能解析出值:生产 CSS
|
|
171
|
+
* 已按 client-lint 检查十六去掉所有颜色字面量兜底,假主题就是「脱离宿主」时唯一的兜底。
|
|
172
|
+
*/
|
|
173
|
+
const BASE_LAYER_CSS = `
|
|
174
|
+
:root{
|
|
175
|
+
--dsw-radius-xs:4px; --dsw-radius-sm:8px; --dsw-radius-md:12px; --dsw-radius-lg:16px; --dsw-radius-xl:20px;
|
|
176
|
+
--dsw-radius-panel:28px;
|
|
177
|
+
--dsw-alias-settings-card-fill:var(--dsw-alias-bg-layer-2);
|
|
178
|
+
--dsw-alias-settings-card-stroke:var(--dsw-alias-border-l4);
|
|
179
|
+
--dsw-focus-ring-width:2px;
|
|
180
|
+
/* 宿主 focus.css 只声明宽度,颜色走 var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))
|
|
181
|
+
—— 这里按宿主那条兜底表达式把「最近语义令牌」补上,别写死某个蓝。 */
|
|
182
|
+
--dsw-focus-ring-color:var(--dsw-alias-state-business-primary);
|
|
183
|
+
}
|
|
184
|
+
/* elevation 与宿主 gradient-shadow-text.css 逐字相同,且必须声明在 body 上(不是 :root):
|
|
185
|
+
它内含 var(--dsw-elevation-stroke-color) → var(--dsw-alias-border-l4),在 :root 上求值就
|
|
186
|
+
钉死了浅色档,深色主题下描边色不会跟着走。 */
|
|
187
|
+
body, body *{
|
|
188
|
+
--dsw-elevation-stroke-color:var(--dsw-alias-border-l4);
|
|
189
|
+
--dsw-elevation-stroke:0 0 0 .5px var(--dsw-elevation-stroke-color);
|
|
190
|
+
--dsw-elevation-panel:var(--dsw-elevation-stroke), 0 3px 8px 0 #00000008, 0 0 16px 0 #00000005;
|
|
191
|
+
--dsw-elevation-prominent:var(--dsw-elevation-stroke), 0 3px 8px 0 #0000000a, 0 0 20px 0 #0000000d;
|
|
192
|
+
}
|
|
193
|
+
@supports (corner-shape:superellipse(1.5)){:root{--dsw-corner-shape:superellipse(1.5)}*,:before,:after{corner-shape:var(--dsw-corner-shape)}}
|
|
194
|
+
`
|
|
195
|
+
|
|
158
196
|
const FALLBACK_SKIN_CSS = `
|
|
159
197
|
:root {
|
|
160
198
|
--dsw-alias-bg-base: #ffffff;
|
|
@@ -180,6 +218,10 @@ const FALLBACK_SKIN_CSS = `
|
|
|
180
218
|
--dsw-alias-state-error-primary: #d1242f;
|
|
181
219
|
--dsw-alias-state-success-primary: #1a7f37;
|
|
182
220
|
--dsw-alias-state-warn-primary: #bf8700;
|
|
221
|
+
--dsw-alias-button-primary-fill: #0f1115;
|
|
222
|
+
--dsw-alias-button-primary-hover: #43454a;
|
|
223
|
+
--dsw-alias-interactive-bg-hover-solid: #f1f3f5;
|
|
224
|
+
--dsw-alias-label-primary-foreground: #fff;
|
|
183
225
|
--dsw-specific-input-major: #ffffff;
|
|
184
226
|
--dsw-specific-menu: #ffffff;
|
|
185
227
|
--dsw-specific-tip: #f6f7f9;
|
|
@@ -214,6 +256,10 @@ body[data-ds-dark-theme] {
|
|
|
214
256
|
--dsw-alias-state-error-primary: #ff6b6b;
|
|
215
257
|
--dsw-alias-state-success-primary: #4cc38a;
|
|
216
258
|
--dsw-alias-state-warn-primary: #e0a94a;
|
|
259
|
+
--dsw-alias-button-primary-fill: #f9fafb;
|
|
260
|
+
--dsw-alias-button-primary-hover: #ebeef2;
|
|
261
|
+
--dsw-alias-interactive-bg-hover-solid: #353638;
|
|
262
|
+
--dsw-alias-label-primary-foreground: #0f1115;
|
|
217
263
|
--dsw-specific-input-major: #17171a;
|
|
218
264
|
--dsw-specific-menu: #24242af0;
|
|
219
265
|
--dsw-specific-tip: #24242a;
|
|
@@ -240,23 +286,17 @@ function prepare() {
|
|
|
240
286
|
|
|
241
287
|
/* ------------------------------ Chrome/CDP ------------------------------ */
|
|
242
288
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
candidates.push('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome')
|
|
257
|
-
candidates.push('/Applications/Chromium.app/Contents/MacOS/Chromium')
|
|
258
|
-
for (const c of candidates) if (existsSync(c)) return c
|
|
259
|
-
throw new Error('找不到 Chrome/Chromium,请设置 CHROME_PATH')
|
|
289
|
+
/*
|
|
290
|
+
* 浏览器发现走仓库根的共享模块(`scripts/chrome-path.mjs`):此前这里抄了一份
|
|
291
|
+
* 「扫 playwright 缓存 → 退回平台候选」的实现,`packages/search` 那份与它**逐字相同**,
|
|
292
|
+
* 两份都只在 macOS 上找得到浏览器(Linux 上必然抛「找不到 Chrome/Chromium」)。
|
|
293
|
+
* playwright 缓存仍排在平台候选**之前**——缓存里那个 Chrome for Testing 版本确定,
|
|
294
|
+
* 比机器上随便装的 Chrome 更适合当截图基准(顺序经 `extraCandidates` 原样保留)。
|
|
295
|
+
*/
|
|
296
|
+
function resolveChrome() {
|
|
297
|
+
const found = findChrome({ extraCandidates: playwrightChromeCandidates })
|
|
298
|
+
if (found === null) throw new Error('找不到 Chrome/Chromium:' + describeChromeSearch())
|
|
299
|
+
return found
|
|
260
300
|
}
|
|
261
301
|
|
|
262
302
|
const WebSocketImpl = (() => {
|
|
@@ -332,7 +372,7 @@ class Cdp {
|
|
|
332
372
|
}
|
|
333
373
|
|
|
334
374
|
async function launchChrome() {
|
|
335
|
-
const chrome =
|
|
375
|
+
const chrome = resolveChrome()
|
|
336
376
|
const userDataDir = join(previewDir, 'chrome-profile')
|
|
337
377
|
rmSync(userDataDir, { recursive: true, force: true })
|
|
338
378
|
const child = spawn(chrome, [
|
|
@@ -20,19 +20,68 @@ import WebServerRuntime from '@deepseek-ai/dsh-host-webserver'
|
|
|
20
20
|
import { LocalSubprocessRuntime } from '@deepseek-ai/dsh-subprocess-local'
|
|
21
21
|
import WebSocket from 'ws'
|
|
22
22
|
import { name, inject, apply } from '../lib/index.js'
|
|
23
|
+
import { createPhaseTracer, describeCrashPoint, traceSync } from './lib/crash-trace.mjs'
|
|
23
24
|
|
|
24
25
|
if (process.platform !== 'win32') {
|
|
25
26
|
console.log(`[windows-smoke] 跳过:当前平台 ${process.platform}——本脚本只在 Windows 上跑(真实验证 cmd.exe / ConPTY 链路),CI 的 windows-latest job 会执行它`)
|
|
26
27
|
process.exit(0)
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* 阶段留痕(2026-10-04 加,起因是 CI 上那次 `0xC0000374` 原生崩溃)。
|
|
32
|
+
*
|
|
33
|
+
* **为什么不能用 `console.log`**:Node 官方规定 **Windows 上 stdout 到管道是异步的**
|
|
34
|
+
* (Linux/macOS 同步)。CI 上 stdout 正是管道,于是 `console.log('[W6] …')` 只把字节交给
|
|
35
|
+
* libuv 写队列——**进程原生崩溃那一刻队列一起没了**,日志里连 `[W6]` 都看不到,
|
|
36
|
+
* 只剩一个「什么都没说」的退出码。所以这里用 `fs.writeSync` 直写 fd 1(见 crash-trace.mjs)。
|
|
37
|
+
*
|
|
38
|
+
* **判据是配对**:`enter` 没配对 `leave` 的阶段 = 崩溃最可能的位置;若最后一条 `enter`
|
|
39
|
+
* 有对应的 `leave`,则崩溃在**下一个阶段入口之前**。2026-10-04 那次正是后者
|
|
40
|
+
* (W5 的 leave 打出来了、W6 的 enter 没打出来 ⇒ 崩在 W5 结束后、W6 入口前)。
|
|
41
|
+
*/
|
|
42
|
+
const tracer = createPhaseTracer()
|
|
43
|
+
|
|
29
44
|
/* 看门狗:任何环节卡死时留痕退出(正常路径会先 process.exit)。 */
|
|
30
45
|
const watchdog = setTimeout(() => {
|
|
46
|
+
// 卡死也是「失败」,先把诊断同步打出去(console.error 在 Windows 管道上同样会丢)
|
|
47
|
+
traceSync(describeCrashPoint({ openPhases: tracer.snapshot(), exitCode: 2 }))
|
|
31
48
|
console.error('[watchdog] 90s 看门狗触发:windows-smoke 卡死')
|
|
32
49
|
process.exit(2)
|
|
33
50
|
}, 90000)
|
|
34
51
|
watchdog.unref()
|
|
35
52
|
|
|
53
|
+
/**
|
|
54
|
+
* 原生崩溃(段错误 / 堆损坏)会**绕过**所有 JS 异常处理,`uncaughtException` 也接不到。
|
|
55
|
+
* 所以真正的保证是上面那条「每阶段同步留痕」——崩溃点 = 最后一条没配对 `leave` 的 `enter`。
|
|
56
|
+
*
|
|
57
|
+
* 下面这几层是**尽力而为**的补充,只捕获 JS 层的漏网异常(那类崩溃此前也只剩一个退出码)。
|
|
58
|
+
*/
|
|
59
|
+
let exitingNormally = false
|
|
60
|
+
|
|
61
|
+
function reportCrash(why, detail) {
|
|
62
|
+
traceSync(describeCrashPoint({ openPhases: tracer.snapshot(), exitCode: process.exitCode }))
|
|
63
|
+
traceSync(`${why}${detail ? ':' + detail : ''}`)
|
|
64
|
+
}
|
|
65
|
+
process.on('uncaughtException', (error) => {
|
|
66
|
+
exitingNormally = true // 自己报过了,别让 exit 钩子再报一遍
|
|
67
|
+
reportCrash('uncaughtException', error?.stack ?? String(error))
|
|
68
|
+
process.exit(1)
|
|
69
|
+
})
|
|
70
|
+
process.on('unhandledRejection', (reason) => {
|
|
71
|
+
exitingNormally = true
|
|
72
|
+
reportCrash('unhandledRejection', reason instanceof Error ? reason.stack : String(reason))
|
|
73
|
+
process.exit(1)
|
|
74
|
+
})
|
|
75
|
+
process.on('exit', (code) => {
|
|
76
|
+
/*
|
|
77
|
+
* 只在**异常终止**时报:正常收尾(末尾那段汇总 + flush 屏障 + exit)已经把事情说清了,
|
|
78
|
+
* 再打一段「崩溃点诊断」只会误导(断言失败被说成崩溃)。
|
|
79
|
+
* 判据用 `exitingNormally` 而不是 code:正常的失败路径 code 也是 1。
|
|
80
|
+
*/
|
|
81
|
+
if (exitingNormally) return
|
|
82
|
+
reportCrash('进程在收尾之前退出', `code=${String(code)}`)
|
|
83
|
+
})
|
|
84
|
+
|
|
36
85
|
const RESULTS = []
|
|
37
86
|
function pass(label) { RESULTS.push(['PASS', label]); console.log(' ✔ PASS ' + label) }
|
|
38
87
|
function fail(label, detail) { RESULTS.push(['FAIL', label, detail]); console.error(' ✘ FAIL ' + label + (detail ? ' — ' + detail : '')) }
|
|
@@ -117,8 +166,11 @@ async function run() {
|
|
|
117
166
|
|
|
118
167
|
// W1:spawn → ready(默认 shell 必须是 %COMSPEC%,且真的起来了)
|
|
119
168
|
console.log('\n[W1] spawn → ready(默认 shell = %COMSPEC%)')
|
|
169
|
+
tracer.enter('W1 openSession + ws 连接')
|
|
120
170
|
const s = openSession(port)
|
|
121
171
|
await s.open()
|
|
172
|
+
tracer.leave('W1 openSession + ws 连接')
|
|
173
|
+
tracer.enter('W1 spawn → ready')
|
|
122
174
|
s.client.send(JSON.stringify({ t: 'spawn', cols: 100, rows: 30 }))
|
|
123
175
|
try {
|
|
124
176
|
await s.waitFor(() => s.state.ready !== null, 20000, 'ready')
|
|
@@ -131,9 +183,11 @@ async function run() {
|
|
|
131
183
|
} catch (error) {
|
|
132
184
|
fail('W1 spawn → ready', error.message)
|
|
133
185
|
}
|
|
186
|
+
tracer.leave('W1 spawn → ready')
|
|
134
187
|
|
|
135
188
|
// W2:跑起来的是 %COMSPEC%(cmd.exe),不是 POSIX 包装层
|
|
136
189
|
console.log('\n[W2] 输入回显:跑起来的是 cmd.exe')
|
|
190
|
+
tracer.enter('W2 %COMSPEC% 回显')
|
|
137
191
|
try {
|
|
138
192
|
s.client.send(JSON.stringify({ t: 'input', d: `echo IT_SHELL_%COMSPEC%${ENTER}` }))
|
|
139
193
|
await s.waitFor(() => /IT_SHELL_.*cmd\.exe/i.test(s.state.text), 15000, 'cmd.exe 回显')
|
|
@@ -141,9 +195,11 @@ async function run() {
|
|
|
141
195
|
} catch (error) {
|
|
142
196
|
fail('W2 %COMSPEC% 生效', error.message)
|
|
143
197
|
}
|
|
198
|
+
tracer.leave('W2 %COMSPEC% 回显')
|
|
144
199
|
|
|
145
200
|
// W3:命令真的被执行(不是只回显了输入)——靠 %OS% 展开证明
|
|
146
201
|
console.log('\n[W3] 命令执行(%OS% 由 shell 展开)')
|
|
202
|
+
tracer.enter('W3 %OS% 展开')
|
|
147
203
|
try {
|
|
148
204
|
s.client.send(JSON.stringify({ t: 'input', d: `echo IT_WIN_%OS%${ENTER}` }))
|
|
149
205
|
await s.waitFor(() => /IT_WIN_Windows_NT/.test(s.state.text), 15000, '%OS% 展开')
|
|
@@ -151,9 +207,11 @@ async function run() {
|
|
|
151
207
|
} catch (error) {
|
|
152
208
|
fail('W3 命令执行生效', error.message)
|
|
153
209
|
}
|
|
210
|
+
tracer.leave('W3 %OS% 展开')
|
|
154
211
|
|
|
155
212
|
// W4:kill 帧 → exit,会话名额释放
|
|
156
213
|
console.log('\n[W4] kill → exit')
|
|
214
|
+
tracer.enter('W4 kill → exit')
|
|
157
215
|
try {
|
|
158
216
|
s.client.send(JSON.stringify({ t: 'kill' }))
|
|
159
217
|
await s.waitFor(() => s.state.exited !== null, 15000, 'exit 帧')
|
|
@@ -161,11 +219,17 @@ async function run() {
|
|
|
161
219
|
} catch (error) {
|
|
162
220
|
fail('W4 kill 帧结束会话', error.message)
|
|
163
221
|
}
|
|
222
|
+
tracer.leave('W4 kill → exit')
|
|
223
|
+
tracer.enter('W4 s.client.close()')
|
|
164
224
|
try { s.client.close() } catch { /* 已关闭 */ }
|
|
225
|
+
tracer.leave('W4 s.client.close()')
|
|
165
226
|
|
|
166
227
|
// W5:kill 之后还能再开一个(名额与清理都正确)
|
|
167
228
|
console.log('\n[W5] kill 后重新 spawn')
|
|
229
|
+
tracer.enter('W5 openSession')
|
|
168
230
|
const s2 = openSession(port)
|
|
231
|
+
tracer.leave('W5 openSession')
|
|
232
|
+
tracer.enter('W5 第二次 spawn → ready → kill → exit')
|
|
169
233
|
try {
|
|
170
234
|
await s2.open()
|
|
171
235
|
s2.client.send(JSON.stringify({ t: 'spawn', cols: 80, rows: 24 }))
|
|
@@ -176,7 +240,10 @@ async function run() {
|
|
|
176
240
|
} catch (error) {
|
|
177
241
|
fail('W5 kill 后可重新 spawn', error.message)
|
|
178
242
|
}
|
|
243
|
+
tracer.leave('W5 第二次 spawn → ready → kill → exit')
|
|
244
|
+
tracer.enter('W5 s2.client.close()')
|
|
179
245
|
try { s2.client.close() } catch { /* 已关闭 */ }
|
|
246
|
+
tracer.leave('W5 s2.client.close()')
|
|
180
247
|
|
|
181
248
|
// W6:agent 侧 `tty_send` 发**裸 LF** 也能提交命令(D74,2026-09-27 真机报告)
|
|
182
249
|
//
|
|
@@ -186,6 +253,7 @@ async function run() {
|
|
|
186
253
|
// 这里用 `%OS%` 展开做判据,理由同 W3:回显里只有 `%OS%` 字面量。
|
|
187
254
|
console.log('\n[W6] tty_send 的 \\n 归一化(真实 conhost:裸 LF 不提交命令)')
|
|
188
255
|
let agentSid = null
|
|
256
|
+
tracer.enter('W6 tty_open → tty_send → tty_capture')
|
|
189
257
|
try {
|
|
190
258
|
const open = TOOLS.get('tty_open')
|
|
191
259
|
const send = TOOLS.get('tty_send')
|
|
@@ -208,6 +276,7 @@ async function run() {
|
|
|
208
276
|
} catch (error) {
|
|
209
277
|
fail('W6 tty_send 的裸 LF 提交命令(D74)', error.message)
|
|
210
278
|
}
|
|
279
|
+
tracer.leave('W6 tty_open → tty_send → tty_capture')
|
|
211
280
|
if (agentSid !== null) {
|
|
212
281
|
try {
|
|
213
282
|
await TOOLS.get('tty_close').execute({ sid: agentSid })
|
|
@@ -222,6 +291,7 @@ async function run() {
|
|
|
222
291
|
// ConPTY 侧进程退出、句柄收尾、宿主把会话**留在表里**而不是摘掉;本地单测用的是
|
|
223
292
|
// 假 PTY,验不到「真机上退出之后 tty_capture 还读得到输出」。
|
|
224
293
|
console.log('\n[W7] 退出后的只读保留(D77)')
|
|
294
|
+
tracer.enter('W7 退出后的只读保留')
|
|
225
295
|
let retainedSid = null
|
|
226
296
|
try {
|
|
227
297
|
const open = TOOLS.get('tty_open')
|
|
@@ -270,11 +340,17 @@ async function run() {
|
|
|
270
340
|
fail('W7d tty_close 释放保留态', error.message)
|
|
271
341
|
}
|
|
272
342
|
}
|
|
343
|
+
tracer.leave('W7 退出后的只读保留')
|
|
273
344
|
}
|
|
274
345
|
|
|
275
346
|
await run()
|
|
347
|
+
// 正常收尾(无论有没有断言失败):告诉 exit 钩子「别打崩溃诊断」——那段是给
|
|
348
|
+
// **异常终止**用的,断言失败被说成「崩溃」只会误导。
|
|
349
|
+
exitingNormally = true
|
|
276
350
|
const failed = RESULTS.filter((row) => row[0] === 'FAIL')
|
|
277
351
|
console.log('\n==== Windows 冒烟:' + String(RESULTS.length - failed.length) + '/' + String(RESULTS.length) + ' PASS ====')
|
|
352
|
+
// 同步再打一份结论:万一下面的 flush 屏障之前进程就没了(原生崩溃),至少这一行一定在。
|
|
353
|
+
traceSync(`Windows 冒烟结论:${String(RESULTS.length - failed.length)}/${String(RESULTS.length)} PASS`)
|
|
278
354
|
for (const row of failed) console.error(' ✘ ' + row[1] + (row[2] ? ' — ' + row[2] : ''))
|
|
279
355
|
clearTimeout(watchdog)
|
|
280
356
|
/**
|