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.
Files changed (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -2
  3. package/client.js +11548 -0
  4. package/cordis.patch.yml +8 -0
  5. package/docs/icon-180.png +0 -0
  6. package/docs/icon.svg +18 -0
  7. package/docs/market-submission.md +32 -0
  8. package/docs/screenshot-editor.jpg +0 -0
  9. package/docs/screenshot-ide.jpg +0 -0
  10. package/host/assets.js +31 -0
  11. package/host/chat.js +523 -0
  12. package/host/context.js +438 -0
  13. package/host/dev.js +35 -0
  14. package/host/fs.js +306 -0
  15. package/host/git.js +532 -0
  16. package/host/project.js +305 -0
  17. package/host/ssh.js +54 -0
  18. package/host/terminal-pty.js +237 -0
  19. package/host/terminal.js +947 -0
  20. package/icon.svg +18 -0
  21. package/index.js +63 -0
  22. package/locale/en.json +121 -0
  23. package/locale/zh.json +121 -0
  24. package/package.json +66 -4
  25. package/scripts/build-client.mjs +38 -0
  26. package/scripts/selftest-client.mjs +457 -0
  27. package/scripts/selftest-host.mjs +197 -0
  28. package/scripts/selftest-http.mjs +284 -0
  29. package/scripts/selftest-router.mjs +101 -0
  30. package/scripts/selftest-terminal-client.mjs +314 -0
  31. package/scripts/selftest-terminal-pty.mjs +190 -0
  32. package/scripts/selftest-terminal.mjs +351 -0
  33. package/scripts/test-chat-render.mjs +702 -0
  34. package/scripts/test-chat.mjs +761 -0
  35. package/scripts/test-editor.mjs +516 -0
  36. package/scripts/verify-workspace-sessions.mjs +135 -0
  37. package/src/client/00-core.js +1686 -0
  38. package/src/client/10-icons.js +205 -0
  39. package/src/client/20-editor.js +1318 -0
  40. package/src/client/30-terminal.js +1510 -0
  41. package/src/client/40-chat.js +1184 -0
  42. package/src/client/46-gitgraph.js +278 -0
  43. package/src/client/50-panels.js +916 -0
  44. package/src/client/55-backdrop.js +508 -0
  45. package/src/client/60-dock.js +662 -0
  46. package/src/client/85-shortcuts.js +516 -0
  47. package/src/client/90-app.js +2272 -0
  48. package/src/client/99-main.js +449 -0
  49. package/vendor/monaco/LICENSE +21 -0
  50. package/vendor/monaco/ThirdPartyNotices.txt +448 -0
  51. package/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  52. package/vendor/monaco/vs/base/worker/workerMain.js +31 -0
  53. package/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  54. package/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  55. package/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  56. package/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  57. package/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  58. package/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  59. package/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  60. package/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  61. package/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  62. package/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  63. package/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  64. package/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  65. package/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  66. package/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  67. package/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  68. package/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  69. package/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  70. package/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  71. package/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  72. package/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  73. package/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  74. package/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  75. package/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  76. package/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  77. package/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  78. package/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  79. package/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  80. package/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  81. package/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  82. package/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  83. package/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  84. package/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  85. package/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  86. package/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  87. package/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  88. package/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  89. package/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  90. package/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  91. package/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  92. package/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  93. package/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  94. package/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  95. package/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  96. package/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  97. package/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  98. package/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  99. package/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  100. package/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  101. package/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  102. package/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  103. package/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  104. package/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  105. package/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  106. package/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  107. package/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  108. package/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  109. package/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  110. package/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  111. package/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  112. package/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  113. package/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  114. package/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  115. package/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  116. package/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  117. package/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  118. package/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  119. package/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  120. package/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  121. package/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  122. package/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  123. package/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  124. package/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  125. package/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  126. package/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  127. package/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  128. package/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  129. package/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
  130. package/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  131. package/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  132. package/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  133. package/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  134. package/vendor/monaco/vs/editor/editor.main.css +8 -0
  135. package/vendor/monaco/vs/editor/editor.main.js +798 -0
  136. package/vendor/monaco/vs/language/css/cssMode.js +13 -0
  137. package/vendor/monaco/vs/language/css/cssWorker.js +77 -0
  138. package/vendor/monaco/vs/language/html/htmlMode.js +13 -0
  139. package/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
  140. package/vendor/monaco/vs/language/json/jsonMode.js +19 -0
  141. package/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
  142. package/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
  143. package/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
  144. package/vendor/monaco/vs/loader.js +11 -0
  145. package/vendor/monaco/vs/nls.messages.de.js +21 -0
  146. package/vendor/monaco/vs/nls.messages.es.js +21 -0
  147. package/vendor/monaco/vs/nls.messages.fr.js +19 -0
  148. package/vendor/monaco/vs/nls.messages.it.js +19 -0
  149. package/vendor/monaco/vs/nls.messages.ja.js +21 -0
  150. package/vendor/monaco/vs/nls.messages.ko.js +19 -0
  151. package/vendor/monaco/vs/nls.messages.ru.js +21 -0
  152. package/vendor/monaco/vs/nls.messages.zh-cn.js +21 -0
  153. package/vendor/monaco/vs/nls.messages.zh-tw.js +19 -0
  154. package/vendor/xterm/LICENSE +21 -0
  155. package/vendor/xterm/LICENSE.addon-fit +19 -0
  156. package/vendor/xterm/addon-fit.js +2 -0
  157. package/vendor/xterm/xterm.css +285 -0
  158. package/vendor/xterm/xterm.js +2 -0
@@ -0,0 +1,516 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 20-editor 纯逻辑自测(无需浏览器):
4
+ * 把 00-core / 10-icons / 20-editor 三个零件按构建顺序装配进沙箱(React/hooks/DOM 用最小桩),
5
+ * 校验:
6
+ * 1) 模块级代码可执行、组件渲染分支(无文件 / 二进制 / 过大 / 正常 / 悬空 activePath)
7
+ * 2) 纯函数:model URI、插入光标推进、主题映射、编辑器选项、worker 启动脚本
8
+ * 3) Monaco loader 状态机:注入 loader.js → onload → require.config → 成功;
9
+ * 失败降级、失败缓存不重复注入、显式重试、非 Monaco AMD loader 拒载、单例不重复注入
10
+ *
11
+ * 用法:node scripts/test-editor.mjs
12
+ * 说明:只依赖本插件源码,不读取其他负责人正在编辑的零件文件。
13
+ */
14
+ import assert from 'node:assert/strict'
15
+ import { readFileSync } from 'node:fs'
16
+ import { dirname, join, resolve } from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
18
+
19
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
20
+ const partsDir = join(root, 'src', 'client')
21
+ const PARTS = ['00-core.js', '10-icons.js', '20-editor.js']
22
+
23
+ const editorSource = readFileSync(join(partsDir, '20-editor.js'), 'utf8')
24
+ const source = PARTS.map((name) => `\n/* ───── ${name} ───── */\n${readFileSync(join(partsDir, name), 'utf8')}`).join('\n')
25
+
26
+ /* ── 最小 React 桩:支持函数组件单次渲染时的 hooks 顺序 ───────────────── */
27
+ function createRuntime() {
28
+ const slots = []
29
+ let cursor = 0
30
+ const react = {
31
+ createElement(type, props, ...children) {
32
+ return { type, props: props ?? {}, children }
33
+ },
34
+ useState(initial) {
35
+ const index = cursor++
36
+ if (!(index in slots)) slots[index] = typeof initial === 'function' ? initial() : initial
37
+ return [slots[index], (next) => {
38
+ slots[index] = typeof next === 'function' ? next(slots[index]) : next
39
+ }]
40
+ },
41
+ useRef(initial) {
42
+ const index = cursor++
43
+ if (!(index in slots)) slots[index] = { current: initial }
44
+ return slots[index]
45
+ },
46
+ useCallback(callback) {
47
+ cursor++
48
+ return callback
49
+ },
50
+ useMemo(factory) {
51
+ cursor++
52
+ return factory()
53
+ },
54
+ useEffect() {
55
+ cursor++
56
+ },
57
+ useLayoutEffect() {
58
+ cursor++
59
+ },
60
+ }
61
+ return {
62
+ react,
63
+ beginRender() {
64
+ cursor = 0
65
+ },
66
+ reset() {
67
+ cursor = 0
68
+ slots.length = 0
69
+ },
70
+ }
71
+ }
72
+
73
+ /* ── 最小 DOM 桩(只覆盖 20-editor 用到的 document / window 能力) ───── */
74
+ function createFakeDom() {
75
+ const scripts = []
76
+ const document = {
77
+ head: {
78
+ append(node) {
79
+ if (node.tagName !== 'SCRIPT') return
80
+ node.parentNode = document.head
81
+ scripts.push(node)
82
+ },
83
+ },
84
+ querySelector(selector) {
85
+ const match = /^script\[(.+?)\]$/.exec(selector)
86
+ if (match === null) return null
87
+ const attribute = match[1]
88
+ return scripts.find((script) => script.getAttribute(attribute) !== undefined) ?? null
89
+ },
90
+ createElement(tagName) {
91
+ const attributes = {}
92
+ return {
93
+ tagName: String(tagName).toUpperCase(),
94
+ parentNode: null,
95
+ onload: null,
96
+ onerror: null,
97
+ src: '',
98
+ async: false,
99
+ setAttribute(name, value) {
100
+ attributes[name] = value
101
+ },
102
+ getAttribute(name) {
103
+ return attributes[name]
104
+ },
105
+ remove() {
106
+ const index = scripts.indexOf(this)
107
+ if (index >= 0) scripts.splice(index, 1)
108
+ },
109
+ }
110
+ },
111
+ }
112
+ return { document, scripts }
113
+ }
114
+
115
+ function createFakeWindow() {
116
+ return {
117
+ setTimeout() {
118
+ /* 测试中不真正排队,避免 10s 的 revoke 定时器拖住进程 */
119
+ },
120
+ requestAnimationFrame() {},
121
+ location: { protocol: 'http:', host: '127.0.0.1' },
122
+ }
123
+ }
124
+
125
+ /** 装配一份全新的 G(模块级单例状态随 factory 调用重新创建)。 */
126
+ function createGufaModule(options = {}) {
127
+ const runtime = createRuntime()
128
+ const fakeDom = createFakeDom()
129
+ const fakeWindow = options.window ?? createFakeWindow()
130
+ const fakeGlobal = options.globalThis ?? {}
131
+ const factory = new Function(
132
+ '__react', 'window', 'document', 'globalThis', 'Blob', 'URL', 'Worker',
133
+ [
134
+ 'const React = __react',
135
+ 'const h = React.createElement',
136
+ 'const { useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect } = React',
137
+ source,
138
+ 'return { G }',
139
+ ].join('\n'),
140
+ )
141
+ const { G } = factory(
142
+ runtime.react,
143
+ fakeWindow,
144
+ options.document ?? fakeDom.document,
145
+ fakeGlobal,
146
+ options.Blob,
147
+ options.URL,
148
+ options.Worker,
149
+ )
150
+ return { G, runtime, window: fakeWindow, document: options.document ?? fakeDom.document, scripts: fakeDom.scripts }
151
+ }
152
+
153
+ /** 模拟一个已注册的 Monaco AMD loader。 */
154
+ function createFakeMonacoLoader(window, monaco, configs) {
155
+ const loader = (ids, success) => {
156
+ if (Array.isArray(ids) && ids[0] === 'vs/editor/editor.main') {
157
+ window.monaco = monaco
158
+ success()
159
+ }
160
+ }
161
+ loader.config = (options) => {
162
+ configs.push(options)
163
+ }
164
+ loader.reset = () => {}
165
+ loader.getConfig = () => ({})
166
+ return loader
167
+ }
168
+
169
+ /* ── 用例收集 ────────────────────────────────────────────────────────── */
170
+ const cases = []
171
+ function test(name, fn) {
172
+ cases.push({ name, fn })
173
+ }
174
+
175
+ /** 遍历 React 元素树。 */
176
+ function walk(node, visit) {
177
+ if (node === null || node === undefined) return
178
+ if (Array.isArray(node)) {
179
+ for (const child of node) walk(child, visit)
180
+ return
181
+ }
182
+ if (typeof node !== 'object' || node.type === undefined) return
183
+ visit(node)
184
+ for (const child of node.children ?? []) walk(child, visit)
185
+ }
186
+
187
+ function findAll(elements, predicate) {
188
+ const found = []
189
+ walk(elements, (element) => {
190
+ if (predicate(element)) found.push(element)
191
+ })
192
+ return found
193
+ }
194
+
195
+ /* ══════════════════════════════════════════════════════════════════════
196
+ * 1. 模块装配 / 导出
197
+ * ══════════════════════════════════════════════════════════════════════ */
198
+ const baseModule = createGufaModule()
199
+ const G = baseModule.G
200
+ const runtime = baseModule.runtime
201
+
202
+ function renderPane(props) {
203
+ runtime.reset()
204
+ runtime.beginRender()
205
+ return G.EditorPane(props)
206
+ }
207
+
208
+ const baseProps = {
209
+ tabs: [],
210
+ activePath: null,
211
+ theme: 'dark',
212
+ root: '/Users/jeff/demo',
213
+ onSelectTab() {},
214
+ onCloseTab() {},
215
+ onChange() {},
216
+ onSave() {},
217
+ onCursor() {},
218
+ }
219
+
220
+ test('模块可执行且导出 G.EditorPane(函数)', () => {
221
+ assert.equal(typeof G.EditorPane, 'function')
222
+ assert.equal(typeof G.EditorPane.internals, 'object')
223
+ assert.equal(Object.isFrozen(G.EditorPane.internals), true)
224
+ })
225
+
226
+ test('源码不 import/require 任何 Harness Client 包', () => {
227
+ assert.doesNotMatch(editorSource, /^\s*import\s/m)
228
+ assert.doesNotMatch(editorSource, /from\s+['"]@deepseek-ai\//)
229
+ assert.doesNotMatch(editorSource, /(^|[^.\w])require\s*\(\s*['"]/)
230
+ })
231
+
232
+ /* ══════════════════════════════════════════════════════════════════════
233
+ * 2. 纯函数:URI / 光标推进 / 主题 / 选项 / worker 脚本
234
+ * ══════════════════════════════════════════════════════════════════════ */
235
+ test('model URI = file:///<root>/<path>(root 为项目绝对路径)', () => {
236
+ const { modelUri } = G.EditorPane.internals
237
+ const captured = []
238
+ const fakeMonaco = {
239
+ Uri: {
240
+ file(absolute) {
241
+ captured.push(absolute)
242
+ return { scheme: 'file', path: absolute, toString: () => `file://${absolute}` }
243
+ },
244
+ },
245
+ }
246
+ const uri = modelUri(fakeMonaco, '/Users/jeff/demo', 'src/a.ts')
247
+ assert.deepEqual(captured, ['/Users/jeff/demo/src/a.ts'])
248
+ assert.equal(uri.toString(), 'file:///Users/jeff/demo/src/a.ts')
249
+ captured.length = 0
250
+ modelUri(fakeMonaco, '', 'README.md')
251
+ assert.deepEqual(captured, ['/README.md'])
252
+ captured.length = 0
253
+ modelUri(fakeMonaco, '/Users/jeff/demo/', '/lib\\b.js')
254
+ assert.deepEqual(captured, ['/Users/jeff/demo/lib/b.js'])
255
+ })
256
+
257
+ test('插入文本后的光标位置推进正确', () => {
258
+ const { advancePosition } = G.EditorPane.internals
259
+ assert.deepEqual(advancePosition({ lineNumber: 3, column: 5 }, 'abc'), { lineNumber: 3, column: 8 })
260
+ assert.deepEqual(advancePosition({ lineNumber: 3, column: 5 }, 'x\nyy'), { lineNumber: 4, column: 3 })
261
+ assert.deepEqual(advancePosition({ lineNumber: 1, column: 1 }, 'a\n'), { lineNumber: 2, column: 1 })
262
+ })
263
+
264
+ test('主题映射 dark→vs-dark / light→vs', () => {
265
+ const { monacoTheme } = G.EditorPane.internals
266
+ assert.equal(monacoTheme('dark'), 'vs-dark')
267
+ assert.equal(monacoTheme('light'), 'vs')
268
+ assert.equal(monacoTheme(undefined), 'vs')
269
+ })
270
+
271
+ test('编辑器选项:automaticLayout 开、minimap/overview ruler 关', () => {
272
+ const options = G.EditorPane.internals.editorOptions('dark')
273
+ assert.equal(options.automaticLayout, true)
274
+ assert.equal(options.minimap.enabled, false)
275
+ assert.equal(options.overviewRulerLanes, 0)
276
+ assert.equal(options.renderOverviewRuler, false)
277
+ assert.equal(options.theme, 'vs-dark')
278
+ assert.equal(G.EditorPane.internals.editorOptions('light').theme, 'vs')
279
+ })
280
+
281
+ test('worker 启动脚本:先加载 loader 并配置 paths.vs,再执行 workerMain', () => {
282
+ const base = G.asset('monaco')
283
+ const script = G.EditorPane.internals.workerScript(base)
284
+ assert.ok(script.includes(`importScripts(${JSON.stringify(`${base}/loader.js`)})`), '应先 importScripts loader.js')
285
+ assert.ok(script.includes(`require.config({ paths: { vs: ${JSON.stringify(base)} } })`), '应配置 paths.vs')
286
+ assert.ok(script.includes(`${base}/base/worker/workerMain.js`), '应执行 workerMain.js')
287
+ assert.ok(script.indexOf('loader.js') < script.indexOf('workerMain.js'), 'loader 必须先于 workerMain')
288
+ })
289
+
290
+ test('language 回退:Monaco 未注册的 id(makefile/cmake)按 plaintext 打开', () => {
291
+ const { resolveLanguage } = G.EditorPane.internals
292
+ const fakeMonaco = { languages: { getLanguages: () => [{ id: 'typescript' }, { id: 'plaintext' }] } }
293
+ assert.equal(resolveLanguage(fakeMonaco, 'typescript'), 'typescript')
294
+ assert.equal(resolveLanguage(fakeMonaco, 'makefile'), undefined)
295
+ assert.equal(resolveLanguage(fakeMonaco, 'cmake'), undefined)
296
+ assert.equal(resolveLanguage(fakeMonaco, ''), undefined)
297
+ assert.equal(resolveLanguage(fakeMonaco, undefined), undefined)
298
+ })
299
+
300
+ /* ══════════════════════════════════════════════════════════════════════
301
+ * 3. 渲染分支
302
+ * ══════════════════════════════════════════════════════════════════════ */
303
+ test('无打开文件时渲染 Empty(noFile),不渲染 textarea', () => {
304
+ const tree = renderPane(baseProps)
305
+ const empties = findAll(tree, (element) => element.type === G.Empty)
306
+ assert.equal(empties.length, 1)
307
+ assert.equal(empties[0].props.title, G.t('editor.noFile'))
308
+ assert.equal(findAll(tree, (element) => element.type === 'textarea').length, 0)
309
+ const pane = findAll(tree, (element) => element.props.className === 'gufa-editor-pane')
310
+ assert.equal(pane.length, 1)
311
+ assert.equal(pane[0].props.style.position, 'absolute')
312
+ assert.equal(pane[0].props.style.inset, 0)
313
+ })
314
+
315
+ test('二进制文件渲染 Empty(binary)', () => {
316
+ const tree = renderPane({
317
+ ...baseProps,
318
+ tabs: [{ path: 'a.png', content: '', language: 'plaintext', binary: true }],
319
+ activePath: 'a.png',
320
+ })
321
+ const empties = findAll(tree, (element) => element.type === G.Empty)
322
+ assert.equal(empties.length, 1)
323
+ assert.equal(empties[0].props.title, G.t('editor.binary'))
324
+ })
325
+
326
+ test('超大文件渲染 Empty(tooLarge)', () => {
327
+ const tree = renderPane({
328
+ ...baseProps,
329
+ tabs: [{ path: 'big.log', content: '', language: 'plaintext', tooLarge: true }],
330
+ activePath: 'big.log',
331
+ })
332
+ const empties = findAll(tree, (element) => element.type === G.Empty)
333
+ assert.equal(empties.length, 1)
334
+ assert.equal(empties[0].props.title, G.t('editor.tooLarge'))
335
+ })
336
+
337
+ test('普通文件:渲染 Monaco 挂载点 + 加载占位,不渲染空状态/降级 textarea', () => {
338
+ const tree = renderPane({
339
+ ...baseProps,
340
+ tabs: [{ path: 'src/a.ts', content: 'const a = 1\n', language: 'typescript' }],
341
+ activePath: 'src/a.ts',
342
+ })
343
+ const hosts = findAll(tree, (element) => element.props.className === 'gufa-monaco-host')
344
+ assert.equal(hosts.length, 1)
345
+ assert.equal(hosts[0].props.style.position, 'absolute')
346
+ assert.equal(findAll(tree, (element) => element.type === G.Empty).length, 0)
347
+ assert.equal(findAll(tree, (element) => element.type === 'textarea').length, 0)
348
+ const loadingTexts = []
349
+ walk(tree, (element) => {
350
+ for (const child of element.children ?? []) {
351
+ if (typeof child === 'string' && child === G.t('editor.loading')) loadingTexts.push(child)
352
+ }
353
+ })
354
+ assert.equal(loadingTexts.length, 1, '首次挂载应显示「加载编辑器…」占位')
355
+ })
356
+
357
+ test('tabs 为 undefined / activePath 悬空时安全渲染', () => {
358
+ const tree = renderPane({ ...baseProps, tabs: undefined, activePath: 'missing.ts' })
359
+ assert.equal(findAll(tree, (element) => element.props.className === 'gufa-editor-pane').length, 1)
360
+ const empties = findAll(tree, (element) => element.type === G.Empty)
361
+ assert.equal(empties.length, 1)
362
+ assert.equal(empties[0].props.title, G.t('editor.noFile'))
363
+ })
364
+
365
+ /* ══════════════════════════════════════════════════════════════════════
366
+ * 4. Monaco loader 状态机(fake DOM / fake AMD loader)
367
+ * ══════════════════════════════════════════════════════════════════════ */
368
+ test('无 AMD loader:注入 loader.js → onload 后 require.config → 成功;不重复注入', async () => {
369
+ const module = createGufaModule()
370
+ const { G: localG, window: fakeWindow, scripts } = module
371
+ const internals = localG.EditorPane.internals
372
+ const configs = []
373
+ const fakeMonaco = { editor: { create() {}, getModel() {} } }
374
+
375
+ const pending = internals.loadMonaco()
376
+ assert.equal(scripts.length, 1, '应注入一个 loader.js')
377
+ assert.equal(scripts[0].src, '/gufa-code/asset/monaco/loader.js')
378
+ assert.equal(scripts[0].getAttribute('data-gufa-monaco-loader'), '1')
379
+
380
+ /* 模拟 loader.js 执行:挂上 window.require */
381
+ fakeWindow.require = createFakeMonacoLoader(fakeWindow, fakeMonaco, configs)
382
+ scripts[0].onload()
383
+ const monaco = await pending
384
+ assert.equal(monaco, fakeMonaco)
385
+ assert.deepEqual(configs[0], { paths: { vs: '/gufa-code/asset/monaco' } })
386
+ assert.equal(typeof fakeWindow.MonacoEnvironment.getWorker, 'function', '应安装 worker 工厂')
387
+
388
+ /* 成功后再次加载:命中缓存,不注入新脚本、不重复 config */
389
+ const again = await internals.loadMonaco()
390
+ assert.equal(again, fakeMonaco)
391
+ assert.equal(scripts.length, 1)
392
+ assert.equal(configs.length, 1)
393
+ })
394
+
395
+ test('已有 Monaco loader(其他插件加载过):跳过注入,直接 require editor.main', async () => {
396
+ const module = createGufaModule()
397
+ const { G: localG, window: fakeWindow, scripts } = module
398
+ const configs = []
399
+ const fakeMonaco = { editor: { create() {}, getModel() {} } }
400
+ fakeWindow.require = createFakeMonacoLoader(fakeWindow, fakeMonaco, configs)
401
+
402
+ const monaco = await localG.EditorPane.internals.loadMonaco()
403
+ assert.equal(monaco, fakeMonaco)
404
+ assert.equal(scripts.length, 0, '不应重复注入 loader.js')
405
+ assert.deepEqual(configs[0], { paths: { vs: '/gufa-code/asset/monaco' } })
406
+ })
407
+
408
+ test('已有其他 getWorker 配置时不覆盖', async () => {
409
+ const module = createGufaModule()
410
+ const { G: localG, window: fakeWindow } = module
411
+ const sentinel = () => {}
412
+ fakeWindow.MonacoEnvironment = { getWorker: sentinel }
413
+ fakeWindow.require = createFakeMonacoLoader(fakeWindow, { editor: { create() {} } }, [])
414
+ await localG.EditorPane.internals.loadMonaco()
415
+ assert.equal(fakeWindow.MonacoEnvironment.getWorker, sentinel)
416
+ })
417
+
418
+ test('loader.js 加载失败:拒绝并清理脚本,降级可用;失败缓存不重复注入', async () => {
419
+ const module = createGufaModule()
420
+ const { G: localG, scripts } = module
421
+ const internals = localG.EditorPane.internals
422
+
423
+ const pending = internals.loadMonaco()
424
+ assert.equal(scripts.length, 1)
425
+ scripts[0].onerror({ type: 'error' })
426
+ await assert.rejects(pending, /Monaco loader\.js 加载失败/)
427
+ assert.equal(scripts.length, 0, '失败后应移除残留脚本')
428
+
429
+ /* 失败缓存:再次挂载不重新注入脚本 */
430
+ await assert.rejects(internals.loadMonaco(), /Monaco loader\.js 加载失败/)
431
+ assert.equal(scripts.length, 0)
432
+
433
+ /* 显式重试:允许重新注入一次 */
434
+ internals.resetMonacoFailure()
435
+ const retry = internals.loadMonaco()
436
+ assert.equal(scripts.length, 1, '重试应重新注入 loader.js')
437
+ /* 模拟 loader.js 执行但没有挂 window.require(全局 module 污染等) */
438
+ scripts[0].onload()
439
+ await assert.rejects(retry, /没有可用的 Monaco AMD loader/)
440
+ })
441
+
442
+ test('window.require 被非 Monaco 的 AMD loader 占用:拒载,不注入脚本', async () => {
443
+ const module = createGufaModule()
444
+ const { G: localG, window: fakeWindow, scripts } = module
445
+ const requireJsLike = function () {}
446
+ requireJsLike.config = () => {}
447
+ fakeWindow.require = requireJsLike
448
+ await assert.rejects(localG.EditorPane.internals.loadMonaco(), /非 Monaco 的 AMD loader/)
449
+ assert.equal(scripts.length, 0)
450
+ })
451
+
452
+ test('worker 工厂在 worker 内先加载 loader、配置 paths.vs,再执行 workerMain', () => {
453
+ const blobs = []
454
+ const workers = []
455
+ class FakeBlob {
456
+ constructor(parts) {
457
+ this.parts = parts
458
+ }
459
+ }
460
+ const fakeURL = {
461
+ createObjectURL(blob) {
462
+ blobs.push(blob)
463
+ return 'blob:gufa-worker'
464
+ },
465
+ revokeObjectURL() {},
466
+ }
467
+ class FakeWorker {
468
+ constructor(url, options) {
469
+ this.url = url
470
+ this.options = options
471
+ workers.push(this)
472
+ }
473
+ }
474
+ const module = createGufaModule({
475
+ window: { ...createFakeWindow(), MonacoEnvironment: undefined },
476
+ Blob: FakeBlob,
477
+ URL: fakeURL,
478
+ Worker: FakeWorker,
479
+ })
480
+ const { G: localG, window: fakeWindow } = module
481
+ const configs = []
482
+ fakeWindow.require = createFakeMonacoLoader(fakeWindow, { editor: { create() {} } }, configs)
483
+ return localG.EditorPane.internals.loadMonaco().then(() => {
484
+ const environment = fakeWindow.MonacoEnvironment
485
+ assert.equal(typeof environment.getWorker, 'function')
486
+ const worker = environment.getWorker('workerMain.js', 'typescript')
487
+ assert.equal(worker.url, 'blob:gufa-worker')
488
+ assert.equal(worker.options.name, 'typescript')
489
+ assert.equal(blobs.length, 1)
490
+ const text = blobs[0].parts.join('\n')
491
+ assert.ok(text.includes('/gufa-code/asset/monaco/loader.js'), 'worker 内应加载 loader.js')
492
+ assert.ok(text.includes(`require.config({ paths: { vs: ${JSON.stringify('/gufa-code/asset/monaco')} } })`))
493
+ assert.ok(text.includes('/gufa-code/asset/monaco/base/worker/workerMain.js'))
494
+ })
495
+ })
496
+
497
+ /* ── 依次执行(支持 async 用例) ─────────────────────────────────────── */
498
+ console.log('20-editor 自测:')
499
+ const failed = []
500
+ let passed = 0
501
+ for (const item of cases) {
502
+ try {
503
+ await item.fn()
504
+ passed += 1
505
+ console.log(` ✓ ${item.name}`)
506
+ } catch (error) {
507
+ failed.push({ name: item.name, error })
508
+ console.error(` ✗ ${item.name}`)
509
+ console.error(String(error?.stack ?? error).split('\n').slice(0, 6).join('\n'))
510
+ }
511
+ }
512
+
513
+ console.log(`\n${passed}/${cases.length} 通过`)
514
+ if (failed.length > 0) {
515
+ process.exitCode = 1
516
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * 离线验证 host/project.js 的 workspaces.sessions 是否与 DSH 经典侧边栏一致。
3
+ * 数据源:磁盘真实数据(~/.dsh/storages/workspace.json + session_projcache)。
4
+ * 校验三个场景:
5
+ * 1) 正常:与经典侧边栏 8 条标题完全一致
6
+ * 2) 标题服务异常:不能清空列表(退回成员列表,11 条)
7
+ * 3) 新建 blank 会话:当前 blank 会话可见,其他 blank 仍隐藏(9 条)
8
+ * 用法:node scripts/verify-workspace-sessions.mjs
9
+ */
10
+ import { readFileSync, existsSync } from 'node:fs'
11
+ import { join } from 'node:path'
12
+ import { register } from '../host/project.js'
13
+
14
+ const DSH = join(process.env.HOME ?? '/Users/jeff', '.dsh')
15
+ const WORKSPACE_ID = 'fa5d306e-891b-49a2-aa86-eab5af86627c'
16
+ const TARGET = '/Users/jeff/dsh'
17
+ const CURRENT = 'session-e0ec4210-fe5b-4587-a0c3-905eb6e8cd0d'
18
+ const NEW_ID = 'session-newblank-0000'
19
+
20
+ const snapshot = JSON.parse(readFileSync(join(DSH, 'storages/workspace.json'), 'utf8'))
21
+ const workspaces = Object.entries(snapshot.tables.workspaces).map(([id, value]) => ({
22
+ workspaceId: id,
23
+ path: value.path,
24
+ title: value.title,
25
+ sessionIds: value.sessionIds,
26
+ }))
27
+ const archivedSessionIds = snapshot.global.archivedSessionIds
28
+
29
+ const cacheOf = (sid) => {
30
+ const file = join(DSH, 'storages/session_projcache/sessions', `${sid}.json`)
31
+ return existsSync(file) ? JSON.parse(readFileSync(file, 'utf8')) : null
32
+ }
33
+ const titleOf = (sid) => {
34
+ const value = cacheOf(sid)?.record?.rows?.title?.val
35
+ return typeof value === 'string' ? value : ''
36
+ }
37
+ const records = [...new Set(workspaces.flatMap((workspace) => workspace.sessionIds))].map((sessionId) => {
38
+ const cache = cacheOf(sessionId)
39
+ return {
40
+ header: {
41
+ id: sessionId,
42
+ cwd: cache?.record?.identity?.cwd ?? TARGET,
43
+ createdAt: cache?.record?.identity?.createdAt ?? 0,
44
+ isSeeded: cache?.record?.identity?.isSeeded === true,
45
+ },
46
+ }
47
+ })
48
+
49
+ function makeRouter(workspaceList, { failTitles = false } = {}) {
50
+ const router = { methods: new Map(), method(name, fn) { router.methods.set(name, fn) } }
51
+ const env = {
52
+ projects: {},
53
+ ctx: {
54
+ get(name) {
55
+ if (name === 'workspaceController') {
56
+ return {
57
+ async *follow() {
58
+ yield { type: 'baseline', value: { items: workspaceList, archivedSessionIds, pinnedSessionIds: [] } }
59
+ },
60
+ }
61
+ }
62
+ if (name === 'workspaceRegistry') return { list: () => [] }
63
+ if (name === 'sessionQuery') {
64
+ return {
65
+ listSessions: async () => records,
66
+ async readTitleSnapshots(ids) {
67
+ if (failTitles) throw new Error('simulated title failure')
68
+ // 与真实 Host 行为一致:blank 会话(title 为 null)不会出现在标题快照结果里
69
+ const out = {}
70
+ for (const id of ids) {
71
+ const title = titleOf(id)
72
+ if (title === '') continue
73
+ out[id] = { title, updatedAt: cacheOf(id)?.record?.identity?.createdAt ?? 0 }
74
+ }
75
+ return out
76
+ },
77
+ async readTitle() {
78
+ if (failTitles) throw new Error('simulated title failure')
79
+ return undefined
80
+ },
81
+ }
82
+ }
83
+ if (name === 'sessions') return { get: () => undefined }
84
+ if (name === 'sessionTitle') return { get: () => undefined }
85
+ return undefined
86
+ },
87
+ },
88
+ }
89
+ register(router, env)
90
+ return router
91
+ }
92
+
93
+ const expected = [
94
+ '古法编程AI辅助IDE插件',
95
+ 'vivo推送RegID是什么',
96
+ 'GitLab 网页系统是否用 PHP',
97
+ 'DeepSeek 多模态支持多少张图片',
98
+ 'BOOK-AI项目',
99
+ '基于Recordly开源项目开发Web录屏应用',
100
+ '玄幻未来科技与东方审美皮肤',
101
+ '命令行使用dsh指南',
102
+ ].sort()
103
+
104
+ let allOk = true
105
+
106
+ // 场景 1:正常
107
+ const router = makeRouter(workspaces)
108
+ const value = await router.methods.get('workspaces.sessions')({ path: TARGET, currentSessionId: CURRENT })
109
+ const got = (value.sessions ?? []).map((row) => row.title).sort()
110
+ const ok1 = got.length === expected.length && got.every((title, index) => title === expected[index])
111
+ allOk = allOk && ok1
112
+ console.log(`normal rows=${got.length} expect=8 ${ok1 ? 'PASS 与经典侧边栏一致' : 'FAIL'}`)
113
+ if (!ok1) console.log(' got=', JSON.stringify(got))
114
+
115
+ // 场景 2:标题服务异常
116
+ const failRouter = makeRouter(workspaces, { failTitles: true })
117
+ const failValue = await failRouter.methods.get('workspaces.sessions')({ path: TARGET, currentSessionId: CURRENT })
118
+ const failCount = (failValue.sessions ?? []).length
119
+ const ok2 = failCount === 11
120
+ allOk = allOk && ok2
121
+ console.log(`failover rows=${failCount} expect=11 ${ok2 ? 'PASS 标题异常时不丢列表' : 'FAIL 列表被清空'}`)
122
+
123
+ // 场景 3:新建 blank 会话作为当前会话
124
+ const newWorkspaces = workspaces.map((workspace) => workspace.workspaceId === WORKSPACE_ID
125
+ ? { ...workspace, sessionIds: [...workspace.sessionIds, NEW_ID] }
126
+ : workspace)
127
+ const newRouter = makeRouter(newWorkspaces)
128
+ const newValue = await newRouter.methods.get('workspaces.sessions')({ path: TARGET, currentSessionId: NEW_ID })
129
+ const newRows = newValue.sessions ?? []
130
+ const ok3 = newRows.length === 9 && newRows.some((row) => row.id === NEW_ID)
131
+ allOk = allOk && ok3
132
+ console.log(`new-session rows=${newRows.length} expect=9 ${ok3 ? 'PASS blank 当前会话可见' : 'FAIL'}`)
133
+
134
+ if (allOk) console.log('\nALL PASS')
135
+ process.exit(allOk ? 0 : 1)