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,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)
|