dsh-gufa-code 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +96 -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-editor.jpg +0 -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 +66 -4
- 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,351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 终端能力自测:不启动 Harness / 不分配真实 PTY。
|
|
4
|
+
*
|
|
5
|
+
* 用假的 ctx.subprocess(handle 用 PassThrough 模拟输出)验证 host/terminal.js 的
|
|
6
|
+
* create/list/close/WS/重连回放/上限/卸载 逻辑,并单测 host/terminal-pty.js 的纯函数。
|
|
7
|
+
*
|
|
8
|
+
* 用法:node scripts/selftest-terminal.mjs
|
|
9
|
+
*/
|
|
10
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
11
|
+
import { tmpdir } from 'node:os'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { PassThrough } from 'node:stream'
|
|
14
|
+
|
|
15
|
+
import { ProjectRegistry } from '../host/context.js'
|
|
16
|
+
import * as terminal from '../host/terminal.js'
|
|
17
|
+
import {
|
|
18
|
+
clampInteger,
|
|
19
|
+
filteredEnvironment,
|
|
20
|
+
ReplayBuffer,
|
|
21
|
+
resolveShellPath,
|
|
22
|
+
tailByBytes,
|
|
23
|
+
terminalTitleFor,
|
|
24
|
+
TERMINAL_TYPE,
|
|
25
|
+
} from '../host/terminal-pty.js'
|
|
26
|
+
|
|
27
|
+
let passed = 0
|
|
28
|
+
let failed = 0
|
|
29
|
+
function check(name, condition, detail) {
|
|
30
|
+
if (condition) {
|
|
31
|
+
passed += 1
|
|
32
|
+
console.log(` ok ${name}`)
|
|
33
|
+
} else {
|
|
34
|
+
failed += 1
|
|
35
|
+
console.error(` FAIL ${name}${detail === undefined ? '' : ` — ${detail}`}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 假 router:只记录注册的 method / ws / onDispose。 */
|
|
40
|
+
function makeRouter() {
|
|
41
|
+
const router = {
|
|
42
|
+
methods: new Map(),
|
|
43
|
+
sockets: [],
|
|
44
|
+
disposers: [],
|
|
45
|
+
method(name, fn) {
|
|
46
|
+
router.methods.set(name, fn)
|
|
47
|
+
},
|
|
48
|
+
ws(channel, handler) {
|
|
49
|
+
router.sockets.push({ channel, handler })
|
|
50
|
+
},
|
|
51
|
+
streams: new Map(),
|
|
52
|
+
stream(name, fn) {
|
|
53
|
+
router.streams.set(name, fn)
|
|
54
|
+
},
|
|
55
|
+
onDispose(fn) {
|
|
56
|
+
router.disposers.push(fn)
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
return router
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 假 PTY handle:PassThrough 输出 + 记录 write/resize/terminate。 */
|
|
63
|
+
function makeFakeSubprocess() {
|
|
64
|
+
const calls = { spawn: [], spec: null }
|
|
65
|
+
const spawned = []
|
|
66
|
+
const service = {
|
|
67
|
+
terminalEnvironment: async () => ({ platform: 'posix', defaultShell: '/bin/zsh' }),
|
|
68
|
+
async spawnTerminal(spec) {
|
|
69
|
+
calls.spawn.push(spec)
|
|
70
|
+
calls.spec = spec
|
|
71
|
+
const output = new PassThrough()
|
|
72
|
+
let resolveExit
|
|
73
|
+
const done = new Promise((resolve) => {
|
|
74
|
+
resolveExit = resolve
|
|
75
|
+
})
|
|
76
|
+
const handle = {
|
|
77
|
+
output,
|
|
78
|
+
writes: [],
|
|
79
|
+
resizes: [],
|
|
80
|
+
terminated: 0,
|
|
81
|
+
write(data) {
|
|
82
|
+
handle.writes.push(data)
|
|
83
|
+
},
|
|
84
|
+
resize(cols, rows) {
|
|
85
|
+
handle.resizes.push([cols, rows])
|
|
86
|
+
},
|
|
87
|
+
async terminate() {
|
|
88
|
+
handle.terminated += 1
|
|
89
|
+
output.end()
|
|
90
|
+
},
|
|
91
|
+
done,
|
|
92
|
+
resolveExit,
|
|
93
|
+
}
|
|
94
|
+
spawned.push(handle)
|
|
95
|
+
return handle
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
return { service, calls, spawned }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 假 WS 连接。 */
|
|
102
|
+
/** 去掉诊断用的 hello 帧,只看终端协议帧。 */
|
|
103
|
+
function stripControlFrames(connection) {
|
|
104
|
+
connection.frames = connection.frames.filter((frame) => frame.type !== 'hello' && frame.type !== 'hello2')
|
|
105
|
+
return connection
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function makeConnection() {
|
|
109
|
+
const handlers = new Map()
|
|
110
|
+
return {
|
|
111
|
+
readyState: 1,
|
|
112
|
+
bufferedAmount: 0,
|
|
113
|
+
frames: [],
|
|
114
|
+
send(text) {
|
|
115
|
+
this.frames.push(JSON.parse(text))
|
|
116
|
+
},
|
|
117
|
+
close() {},
|
|
118
|
+
ping() {},
|
|
119
|
+
on(event, fn) {
|
|
120
|
+
handlers.set(event, fn)
|
|
121
|
+
},
|
|
122
|
+
once(event, fn) {
|
|
123
|
+
handlers.set(event, fn)
|
|
124
|
+
},
|
|
125
|
+
emit(event, value) {
|
|
126
|
+
handlers.get(event)?.(value)
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 已经过 decode 的一帧输出(模拟 PTY 流)。 */
|
|
132
|
+
function emit(handle, text) {
|
|
133
|
+
handle.output.write(Buffer.from(text, 'utf8'))
|
|
134
|
+
return new Promise((resolve) => setTimeout(resolve, 0))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const sandbox = mkdtempSync(join(tmpdir(), 'gufa-terminal-selftest-'))
|
|
138
|
+
const projectDir = join(sandbox, 'demo-project')
|
|
139
|
+
const { mkdirSync } = await import('node:fs')
|
|
140
|
+
mkdirSync(projectDir)
|
|
141
|
+
|
|
142
|
+
console.log('terminal-pty 纯函数')
|
|
143
|
+
{
|
|
144
|
+
const env = filteredEnvironment({
|
|
145
|
+
HOME: '/Users/demo',
|
|
146
|
+
PATH: '/usr/bin:/bin',
|
|
147
|
+
SHELL: '/bin/zsh',
|
|
148
|
+
LANG: 'zh_CN.UTF-8',
|
|
149
|
+
OPENAI_API_KEY: 'sk-secret',
|
|
150
|
+
GITHUB_TOKEN: 'ghp_secret',
|
|
151
|
+
AWS_SECRET_ACCESS_KEY: 'secret',
|
|
152
|
+
DSH_SESSION_ID: 'abc',
|
|
153
|
+
SSH_AUTH_SOCK: '/tmp/agent.sock',
|
|
154
|
+
MONKEY: 'not-a-key',
|
|
155
|
+
KEYBOARD_LAYOUT: 'us',
|
|
156
|
+
})
|
|
157
|
+
check('keeps HOME/PATH/SHELL/LANG', env.HOME === '/Users/demo' && env.PATH === '/usr/bin:/bin' && env.SHELL === '/bin/zsh' && env.LANG === 'zh_CN.UTF-8')
|
|
158
|
+
check('filters *_KEY / *_TOKEN / *_SECRET', env.OPENAI_API_KEY === undefined && env.GITHUB_TOKEN === undefined && env.AWS_SECRET_ACCESS_KEY === undefined)
|
|
159
|
+
check('filters DSH_* namespace', env.DSH_SESSION_ID === undefined)
|
|
160
|
+
check('keeps SSH_AUTH_SOCK', env.SSH_AUTH_SOCK === '/tmp/agent.sock')
|
|
161
|
+
check('does not false-positive MONKEY/KEYBOARD_LAYOUT', env.MONKEY === 'not-a-key' && env.KEYBOARD_LAYOUT === 'us')
|
|
162
|
+
check('forces TERM=xterm-256color', env.TERM === TERMINAL_TYPE)
|
|
163
|
+
|
|
164
|
+
check('clampInteger default', clampInteger(undefined, 80, 2, 1000) === 80)
|
|
165
|
+
check('clampInteger bounds', clampInteger(1, 80, 2, 1000) === 2 && clampInteger(99999, 80, 2, 1000) === 1000)
|
|
166
|
+
check('clampInteger truncates', clampInteger(33.9, 80, 2, 1000) === 33)
|
|
167
|
+
check('clampInteger rejects NaN', clampInteger(Number.NaN, 24, 2, 500) === 24)
|
|
168
|
+
|
|
169
|
+
const title = terminalTitleFor('/bin/zsh')
|
|
170
|
+
check('terminalTitleFor zsh', title === 'zsh', title)
|
|
171
|
+
check('terminalTitleFor windows', terminalTitleFor('C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe') === 'powershell')
|
|
172
|
+
check('tailByBytes trims to limit', Buffer.byteLength(tailByBytes('你好世界hello', 10), 'utf8') <= 10)
|
|
173
|
+
check('tailByBytes keeps short text', tailByBytes('abc', 10) === 'abc')
|
|
174
|
+
|
|
175
|
+
const exists = async (path) => path === '/bin/zsh' || path === '/opt/custom'
|
|
176
|
+
check('resolveShellPath explicit wins', (await resolveShellPath({ explicit: '/opt/custom', preferred: '/bin/zsh', env: {}, exists })) === '/opt/custom')
|
|
177
|
+
check('resolveShellPath explicit missing falls back', (await resolveShellPath({ explicit: '/nope', preferred: '/bin/zsh', env: {}, exists })) === '/bin/zsh')
|
|
178
|
+
check('resolveShellPath env SHELL fallback', (await resolveShellPath({ env: { SHELL: '/bin/zsh' }, exists, platform: 'darwin' })) === '/bin/zsh')
|
|
179
|
+
check('resolveShellPath platform default', (await resolveShellPath({ env: {}, platform: 'darwin', exists: async () => false })) === '/bin/zsh')
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log('ReplayBuffer')
|
|
183
|
+
{
|
|
184
|
+
const buffer = new ReplayBuffer(32)
|
|
185
|
+
buffer.push('hello')
|
|
186
|
+
check('totalBytes after push', buffer.totalBytes === 5, String(buffer.totalBytes))
|
|
187
|
+
check('retainedStart after push', buffer.retainedStart === 0, String(buffer.retainedStart))
|
|
188
|
+
check('sliceFrom aligned', buffer.sliceFrom(2).text === 'llo' && buffer.sliceFrom(2).lossy === false)
|
|
189
|
+
check('sliceFrom end', buffer.sliceFrom(5).text === '')
|
|
190
|
+
buffer.push(' world')
|
|
191
|
+
check('toString joins', buffer.toString() === 'hello world')
|
|
192
|
+
check('sliceFrom across chunks', buffer.sliceFrom(5).text === ' world')
|
|
193
|
+
check('sliceFrom future offset is lossy', buffer.sliceFrom(999).lossy === true)
|
|
194
|
+
check('sliceFrom negative is lossy', buffer.sliceFrom(-1).lossy === true)
|
|
195
|
+
check('sliceFrom inside chunk (misaligned) resyncs', buffer.sliceFrom(2).text.startsWith('llo'))
|
|
196
|
+
const trimmed = new ReplayBuffer(16)
|
|
197
|
+
trimmed.push('aaaaaaaaaa') // 10
|
|
198
|
+
trimmed.push('bbbbbbbbbb') // 20 > 16 → 丢弃第一段
|
|
199
|
+
check('trims oldest chunk', trimmed.toString() === 'bbbbbbbbbb', trimmed.toString())
|
|
200
|
+
check('retainedStart advances', trimmed.retainedStart === 10, String(trimmed.retainedStart))
|
|
201
|
+
check('old offset becomes lossy', trimmed.sliceFrom(5).lossy === true)
|
|
202
|
+
const huge = new ReplayBuffer(8)
|
|
203
|
+
huge.push('0123456789abcdef')
|
|
204
|
+
check('oversized chunk keeps tail', huge.toString() === '89abcdef', huge.toString())
|
|
205
|
+
check('oversized chunk totalBytes', huge.totalBytes === 16, String(huge.totalBytes))
|
|
206
|
+
check('oversized retainedStart', huge.retainedStart === 8, String(huge.retainedStart))
|
|
207
|
+
huge.clear()
|
|
208
|
+
check('clear keeps absolute counter', huge.totalBytes === 16 && huge.retainedBytes === 0)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log('terminal.create / list / close(假 subprocess)')
|
|
212
|
+
{
|
|
213
|
+
const router = makeRouter()
|
|
214
|
+
const { service, calls, spawned } = makeFakeSubprocess()
|
|
215
|
+
const env = {
|
|
216
|
+
ctx: {
|
|
217
|
+
get(name) {
|
|
218
|
+
return name === 'subprocess' ? service : undefined
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
projects: new ProjectRegistry(),
|
|
222
|
+
}
|
|
223
|
+
const opened = await env.projects.open(projectDir)
|
|
224
|
+
const root = opened.root
|
|
225
|
+
terminal.register(router, env)
|
|
226
|
+
const call = (name, body = {}) => router.methods.get(name)(body, {})
|
|
227
|
+
|
|
228
|
+
const created = await call('terminal.create', { root, cols: 120, rows: 30 })
|
|
229
|
+
check('create returns id/title', typeof created.id === 'string' && created.id.startsWith('term-') && created.title === 'zsh', JSON.stringify(created))
|
|
230
|
+
const spec = calls.spawn[0]
|
|
231
|
+
check('spawn uses login shell', spec.argv[0] === '/bin/zsh' && spec.argv.includes('-l'), JSON.stringify(spec.argv))
|
|
232
|
+
check('spawn cwd = project root', spec.cwd === root, spec.cwd)
|
|
233
|
+
check('spawn terminalType', spec.terminalType === TERMINAL_TYPE)
|
|
234
|
+
check('spawn graceMs', spec.graceMs === 5000, String(spec.graceMs))
|
|
235
|
+
check('spawn rows/cols from request', spec.rows === 30 && spec.cols === 120)
|
|
236
|
+
check('spawn env filtered', spec.env.TERM === TERMINAL_TYPE && typeof spec.env.PATH === 'string' && spec.env.DSH_SESSION_ID === undefined)
|
|
237
|
+
|
|
238
|
+
const list = await call('terminal.list', { root })
|
|
239
|
+
check('list reports one alive terminal', list.terminals.length === 1 && list.terminals[0].alive === true && list.terminals[0].cols === 120)
|
|
240
|
+
|
|
241
|
+
// WS 接入 + 回放
|
|
242
|
+
const handle = spawned[0]
|
|
243
|
+
await emit(handle, 'welcome $ ')
|
|
244
|
+
const connection = makeConnection()
|
|
245
|
+
const wsEntry = router.sockets.find((item) => item.channel === 'terminal')
|
|
246
|
+
const cleanup = wsEntry.handler(connection, new URL(`http://dsh.local/gufa-code/ws?channel=terminal&root=${encodeURIComponent(root)}&id=${encodeURIComponent(created.id)}`), {})
|
|
247
|
+
stripControlFrames(connection)
|
|
248
|
+
check('ws attach replays existing output with offset', connection.frames[0]?.type === 'output' && connection.frames[0].data === 'welcome $ ' && connection.frames[0].offset === 0, JSON.stringify(connection.frames))
|
|
249
|
+
check('ws handler returns cleanup', typeof cleanup === 'function')
|
|
250
|
+
|
|
251
|
+
await emit(handle, 'ls -la\r\n')
|
|
252
|
+
check('live output broadcast', connection.frames.at(-1)?.type === 'output' && connection.frames.at(-1).data === 'ls -la\r\n')
|
|
253
|
+
const receivedBytes = connection.frames.at(-1).offset + Buffer.byteLength(connection.frames.at(-1).data)
|
|
254
|
+
|
|
255
|
+
connection.emit('message', Buffer.from(JSON.stringify({ type: 'input', data: 'echo hi\r' })))
|
|
256
|
+
check('input forwarded to pty', handle.writes.at(-1) === 'echo hi\r', JSON.stringify(handle.writes))
|
|
257
|
+
connection.emit('message', Buffer.from(JSON.stringify({ type: 'resize', cols: 90, rows: 20 })))
|
|
258
|
+
check('resize forwarded to pty', handle.resizes.at(-1)?.[0] === 90 && handle.resizes.at(-1)?.[1] === 20, JSON.stringify(handle.resizes))
|
|
259
|
+
connection.emit('message', Buffer.from('not-json'))
|
|
260
|
+
check('invalid ws message ignored', handle.writes.length === 1)
|
|
261
|
+
|
|
262
|
+
// 断开后重连:只补发缺失部分
|
|
263
|
+
cleanup()
|
|
264
|
+
await emit(handle, 'after-disconnect')
|
|
265
|
+
const reconnect = makeConnection()
|
|
266
|
+
wsEntry.handler(reconnect, new URL(`http://dsh.local/gufa-code/ws?channel=terminal&root=${encodeURIComponent(root)}&id=${encodeURIComponent(created.id)}&since=${receivedBytes}`), {})
|
|
267
|
+
stripControlFrames(reconnect)
|
|
268
|
+
check('reconnect sends only missing tail', reconnect.frames.length === 1 && reconnect.frames[0].type === 'output' && reconnect.frames[0].data === 'after-disconnect', JSON.stringify(reconnect.frames))
|
|
269
|
+
check('reconnect tail offset = since', reconnect.frames[0].offset === receivedBytes, String(reconnect.frames[0]?.offset))
|
|
270
|
+
|
|
271
|
+
// 退出
|
|
272
|
+
handle.resolveExit({ exitCode: 0, signal: null })
|
|
273
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
274
|
+
check('exit broadcast to ws', reconnect.frames.at(-1)?.type === 'exit' && reconnect.frames.at(-1).code === 0, JSON.stringify(reconnect.frames.at(-1)))
|
|
275
|
+
const exitedList = await call('terminal.list', { root })
|
|
276
|
+
check('list marks exited terminal', exitedList.terminals[0].alive === false && exitedList.terminals[0].exitCode === 0)
|
|
277
|
+
|
|
278
|
+
// 关闭
|
|
279
|
+
const closed = await call('terminal.close', { root, id: created.id })
|
|
280
|
+
check('close returns ok', closed.ok === true)
|
|
281
|
+
check('close terminates pty', handle.terminated === 1)
|
|
282
|
+
check('list empty after close', (await call('terminal.list', { root })).terminals.length === 0)
|
|
283
|
+
let missing = false
|
|
284
|
+
try {
|
|
285
|
+
await call('terminal.close', { root, id: created.id })
|
|
286
|
+
} catch (error) {
|
|
287
|
+
missing = error.code === 'not-found'
|
|
288
|
+
}
|
|
289
|
+
check('second close -> not-found', missing)
|
|
290
|
+
|
|
291
|
+
// 上限 8
|
|
292
|
+
const ids = []
|
|
293
|
+
for (let index = 0; index < 8; index += 1) ids.push((await call('terminal.create', { root })).id)
|
|
294
|
+
let limited = false
|
|
295
|
+
try {
|
|
296
|
+
await call('terminal.create', { root })
|
|
297
|
+
} catch (error) {
|
|
298
|
+
limited = error.code === 'terminal-limit'
|
|
299
|
+
}
|
|
300
|
+
check('per-root terminal limit is 8', limited)
|
|
301
|
+
|
|
302
|
+
// 未知 root 被拒绝
|
|
303
|
+
let rejected = false
|
|
304
|
+
try {
|
|
305
|
+
await call('terminal.create', { root: '/tmp/not-opened' })
|
|
306
|
+
} catch (error) {
|
|
307
|
+
rejected = error.code === 'project-not-open'
|
|
308
|
+
}
|
|
309
|
+
check('unregistered root rejected', rejected)
|
|
310
|
+
|
|
311
|
+
// dispose 清理全部 PTY
|
|
312
|
+
for (const dispose of router.disposers) dispose()
|
|
313
|
+
check('dispose terminates every pty', spawned.every((item) => item.terminated >= 1), spawned.map((item) => item.terminated).join(','))
|
|
314
|
+
check('dispose clears list', (await (async () => { try { return await call('terminal.list', { root }) } catch { return { terminals: [] } } })()).terminals.length === 0)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
console.log('WS 错误路径')
|
|
318
|
+
{
|
|
319
|
+
const router = makeRouter()
|
|
320
|
+
const { service } = makeFakeSubprocess()
|
|
321
|
+
const env = {
|
|
322
|
+
ctx: { get: (name) => (name === 'subprocess' ? service : undefined) },
|
|
323
|
+
projects: new ProjectRegistry(),
|
|
324
|
+
}
|
|
325
|
+
await env.projects.open(projectDir)
|
|
326
|
+
terminal.register(router, env)
|
|
327
|
+
const wsEntry = router.sockets.find((item) => item.channel === 'terminal')
|
|
328
|
+
const connection = makeConnection()
|
|
329
|
+
wsEntry.handler(connection, new URL('http://dsh.local/gufa-code/ws?channel=terminal&root=%2Fnope&id=nope'), {})
|
|
330
|
+
check('unknown terminal -> error frame', connection.frames[0]?.type === 'error', JSON.stringify(connection.frames))
|
|
331
|
+
for (const dispose of router.disposers) dispose()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
console.log('终端服务缺失')
|
|
335
|
+
{
|
|
336
|
+
const router = makeRouter()
|
|
337
|
+
const env = { ctx: { get: () => undefined }, projects: new ProjectRegistry() }
|
|
338
|
+
const opened = await env.projects.open(projectDir)
|
|
339
|
+
terminal.register(router, env)
|
|
340
|
+
let code = ''
|
|
341
|
+
try {
|
|
342
|
+
await router.methods.get('terminal.create')({ root: opened.root }, {})
|
|
343
|
+
} catch (error) {
|
|
344
|
+
code = error.code
|
|
345
|
+
}
|
|
346
|
+
check('missing subprocess -> terminal-unavailable', code === 'terminal-unavailable', code)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
rmSync(sandbox, { recursive: true, force: true })
|
|
350
|
+
console.log(`\n${passed} passed, ${failed} failed`)
|
|
351
|
+
process.exit(failed === 0 ? 0 : 1)
|