dsh-gufa-code 0.0.0-stage → 1.0.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.
- package/LICENSE +21 -0
- package/README.md +93 -2
- package/client.js +11548 -0
- package/cordis.patch.yml +8 -0
- package/docs/icon-180.png +0 -0
- package/docs/icon.svg +18 -0
- package/docs/market-submission.md +32 -0
- package/docs/screenshot-ide.jpg +0 -0
- package/host/assets.js +31 -0
- package/host/chat.js +523 -0
- package/host/context.js +438 -0
- package/host/dev.js +35 -0
- package/host/fs.js +306 -0
- package/host/git.js +532 -0
- package/host/project.js +305 -0
- package/host/ssh.js +54 -0
- package/host/terminal-pty.js +237 -0
- package/host/terminal.js +947 -0
- package/icon.svg +18 -0
- package/index.js +63 -0
- package/locale/en.json +121 -0
- package/locale/zh.json +121 -0
- package/package.json +67 -4
- package/screenshots.json +3 -0
- package/scripts/build-client.mjs +38 -0
- package/scripts/selftest-client.mjs +457 -0
- package/scripts/selftest-host.mjs +197 -0
- package/scripts/selftest-http.mjs +284 -0
- package/scripts/selftest-router.mjs +101 -0
- package/scripts/selftest-terminal-client.mjs +314 -0
- package/scripts/selftest-terminal-pty.mjs +190 -0
- package/scripts/selftest-terminal.mjs +351 -0
- package/scripts/test-chat-render.mjs +702 -0
- package/scripts/test-chat.mjs +761 -0
- package/scripts/test-editor.mjs +516 -0
- package/scripts/verify-workspace-sessions.mjs +135 -0
- package/src/client/00-core.js +1686 -0
- package/src/client/10-icons.js +205 -0
- package/src/client/20-editor.js +1318 -0
- package/src/client/30-terminal.js +1510 -0
- package/src/client/40-chat.js +1184 -0
- package/src/client/46-gitgraph.js +278 -0
- package/src/client/50-panels.js +916 -0
- package/src/client/55-backdrop.js +508 -0
- package/src/client/60-dock.js +662 -0
- package/src/client/85-shortcuts.js +516 -0
- package/src/client/90-app.js +2272 -0
- package/src/client/99-main.js +449 -0
- package/vendor/monaco/LICENSE +21 -0
- package/vendor/monaco/ThirdPartyNotices.txt +448 -0
- package/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- package/vendor/monaco/vs/base/worker/workerMain.js +31 -0
- package/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- package/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- package/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- package/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- package/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- package/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- package/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- package/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- package/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- package/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- package/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- package/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- package/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- package/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- package/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- package/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- package/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- package/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- package/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- package/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- package/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- package/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- package/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- package/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- package/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- package/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- package/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- package/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- package/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- package/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- package/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- package/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- package/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- package/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- package/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- package/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- package/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- package/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- package/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- package/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- package/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- package/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- package/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- package/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- package/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- package/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- package/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- package/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- package/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- package/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- package/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- package/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- package/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- package/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- package/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- package/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- package/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- package/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- package/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- package/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- package/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- package/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- package/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- package/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- package/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- package/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- package/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- package/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- package/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- package/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- package/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- package/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- package/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- package/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- package/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- package/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- package/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
- package/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- package/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- package/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- package/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- package/vendor/monaco/vs/editor/editor.main.css +8 -0
- package/vendor/monaco/vs/editor/editor.main.js +798 -0
- package/vendor/monaco/vs/language/css/cssMode.js +13 -0
- package/vendor/monaco/vs/language/css/cssWorker.js +77 -0
- package/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- package/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
- package/vendor/monaco/vs/language/json/jsonMode.js +19 -0
- package/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
- package/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- package/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
- package/vendor/monaco/vs/loader.js +11 -0
- package/vendor/monaco/vs/nls.messages.de.js +21 -0
- package/vendor/monaco/vs/nls.messages.es.js +21 -0
- package/vendor/monaco/vs/nls.messages.fr.js +19 -0
- package/vendor/monaco/vs/nls.messages.it.js +19 -0
- package/vendor/monaco/vs/nls.messages.ja.js +21 -0
- package/vendor/monaco/vs/nls.messages.ko.js +19 -0
- package/vendor/monaco/vs/nls.messages.ru.js +21 -0
- package/vendor/monaco/vs/nls.messages.zh-cn.js +21 -0
- package/vendor/monaco/vs/nls.messages.zh-tw.js +19 -0
- package/vendor/xterm/LICENSE +21 -0
- package/vendor/xterm/LICENSE.addon-fit +19 -0
- package/vendor/xterm/addon-fit.js +2 -0
- package/vendor/xterm/xterm.css +285 -0
- package/vendor/xterm/xterm.js +2 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 客户端终端零件静态自检:不依赖浏览器 / React / 构建产物。
|
|
4
|
+
*
|
|
5
|
+
* 做法:
|
|
6
|
+
* 1. 把 00-core.js + 10-icons.js + 30-terminal.js 按 build-client.mjs 的 factory 头拼接,
|
|
7
|
+
* 在 vm 沙箱里执行(只拼接、不写 client.js),验证零件可组合、G.TerminalPane 注册成功;
|
|
8
|
+
* 2. 用最小 hooks 运行时调用 G.TerminalPane / GufaTerminalTab / GufaTerminalView,
|
|
9
|
+
* 展开渲染树做冒烟测试(白屏/未知组件/条件调用 hooks 这类错误会在这里暴露);
|
|
10
|
+
* 3. 关键行为静态断言(xterm 资产地址、WS 地址、业务总线、无 import/export 等)。
|
|
11
|
+
*
|
|
12
|
+
* 用法:node scripts/selftest-terminal-client.mjs
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync } from 'node:fs'
|
|
15
|
+
import { dirname, join, resolve } from 'node:path'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
17
|
+
import vm from 'node:vm'
|
|
18
|
+
|
|
19
|
+
let passed = 0
|
|
20
|
+
let failed = 0
|
|
21
|
+
function check(name, condition, detail) {
|
|
22
|
+
if (condition) {
|
|
23
|
+
passed += 1
|
|
24
|
+
console.log(` ok ${name}`)
|
|
25
|
+
} else {
|
|
26
|
+
failed += 1
|
|
27
|
+
console.error(` FAIL ${name}${detail === undefined ? '' : ` — ${detail}`}`)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
32
|
+
const readPart = (name) => readFileSync(join(packageRoot, 'src', 'client', name), 'utf8')
|
|
33
|
+
|
|
34
|
+
/* ── 1. 源码静态断言 ───────────────────────────────────────────────────── */
|
|
35
|
+
const terminalSource = readPart('30-terminal.js')
|
|
36
|
+
console.log('30-terminal.js 静态断言')
|
|
37
|
+
check('无 import/export(零件共享 factory 作用域)', !/^\s*(?:import|export)\s/m.test(terminalSource))
|
|
38
|
+
check('xterm.js 走 G.asset', terminalSource.includes("G.asset('xterm/xterm.js')"))
|
|
39
|
+
check('xterm.css 走 G.asset', terminalSource.includes("G.asset('xterm/xterm.css')"))
|
|
40
|
+
check('addon-fit 走 G.asset', terminalSource.includes("G.asset('xterm/addon-fit.js')"))
|
|
41
|
+
check('输出走直连 SSE(票据鉴权)', terminalSource.includes('gufa-code/terminal-stream') && terminalSource.includes('terminal.wsTicket'))
|
|
42
|
+
check('输入走 HTTP terminal.input', terminalSource.includes("G.api('terminal.input'"))
|
|
43
|
+
check('尺寸走 HTTP terminal.resize', terminalSource.includes("G.api('terminal.resize'"))
|
|
44
|
+
check('不再使用 WebSocket', !terminalSource.includes('new WebSocket('))
|
|
45
|
+
check('监听 G.bus terminal:send', terminalSource.includes("G.bus.on('terminal:send'"))
|
|
46
|
+
check('未直接 append 到 document.body', !/document\.body\.append/.test(terminalSource))
|
|
47
|
+
check('未修改 props.height', !/props\.height\s*=(?!=)/.test(terminalSource))
|
|
48
|
+
check('注册 G.TerminalPane', /G\.TerminalPane\s*=\s*function\s+TerminalPane/.test(terminalSource))
|
|
49
|
+
|
|
50
|
+
/* ── 2. 最小 React hooks 运行时 ────────────────────────────────────────── */
|
|
51
|
+
let currentHooks = null
|
|
52
|
+
function makeHooks() {
|
|
53
|
+
const hooks = { states: [], refs: [], effects: [], index: 0 }
|
|
54
|
+
currentHooks = hooks
|
|
55
|
+
return hooks
|
|
56
|
+
}
|
|
57
|
+
const Fragment = Symbol('Fragment')
|
|
58
|
+
const ReactStub = {
|
|
59
|
+
Fragment,
|
|
60
|
+
createElement(type, props, ...children) {
|
|
61
|
+
const merged = { ...(props === null || props === undefined ? {} : props) }
|
|
62
|
+
if (children.length === 1) merged.children = children[0]
|
|
63
|
+
else if (children.length > 1) merged.children = children
|
|
64
|
+
return { type, props: merged }
|
|
65
|
+
},
|
|
66
|
+
useState(initial) {
|
|
67
|
+
const index = currentHooks.index
|
|
68
|
+
currentHooks.index += 1
|
|
69
|
+
if (!(index in currentHooks.states)) currentHooks.states[index] = typeof initial === 'function' ? initial() : initial
|
|
70
|
+
return [currentHooks.states[index], (next) => {
|
|
71
|
+
currentHooks.states[index] = typeof next === 'function' ? next(currentHooks.states[index]) : next
|
|
72
|
+
}]
|
|
73
|
+
},
|
|
74
|
+
useRef(initial) {
|
|
75
|
+
const index = currentHooks.index
|
|
76
|
+
currentHooks.index += 1
|
|
77
|
+
if (!(index in currentHooks.refs)) currentHooks.refs[index] = { current: initial }
|
|
78
|
+
return currentHooks.refs[index]
|
|
79
|
+
},
|
|
80
|
+
useEffect(fn) {
|
|
81
|
+
currentHooks.index += 1
|
|
82
|
+
currentHooks.effects.push(fn)
|
|
83
|
+
},
|
|
84
|
+
useLayoutEffect(fn) {
|
|
85
|
+
currentHooks.index += 1
|
|
86
|
+
currentHooks.effects.push(fn)
|
|
87
|
+
},
|
|
88
|
+
useCallback(fn) {
|
|
89
|
+
currentHooks.index += 1
|
|
90
|
+
return fn
|
|
91
|
+
},
|
|
92
|
+
useMemo(fn) {
|
|
93
|
+
currentHooks.index += 1
|
|
94
|
+
return fn()
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 以独立 hooks 实例渲染一个组件,返回 { vnode, hookCount }。 */
|
|
99
|
+
function renderComponent(component, props) {
|
|
100
|
+
const hooks = makeHooks()
|
|
101
|
+
let vnode
|
|
102
|
+
try {
|
|
103
|
+
vnode = component(props)
|
|
104
|
+
} finally {
|
|
105
|
+
currentHooks = null
|
|
106
|
+
}
|
|
107
|
+
return { vnode, hookCount: hooks.index, hooks }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 展开渲染树:宿主编元素/文本直接消费,函数组件递归渲染(用于发现未知组件/渲染异常)。 */
|
|
111
|
+
function expand(vnode, depth = 0) {
|
|
112
|
+
if (depth > 80) throw new Error('渲染树过深(疑似组件递归)')
|
|
113
|
+
if (vnode === null || vnode === undefined || typeof vnode === 'boolean') return []
|
|
114
|
+
if (typeof vnode === 'string' || typeof vnode === 'number') return []
|
|
115
|
+
if (Array.isArray(vnode)) {
|
|
116
|
+
const list = []
|
|
117
|
+
for (const item of vnode) list.push(...expand(item, depth + 1))
|
|
118
|
+
return list
|
|
119
|
+
}
|
|
120
|
+
if (typeof vnode !== 'object') return []
|
|
121
|
+
const type = vnode.type
|
|
122
|
+
const props = vnode.props === null || vnode.props === undefined ? {} : vnode.props
|
|
123
|
+
const children = props.children
|
|
124
|
+
if (typeof type === 'string' || type === Fragment) {
|
|
125
|
+
if (Array.isArray(children)) {
|
|
126
|
+
const list = []
|
|
127
|
+
for (const child of children) list.push(...expand(child, depth + 1))
|
|
128
|
+
return list
|
|
129
|
+
}
|
|
130
|
+
return expand(children, depth + 1)
|
|
131
|
+
}
|
|
132
|
+
if (typeof type === 'function') {
|
|
133
|
+
const rendered = renderComponent(type, props)
|
|
134
|
+
return expand(rendered.vnode, depth + 1)
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`未知 vnode type: ${String(type)}`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* ── 3. 在沙箱中执行拼接后的 factory ───────────────────────────────────── */
|
|
140
|
+
const parts = ['00-core.js', '10-icons.js', '30-terminal.js']
|
|
141
|
+
const header = `window.__ModuleLoader__.load({
|
|
142
|
+
id: 'dsh-gufa-code',
|
|
143
|
+
factory(require) {
|
|
144
|
+
const React = require('react')
|
|
145
|
+
const h = React.createElement
|
|
146
|
+
const { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect } = React
|
|
147
|
+
`
|
|
148
|
+
const footer = `
|
|
149
|
+
;globalThis.__GUFA_TEST__ = { G, GufaTerminalView, GufaTerminalTab }
|
|
150
|
+
return { inject: [], apply() {} }
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
`
|
|
154
|
+
const source = header + parts.map((name) => `\n/* ── ${name} ── */\n${readPart(name)}\n`).join('') + footer
|
|
155
|
+
|
|
156
|
+
function fakeElement(tagName) {
|
|
157
|
+
return {
|
|
158
|
+
tagName,
|
|
159
|
+
dataset: {},
|
|
160
|
+
style: {},
|
|
161
|
+
children: [],
|
|
162
|
+
append() {},
|
|
163
|
+
remove() {},
|
|
164
|
+
setAttribute() {},
|
|
165
|
+
addEventListener() {},
|
|
166
|
+
removeEventListener() {},
|
|
167
|
+
querySelector() {
|
|
168
|
+
return null
|
|
169
|
+
},
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const documentStub = {
|
|
173
|
+
querySelector() {
|
|
174
|
+
return null
|
|
175
|
+
},
|
|
176
|
+
createElement(tagName) {
|
|
177
|
+
return fakeElement(tagName)
|
|
178
|
+
},
|
|
179
|
+
head: { append() {} },
|
|
180
|
+
body: { hasAttribute() { return false }, append() {} },
|
|
181
|
+
addEventListener() {},
|
|
182
|
+
removeEventListener() {},
|
|
183
|
+
}
|
|
184
|
+
const windowStub = {
|
|
185
|
+
__ModuleLoader__: {
|
|
186
|
+
load(module) {
|
|
187
|
+
windowStub.__loaded = module
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
addEventListener() {},
|
|
191
|
+
removeEventListener() {},
|
|
192
|
+
requestAnimationFrame() {
|
|
193
|
+
return 0
|
|
194
|
+
},
|
|
195
|
+
cancelAnimationFrame() {},
|
|
196
|
+
}
|
|
197
|
+
const sandbox = {
|
|
198
|
+
window: windowStub,
|
|
199
|
+
document: documentStub,
|
|
200
|
+
location: { protocol: 'http:', host: '127.0.0.1:53091' },
|
|
201
|
+
console,
|
|
202
|
+
setTimeout,
|
|
203
|
+
clearTimeout,
|
|
204
|
+
TextEncoder,
|
|
205
|
+
URL,
|
|
206
|
+
MutationObserver: class {
|
|
207
|
+
observe() {}
|
|
208
|
+
disconnect() {}
|
|
209
|
+
},
|
|
210
|
+
localStorage: {
|
|
211
|
+
getItem() {
|
|
212
|
+
return null
|
|
213
|
+
},
|
|
214
|
+
setItem() {},
|
|
215
|
+
removeItem() {},
|
|
216
|
+
},
|
|
217
|
+
}
|
|
218
|
+
sandbox.globalThis = sandbox
|
|
219
|
+
vm.createContext(sandbox)
|
|
220
|
+
|
|
221
|
+
let loadedModule
|
|
222
|
+
try {
|
|
223
|
+
vm.runInContext(source, sandbox, { filename: 'dsh-gufa-code.selftest.js' })
|
|
224
|
+
loadedModule = windowStub.__loaded
|
|
225
|
+
} catch (error) {
|
|
226
|
+
failed += 1
|
|
227
|
+
console.error(` FAIL 拼接执行失败 — ${error instanceof Error ? error.stack : String(error)}`)
|
|
228
|
+
}
|
|
229
|
+
console.log('拼接 factory / 组件冒烟')
|
|
230
|
+
check('factory 已通过 __ModuleLoader__.load 注册', loadedModule !== undefined)
|
|
231
|
+
if (loadedModule !== undefined) {
|
|
232
|
+
let plugin
|
|
233
|
+
try {
|
|
234
|
+
plugin = loadedModule.factory((name) => {
|
|
235
|
+
if (name === 'react') return ReactStub
|
|
236
|
+
throw new Error(`未预期的 require(${name})`)
|
|
237
|
+
})
|
|
238
|
+
} catch (error) {
|
|
239
|
+
console.error(' FAIL factory 执行失败', error)
|
|
240
|
+
}
|
|
241
|
+
check('factory 返回 { inject, apply }', plugin !== undefined && Array.isArray(plugin.inject) && typeof plugin.apply === 'function')
|
|
242
|
+
const exposed = sandbox.__GUFA_TEST__
|
|
243
|
+
check('G 命名空间暴露给测试', exposed !== undefined && typeof exposed.G === 'object')
|
|
244
|
+
if (exposed !== undefined) {
|
|
245
|
+
const G = exposed.G
|
|
246
|
+
check('G.TerminalPane 是函数组件', typeof G.TerminalPane === 'function')
|
|
247
|
+
check('GufaTerminalTab / GufaTerminalView 已定义', typeof exposed.GufaTerminalTab === 'function' && typeof exposed.GufaTerminalView === 'function')
|
|
248
|
+
check('G.translate 可用', typeof G.t === 'function' && G.t('terminal.title') !== '')
|
|
249
|
+
|
|
250
|
+
// G.TerminalPane:无项目 / 有项目但 xterm 加载中
|
|
251
|
+
for (const scenario of [
|
|
252
|
+
{ name: '无项目', props: { root: null, visible: true, height: 220, theme: 'dark', onToggleHeight() {} } },
|
|
253
|
+
{ name: '亮色主题 + 项目 root(资产加载中)', props: { root: '/tmp/gufa-demo', visible: true, height: 300, theme: 'light', onToggleHeight() {} } },
|
|
254
|
+
]) {
|
|
255
|
+
try {
|
|
256
|
+
const first = renderComponent(G.TerminalPane, scenario.props)
|
|
257
|
+
const elements = expand(first.vnode)
|
|
258
|
+
check(`G.TerminalPane 渲染:${scenario.name}`, first.vnode !== undefined && first.vnode !== null)
|
|
259
|
+
const second = renderComponent(G.TerminalPane, scenario.props)
|
|
260
|
+
check(`G.TerminalPane hook 数量稳定:${scenario.name}`, first.hookCount === second.hookCount, `${first.hookCount} vs ${second.hookCount}`)
|
|
261
|
+
expand(second.vnode)
|
|
262
|
+
check(`G.TerminalPane 渲染树非空:${scenario.name}`, elements.length >= 0)
|
|
263
|
+
} catch (error) {
|
|
264
|
+
check(`G.TerminalPane 渲染:${scenario.name}`, false, error instanceof Error ? error.stack : String(error))
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 终端标签
|
|
269
|
+
try {
|
|
270
|
+
const session = { id: 'term-1', title: 'zsh', exited: false, exitCode: null }
|
|
271
|
+
const rendered = renderComponent(exposed.GufaTerminalTab, { session, active: true, onSelect() {}, onClose() {} })
|
|
272
|
+
expand(rendered.vnode)
|
|
273
|
+
check('GufaTerminalTab 渲染', rendered.vnode !== undefined && rendered.vnode !== null)
|
|
274
|
+
const exited = renderComponent(exposed.GufaTerminalTab, { session: { ...session, exited: true }, active: false, onSelect() {}, onClose() {} })
|
|
275
|
+
expand(exited.vnode)
|
|
276
|
+
check('GufaTerminalTab 渲染(已退出)', exited.vnode !== undefined && exited.vnode !== null)
|
|
277
|
+
check('GufaTerminalTab 不调用 hooks', rendered.hookCount === 0 && exited.hookCount === 0)
|
|
278
|
+
} catch (error) {
|
|
279
|
+
check('GufaTerminalTab 渲染', false, error instanceof Error ? error.stack : String(error))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 单个终端视图:xterm 资产失败 → 纯文本降级 UI;以及已退出状态
|
|
283
|
+
const failedAssets = { status: 'failed', Terminal: null, FitAddon: null, error: '模拟 xterm 加载失败' }
|
|
284
|
+
for (const scenario of [
|
|
285
|
+
{ name: '降级模式(xterm 加载失败)', session: { id: 'term-1', title: 'zsh', exited: false }, assets: failedAssets },
|
|
286
|
+
{ name: '已退出的会话', session: { id: 'term-1', title: 'zsh', exited: true, exitCode: 1 }, assets: failedAssets },
|
|
287
|
+
]) {
|
|
288
|
+
try {
|
|
289
|
+
const props = {
|
|
290
|
+
session: scenario.session,
|
|
291
|
+
root: '/tmp/gufa-demo',
|
|
292
|
+
active: true,
|
|
293
|
+
visible: true,
|
|
294
|
+
theme: 'dark',
|
|
295
|
+
assets: scenario.assets,
|
|
296
|
+
register() {},
|
|
297
|
+
unregister() {},
|
|
298
|
+
onExit() {},
|
|
299
|
+
onTitle() {},
|
|
300
|
+
}
|
|
301
|
+
const first = renderComponent(exposed.GufaTerminalView, props)
|
|
302
|
+
expand(first.vnode)
|
|
303
|
+
const second = renderComponent(exposed.GufaTerminalView, props)
|
|
304
|
+
check(`GufaTerminalView 渲染:${scenario.name}`, first.vnode !== undefined && first.vnode !== null)
|
|
305
|
+
check(`GufaTerminalView hook 数量稳定:${scenario.name}`, first.hookCount === second.hookCount, `${first.hookCount} vs ${second.hookCount}`)
|
|
306
|
+
} catch (error) {
|
|
307
|
+
check(`GufaTerminalView 渲染:${scenario.name}`, false, error instanceof Error ? error.stack : String(error))
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
console.log(`\n${passed} passed, ${failed} failed`)
|
|
314
|
+
process.exit(failed === 0 ? 0 : 1)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 真实 PTY 集成自测(可选,需要本机解包后的 DSH 安装目录)。
|
|
4
|
+
*
|
|
5
|
+
* 不启动 Harness,而是直接实例化 DSH 的 `LocalSubprocessRuntime`(本地 node-pty 实现),
|
|
6
|
+
* 用 host/terminal.js 的 register() 走一遍真实链路:
|
|
7
|
+
* spawnTerminal → PTY 输出 → WS 广播 → 键盘输入 → resize → exit → close
|
|
8
|
+
* 并验证环境变量过滤与 `since` 增量回放语义。
|
|
9
|
+
*
|
|
10
|
+
* 依赖:/tmp/dsh-asar/dsh(或 GUFA_DSH_ASAR 指定的目录)与 node-pty 的
|
|
11
|
+
* prebuilds/darwin-arm64/{pty.node,spawn-helper}(从 app.asar.unpacked 复制)。
|
|
12
|
+
* 条件不满足时打印 skip 并以 0 退出,不影响常规 CI。
|
|
13
|
+
*
|
|
14
|
+
* 用法:node scripts/selftest-terminal-pty.mjs
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'
|
|
17
|
+
import { tmpdir } from 'node:os'
|
|
18
|
+
import { dirname, join, resolve } from 'node:path'
|
|
19
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
20
|
+
|
|
21
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
22
|
+
const dshRoot = process.env.GUFA_DSH_ASAR ?? '/tmp/dsh-asar/dsh'
|
|
23
|
+
const modules = join(dshRoot, 'node_modules')
|
|
24
|
+
|
|
25
|
+
function skip(reason) {
|
|
26
|
+
console.log(`selftest-terminal-pty: skip — ${reason}`)
|
|
27
|
+
process.exit(0)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const cordisEntry = join(modules, '@deepseek-ai', 'cordis', 'lib', 'index.js')
|
|
31
|
+
const subprocessEntry = join(modules, '@deepseek-ai', 'dsh-subprocess-local', 'lib', 'index.js')
|
|
32
|
+
const ptyPrebuild = join(modules, 'node-pty', 'prebuilds', `${process.platform}-${process.arch}`)
|
|
33
|
+
if (!existsSync(cordisEntry) || !existsSync(subprocessEntry)) skip(`未找到解包后的 DSH 安装:${dshRoot}`)
|
|
34
|
+
if (!existsSync(join(ptyPrebuild, 'pty.node')) || !existsSync(join(ptyPrebuild, 'spawn-helper'))) {
|
|
35
|
+
skip(`node-pty 原生模块缺失:${ptyPrebuild}(可从 DSH.app/Contents/Resources/app.asar.unpacked 复制)`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let passed = 0
|
|
39
|
+
let failed = 0
|
|
40
|
+
function check(name, condition, detail) {
|
|
41
|
+
if (condition) {
|
|
42
|
+
passed += 1
|
|
43
|
+
console.log(` ok ${name}`)
|
|
44
|
+
} else {
|
|
45
|
+
failed += 1
|
|
46
|
+
console.error(` FAIL ${name}${detail === undefined ? '' : ` — ${detail}`}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { Context } = await import(pathToFileURL(cordisEntry).href)
|
|
51
|
+
const { default: LocalSubprocessRuntime } = await import(pathToFileURL(subprocessEntry).href)
|
|
52
|
+
const { ProjectRegistry } = await import(pathToFileURL(join(packageRoot, 'host', 'context.js')).href)
|
|
53
|
+
const terminal = await import(pathToFileURL(join(packageRoot, 'host', 'terminal.js')).href)
|
|
54
|
+
|
|
55
|
+
/* ── 假 router / WS 连接(与 selftest-terminal.mjs 一致) ───────────────── */
|
|
56
|
+
function makeRouter() {
|
|
57
|
+
const router = {
|
|
58
|
+
methods: new Map(),
|
|
59
|
+
sockets: [],
|
|
60
|
+
disposers: [],
|
|
61
|
+
method(name, fn) {
|
|
62
|
+
router.methods.set(name, fn)
|
|
63
|
+
},
|
|
64
|
+
ws(channel, handler) {
|
|
65
|
+
router.sockets.push({ channel, handler })
|
|
66
|
+
},
|
|
67
|
+
onDispose(fn) {
|
|
68
|
+
router.disposers.push(fn)
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
return router
|
|
72
|
+
}
|
|
73
|
+
function makeConnection() {
|
|
74
|
+
const handlers = new Map()
|
|
75
|
+
const connection = {
|
|
76
|
+
readyState: 1,
|
|
77
|
+
bufferedAmount: 0,
|
|
78
|
+
frames: [],
|
|
79
|
+
send(text) {
|
|
80
|
+
connection.frames.push(JSON.parse(text))
|
|
81
|
+
},
|
|
82
|
+
close() {},
|
|
83
|
+
ping() {},
|
|
84
|
+
on(event, fn) {
|
|
85
|
+
handlers.set(event, fn)
|
|
86
|
+
},
|
|
87
|
+
once(event, fn) {
|
|
88
|
+
handlers.set(event, fn)
|
|
89
|
+
},
|
|
90
|
+
emit(event, value) {
|
|
91
|
+
handlers.get(event)?.(value)
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
return connection
|
|
95
|
+
}
|
|
96
|
+
const sleep = (ms) => new Promise((done) => setTimeout(done, ms))
|
|
97
|
+
const textOf = (connection) => connection.frames.filter((frame) => frame.type === 'output').map((frame) => frame.data).join('')
|
|
98
|
+
const lastOutput = (connection) => [...connection.frames].reverse().find((frame) => frame.type === 'output')
|
|
99
|
+
|
|
100
|
+
/* ── 启动真实 subprocess 服务 ──────────────────────────────────────────── */
|
|
101
|
+
const ctx = new Context()
|
|
102
|
+
ctx.logger = { debug() {}, warn() {}, error: (...args) => console.error('[host]', ...args) }
|
|
103
|
+
const runtime = new LocalSubprocessRuntime(ctx)
|
|
104
|
+
check('ctx.get(subprocess).spawnTerminal 可用', typeof ctx.get('subprocess')?.spawnTerminal === 'function')
|
|
105
|
+
|
|
106
|
+
const sandboxRoot = mkdtempSync(join(tmpdir(), 'gufa-terminal-pty-'))
|
|
107
|
+
const projectDir = join(sandboxRoot, 'project')
|
|
108
|
+
mkdirSync(projectDir)
|
|
109
|
+
const router = makeRouter()
|
|
110
|
+
const env = { ctx, projects: new ProjectRegistry() }
|
|
111
|
+
const opened = await env.projects.open(projectDir)
|
|
112
|
+
terminal.register(router, env)
|
|
113
|
+
const call = (name, body = {}) => router.methods.get(name)(body, {})
|
|
114
|
+
const wsUrl = (id, since) => new URL(`http://dsh.local/gufa-code/ws?channel=terminal&root=${encodeURIComponent(opened.root)}&id=${encodeURIComponent(id)}${since === undefined ? '' : `&since=${since}`}`)
|
|
115
|
+
const attach = (connection, id, since) => router.sockets.find((socket) => socket.channel === 'terminal').handler(connection, wsUrl(id, since), {})
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
console.log('真实 PTY:create / 输入输出 / resize')
|
|
119
|
+
process.env.GUFA_TERMINAL_SELFTEST_TOKEN = 'must-not-leak'
|
|
120
|
+
const created = await call('terminal.create', { root: opened.root, cols: 100, rows: 30 })
|
|
121
|
+
check('create 返回 title=zsh', created.title === 'zsh', created.title)
|
|
122
|
+
|
|
123
|
+
const connection = makeConnection()
|
|
124
|
+
attach(connection, created.id)
|
|
125
|
+
await sleep(2500)
|
|
126
|
+
// 通过 shell 打印敏感变量(应为空)与中间结果,验证环境过滤 + PTY 双向通道
|
|
127
|
+
connection.emit('message', Buffer.from(JSON.stringify({ type: 'input', data: `printf 'token=[%s]\\n' "$GUFA_TERMINAL_SELFTEST_TOKEN"; echo REAL_PTY_$((6*7))\r` })))
|
|
128
|
+
await sleep(2000)
|
|
129
|
+
const streamText = textOf(connection)
|
|
130
|
+
check('PTY 输出回传(命令结果)', streamText.includes('REAL_PTY_42'))
|
|
131
|
+
check('敏感环境变量被过滤', streamText.includes('token=[]'), JSON.stringify(streamText.slice(-120)))
|
|
132
|
+
|
|
133
|
+
connection.emit('message', Buffer.from(JSON.stringify({ type: 'resize', cols: 120, rows: 40 })))
|
|
134
|
+
await sleep(400)
|
|
135
|
+
const resized = await call('terminal.list', { root: opened.root })
|
|
136
|
+
check('resize 生效', resized.terminals[0]?.cols === 120 && resized.terminals[0]?.rows === 40, JSON.stringify(resized.terminals[0]))
|
|
137
|
+
|
|
138
|
+
console.log('真实 PTY:since 增量回放')
|
|
139
|
+
const bytes1 = lastOutput(connection).offset + Buffer.byteLength(lastOutput(connection).data)
|
|
140
|
+
connection.emit('close')
|
|
141
|
+
await sleep(100)
|
|
142
|
+
const c2 = makeConnection()
|
|
143
|
+
attach(c2, created.id, bytes1)
|
|
144
|
+
check('无新输出时 since 重连不重复回放', textOf(c2).length === 0)
|
|
145
|
+
c2.emit('message', Buffer.from(JSON.stringify({ type: 'input', data: 'echo MARK_AFTER_RECONNECT\r' })))
|
|
146
|
+
await sleep(1500)
|
|
147
|
+
check('since 重连后仍可输入输出', textOf(c2).includes('MARK_AFTER_RECONNECT'))
|
|
148
|
+
const bytes2 = lastOutput(c2).offset + Buffer.byteLength(lastOutput(c2).data)
|
|
149
|
+
c2.emit('close')
|
|
150
|
+
await sleep(100)
|
|
151
|
+
|
|
152
|
+
// c2 断开期间由另一个连接产生输出,再以 bytes2 重连:只补缺失段
|
|
153
|
+
const producer = makeConnection()
|
|
154
|
+
attach(producer, created.id)
|
|
155
|
+
producer.emit('message', Buffer.from(JSON.stringify({ type: 'input', data: 'echo MISSED_WHILE_OFFLINE\r' })))
|
|
156
|
+
await sleep(1500)
|
|
157
|
+
producer.emit('close')
|
|
158
|
+
await sleep(100)
|
|
159
|
+
const catchUp = makeConnection()
|
|
160
|
+
attach(catchUp, created.id, bytes2)
|
|
161
|
+
const catchUpText = textOf(catchUp)
|
|
162
|
+
check('since 重连补发缺失输出', catchUpText.includes('MISSED_WHILE_OFFLINE'))
|
|
163
|
+
check('since 重连不重复已有输出', !catchUpText.includes('MARK_AFTER_RECONNECT'))
|
|
164
|
+
check('补发帧 offset 等于 since', lastOutput(catchUp)?.offset === bytes2)
|
|
165
|
+
const latestBytes = lastOutput(catchUp).offset + Buffer.byteLength(lastOutput(catchUp).data)
|
|
166
|
+
const idle = makeConnection()
|
|
167
|
+
attach(idle, created.id, latestBytes)
|
|
168
|
+
check('since 等于最新偏移时无回放', textOf(idle).length === 0)
|
|
169
|
+
|
|
170
|
+
console.log('真实 PTY:exit / close')
|
|
171
|
+
catchUp.emit('message', Buffer.from(JSON.stringify({ type: 'input', data: 'exit\r' })))
|
|
172
|
+
await sleep(2000)
|
|
173
|
+
const exitFrame = [...catchUp.frames, ...idle.frames].find((frame) => frame.type === 'exit')
|
|
174
|
+
check('shell 退出广播 exit 帧', exitFrame !== undefined && typeof exitFrame.code === 'number', JSON.stringify(exitFrame))
|
|
175
|
+
const closed = await call('terminal.close', { root: opened.root, id: created.id })
|
|
176
|
+
check('terminal.close 幂等成功', closed.ok === true)
|
|
177
|
+
} finally {
|
|
178
|
+
for (const dispose of router.disposers) dispose()
|
|
179
|
+
await sleep(500)
|
|
180
|
+
rmSync(sandboxRoot, { recursive: true, force: true })
|
|
181
|
+
// 释放 subprocess 服务(会终止仍存活的 PTY)
|
|
182
|
+
try {
|
|
183
|
+
await runtime.disposeManagedProcesses?.()
|
|
184
|
+
} catch {
|
|
185
|
+
/* ignore */
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log(`\n${passed} passed, ${failed} failed`)
|
|
190
|
+
process.exit(failed === 0 ? 0 : 1)
|