dsh-gufa-code 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -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-ide.jpg +0 -0
  9. package/host/assets.js +31 -0
  10. package/host/chat.js +523 -0
  11. package/host/context.js +438 -0
  12. package/host/dev.js +35 -0
  13. package/host/fs.js +306 -0
  14. package/host/git.js +532 -0
  15. package/host/project.js +305 -0
  16. package/host/ssh.js +54 -0
  17. package/host/terminal-pty.js +237 -0
  18. package/host/terminal.js +947 -0
  19. package/icon.svg +18 -0
  20. package/index.js +63 -0
  21. package/locale/en.json +121 -0
  22. package/locale/zh.json +121 -0
  23. package/package.json +67 -4
  24. package/screenshots.json +3 -0
  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,284 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Host HTTP / WebSocket 端到端自测(不启动 Harness):
4
+ * 用假 ctx 把真实 host 半(index.js → router → host/*.js)挂到本地 http 服务上,
5
+ * 走真实 fetch / ws 验证:鉴权围栏、项目/文件/Git API、静态资产、SSE、错误响应、WS 升级。
6
+ *
7
+ * 用法:node scripts/selftest-http.mjs
8
+ */
9
+ import { createServer } from 'node:http'
10
+ import { PassThrough } from 'node:stream'
11
+ import { execFileSync } from 'node:child_process'
12
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
13
+ import { tmpdir } from 'node:os'
14
+ import { join } from 'node:path'
15
+ import WebSocket from 'ws'
16
+
17
+ import { ProjectRegistry } from '../host/context.js'
18
+
19
+ let passed = 0
20
+ let failed = 0
21
+ function check(name, condition, detail) {
22
+ if (condition) {
23
+ passed += 1
24
+ console.log(` ok ${name}`)
25
+ } else {
26
+ failed += 1
27
+ console.error(` FAIL ${name}${detail === undefined ? '' : ` — ${detail}`}`)
28
+ }
29
+ }
30
+
31
+ /* ── 假 ctx:把路由挂到真实 http 服务 ─────────────────────────────────── */
32
+ const httpRoutes = []
33
+ const upgradeRoutes = []
34
+ const effects = []
35
+
36
+ /* 假 Host 服务:subprocess(PTY 回显)与 llm(流式两段式输出)。
37
+ 让 host/terminal.js 与 host/chat.js 的完整路径在无 Harness 环境下也能端到端验证。 */
38
+ const fakeTerminals = new Map()
39
+ const fakeLlmRequests = []
40
+ const fakeSubprocess = {
41
+ async terminalEnvironment() {
42
+ return { defaultShell: '/bin/zsh' }
43
+ },
44
+ async spawnTerminal(spec) {
45
+ const output = new PassThrough()
46
+ const id = `fake-${fakeTerminals.size + 1}`
47
+ const handle = {
48
+ pid: 9000 + fakeTerminals.size,
49
+ cols: spec?.cols ?? 80,
50
+ rows: spec?.rows ?? 24,
51
+ output,
52
+ write: async (data) => {
53
+ output.write(data)
54
+ return true
55
+ },
56
+ resize: () => {},
57
+ terminate: async () => {
58
+ output.end()
59
+ },
60
+ done: new Promise(() => {}),
61
+ }
62
+ fakeTerminals.set(id, handle)
63
+ setTimeout(() => output.write('gufa-fake-prompt$ '), 10)
64
+ return handle
65
+ },
66
+ }
67
+ const fakeLlm = {
68
+ stream(options) {
69
+ fakeLlmRequests.push(options)
70
+ return (async function* stream() {
71
+ yield { type: 'block-start', index: 0, blockType: 'text' }
72
+ yield { type: 'text-delta', index: 0, text: '你好,' }
73
+ yield { type: 'text-delta', index: 0, text: '这是古法 AI。' }
74
+ yield { type: 'block-end', index: 0, block: { type: 'text', text: '你好,这是古法 AI。' } }
75
+ yield { type: 'finish', reason: { kind: 'stop' } }
76
+ })()
77
+ },
78
+ }
79
+ const fakeDefaultModel = {
80
+ currentSelection() {
81
+ return { provider: 'deepseek-official', model: 'deepseek-flash' }
82
+ },
83
+ }
84
+
85
+ const ctx = {
86
+ get(name) {
87
+ if (name === 'subprocess') return fakeSubprocess
88
+ if (name === 'llm') return fakeLlm
89
+ if (name === 'agentDefaultModel') return fakeDefaultModel
90
+ return undefined
91
+ },
92
+ effect(fn) {
93
+ const dispose = fn()
94
+ effects.push(typeof dispose === 'function' ? dispose : () => {})
95
+ return () => {}
96
+ },
97
+ webServer: {
98
+ register(route) {
99
+ const entry = { ...route }
100
+ httpRoutes.push(entry)
101
+ return () => {
102
+ const index = httpRoutes.indexOf(entry)
103
+ if (index >= 0) httpRoutes.splice(index, 1)
104
+ }
105
+ },
106
+ registerUpgrade(route) {
107
+ const entry = { ...route }
108
+ upgradeRoutes.push(entry)
109
+ return () => {
110
+ const index = upgradeRoutes.indexOf(entry)
111
+ if (index >= 0) upgradeRoutes.splice(index, 1)
112
+ }
113
+ },
114
+ },
115
+ connection: { requestRejection: () => undefined },
116
+ logger: { warn: () => {}, error: () => {} },
117
+ }
118
+
119
+ const plugin = await import('../index.js')
120
+ check('plugin entry exposes apply', typeof plugin.apply === 'function' && plugin.name === 'dsh-gufa-code')
121
+ plugin.apply(ctx)
122
+
123
+ const server = createServer((req, res) => {
124
+ const pathname = new URL(req.url ?? '/', 'http://x').pathname
125
+ const route = httpRoutes
126
+ .filter((item) => (item.kind === 'prefix' ? pathname.startsWith(item.path) : pathname === item.path))
127
+ .sort((a, b) => b.path.length - a.path.length)[0]
128
+ if (route === undefined) {
129
+ res.writeHead(404, { 'content-type': 'text/plain' })
130
+ res.end('not found')
131
+ return
132
+ }
133
+ Promise.resolve(route.handler(req, res)).catch((error) => {
134
+ try {
135
+ res.writeHead(500)
136
+ res.end(String(error))
137
+ } catch {
138
+ /* ignore */
139
+ }
140
+ })
141
+ })
142
+ server.on('upgrade', (req, socket, head) => {
143
+ const pathname = new URL(req.url ?? '/', 'http://x').pathname
144
+ const route = upgradeRoutes.find((item) => item.path === pathname)
145
+ if (route === undefined) {
146
+ socket.destroy()
147
+ return
148
+ }
149
+ Promise.resolve(route.handler(req, socket, head)).catch(() => socket.destroy())
150
+ })
151
+
152
+ await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
153
+ const port = server.address().port
154
+ const base = `http://127.0.0.1:${port}`
155
+ const sandbox = mkdtempSync(join(tmpdir(), 'gufa-http-test-'))
156
+
157
+ async function api(method, body) {
158
+ const response = await fetch(`${base}/gufa-code/api/${method}`, {
159
+ method: 'POST',
160
+ headers: { 'content-type': 'application/json' },
161
+ body: JSON.stringify(body ?? {}),
162
+ })
163
+ const payload = await response.json().catch(() => ({}))
164
+ return { status: response.status, payload }
165
+ }
166
+
167
+ try {
168
+ /* 项目 / 文件 */
169
+ const created = await api('project.create', { parent: sandbox, name: 'http-project' })
170
+ check('project.create ok', created.status === 200 && created.payload.ok === true && created.payload.value.name === 'http-project', JSON.stringify(created.payload))
171
+ const root = created.payload.value.root
172
+ execFileSync('git', ['init', '-q'], { cwd: root })
173
+ await api('fs.create', { root, path: 'src', kind: 'dir' })
174
+ await api('fs.create', { root, path: 'src/app.ts', kind: 'file' })
175
+ const written = await api('fs.write', { root, path: 'src/app.ts', content: 'export const x = 1\n' })
176
+ check('fs.write ok', written.status === 200 && typeof written.payload.value.revision === 'string')
177
+ const listed = await api('fs.list', { root, path: '' })
178
+ check('fs.list sees dir', listed.payload.value.entries.some((entry) => entry.name === 'src'))
179
+ const unknown = await api('nope.nope', {})
180
+ check('unknown method 404', unknown.status === 404 && unknown.payload.error.code === 'not-found', String(unknown.status))
181
+ const unopened = await api('fs.list', { root: '/tmp', path: '' })
182
+ check('unopened root rejected', unopened.status === 403 && unopened.payload.error.code === 'project-not-open', JSON.stringify(unopened.payload))
183
+
184
+ /* Git */
185
+ const gitStatus = await api('git.status', { root })
186
+ check('git.status detects repo', gitStatus.payload.value.isGit === true, JSON.stringify(gitStatus.payload))
187
+ check('git.status lists change', gitStatus.payload.value.changes.some((change) => change.path === 'src/app.ts'))
188
+
189
+ /* 静态资产 */
190
+ const loader = await fetch(`${base}/gufa-code/asset/monaco/loader.js`)
191
+ const loaderText = await loader.text()
192
+ check('monaco loader served', loader.status === 200 && loader.headers.get('content-type').includes('javascript') && loaderText.length > 1000, String(loader.status))
193
+ const xterm = await fetch(`${base}/gufa-code/asset/xterm/xterm.js`)
194
+ check('xterm served', xterm.status === 200)
195
+ const traversal = await fetch(`${base}/gufa-code/asset/monaco/..%2f..%2f..%2fpackage.json`)
196
+ check('asset traversal rejected', traversal.status === 404 || traversal.status === 403, String(traversal.status))
197
+
198
+ /* chat.info / SSE */
199
+ const info = await api('chat.info', {})
200
+ check('chat.info ok', info.status === 200 && info.payload.ok === true, JSON.stringify(info.payload))
201
+ const streamResponse = await fetch(`${base}/gufa-code/api/chat.stream`, {
202
+ method: 'POST',
203
+ headers: { 'content-type': 'application/json' },
204
+ body: JSON.stringify({ messages: [{ role: 'user', content: 'hi' }], context: { root, path: 'src/app.ts', content: 'x' } }),
205
+ })
206
+ check('chat.stream sse content-type', streamResponse.status === 200 && (streamResponse.headers.get('content-type') ?? '').includes('text/event-stream'), String(streamResponse.status))
207
+ const reader = streamResponse.body?.getReader()
208
+ let firstFrame = ''
209
+ if (reader !== undefined) {
210
+ const { value } = await reader.read()
211
+ firstFrame = value === undefined ? '' : Buffer.from(value).toString('utf8')
212
+ await reader.cancel().catch(() => {})
213
+ }
214
+ check('chat.stream emits sse frame', firstFrame.startsWith('data: '), firstFrame.slice(0, 120))
215
+ const rest = reader === undefined ? '' : ''
216
+ void rest
217
+ check('chat.stream sends llm delta', firstFrame.includes('"type":"delta"') || firstFrame.includes('"type": "delta"'), firstFrame.slice(0, 160))
218
+ check('chat.stream called ctx.llm.stream', fakeLlmRequests.length === 1 && fakeLlmRequests[0].provider === 'deepseek-official', JSON.stringify(fakeLlmRequests.map((item) => ({ provider: item.provider, model: item.model }))))
219
+ check('chat.stream prompt embeds context', typeof fakeLlmRequests[0]?.messages?.[0]?.content?.[0]?.text === 'string' && fakeLlmRequests[0].messages[0].content[0].text.includes('src/app.ts'), 'prompt missing file context')
220
+
221
+ /* 终端:create + WS 往返(假 PTY 回显 input) */
222
+ const terminalCreated = await api('terminal.create', { root, cols: 80, rows: 24 })
223
+ if (terminalCreated.status === 200 && terminalCreated.payload.ok === true) {
224
+ const info2 = terminalCreated.payload.value
225
+ check('terminal.create returned id', typeof info2.id === 'string' && info2.id.length > 0, JSON.stringify(info2))
226
+ const wsResult = await new Promise((resolve) => {
227
+ const socket = new WebSocket(`ws://127.0.0.1:${port}/gufa-code/ws?channel=terminal&root=${encodeURIComponent(root)}&id=${encodeURIComponent(info2.id)}`)
228
+ const timer = setTimeout(() => {
229
+ try {
230
+ socket.close()
231
+ } catch {
232
+ /* ignore */
233
+ }
234
+ resolve({ ok: false, reason: 'timeout' })
235
+ }, 8000)
236
+ socket.on('message', (raw) => {
237
+ const message = JSON.parse(raw.toString())
238
+ if (message.type === 'output' && typeof message.data === 'string' && message.data.length > 0) {
239
+ clearTimeout(timer)
240
+ try {
241
+ socket.close()
242
+ } catch {
243
+ /* ignore */
244
+ }
245
+ resolve({ ok: true, sample: message.data.slice(0, 80) })
246
+ }
247
+ })
248
+ socket.on('open', () => {
249
+ socket.send(JSON.stringify({ type: 'input', data: 'echo gufa-terminal-ok\r' }))
250
+ })
251
+ socket.on('error', (error) => {
252
+ clearTimeout(timer)
253
+ resolve({ ok: false, reason: error.message })
254
+ })
255
+ socket.on('close', () => {
256
+ clearTimeout(timer)
257
+ resolve({ ok: false, reason: 'closed' })
258
+ })
259
+ })
260
+ check('terminal ws roundtrip', wsResult.ok, JSON.stringify(wsResult))
261
+ await api('terminal.close', { root, id: info2.id })
262
+ } else {
263
+ check('terminal.create ok', false, JSON.stringify(terminalCreated))
264
+ }
265
+
266
+ /* 鉴权围栏:模拟 connection 拒绝 */
267
+ ctx.connection.requestRejection = () => 401
268
+ const denied = await api('chat.info', {})
269
+ check('auth fence rejects', denied.status === 401, String(denied.status))
270
+ ctx.connection.requestRejection = () => undefined
271
+ } finally {
272
+ await new Promise((resolve) => server.close(resolve))
273
+ for (const dispose of effects.splice(0)) {
274
+ try {
275
+ dispose()
276
+ } catch {
277
+ /* ignore */
278
+ }
279
+ }
280
+ rmSync(sandbox, { recursive: true, force: true })
281
+ }
282
+
283
+ console.log(`\n${passed} passed, ${failed} failed`)
284
+ process.exit(failed === 0 ? 0 : 1)
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Router 自测:路由挂载、方法分发、WS/资产注册,以及 host 模块热重载(签名变化 → 重新加载并清理旧 scope)。
4
+ * 不启动 Harness,用最小的 ctx 假体。
5
+ * 用法:node scripts/selftest-router.mjs
6
+ */
7
+ import { createRouter } from '../host/context.js'
8
+
9
+ let passed = 0
10
+ let failed = 0
11
+ function check(name, condition, detail) {
12
+ if (condition) {
13
+ passed += 1
14
+ console.log(` ok ${name}`)
15
+ } else {
16
+ failed += 1
17
+ console.error(` FAIL ${name}${detail === undefined ? '' : ` — ${detail}`}`)
18
+ }
19
+ }
20
+
21
+ const effects = []
22
+ const routes = []
23
+ const upgrades = []
24
+ const ctx = {
25
+ effect(fn, label) {
26
+ const dispose = fn() ?? (() => {})
27
+ effects.push({ dispose, label })
28
+ return dispose
29
+ },
30
+ webServer: {
31
+ register(route) {
32
+ routes.push(route)
33
+ return () => {}
34
+ },
35
+ registerUpgrade(route) {
36
+ upgrades.push(route)
37
+ return () => {}
38
+ },
39
+ },
40
+ connection: { requestRejection: () => undefined },
41
+ }
42
+
43
+ const router = createRouter(ctx)
44
+ let signature = 'v1'
45
+ let loads = 0
46
+ let disposed = 0
47
+ const seen = []
48
+
49
+ router.setReloader(async () => {
50
+ loads += 1
51
+ const generation = loads
52
+ const { scope, registrar } = router.createScope()
53
+ registrar.method('ping', async (body) => {
54
+ seen.push(`method:${generation}`)
55
+ return { pong: body.x ?? null, generation }
56
+ })
57
+ registrar.stream('events', async (body, { send, close }) => {
58
+ send({ type: 'delta', text: String(generation) })
59
+ close()
60
+ })
61
+ registrar.ws('demo', () => () => {
62
+ disposed += 1
63
+ })
64
+ registrar.asset('demo', async (raw) => ({ path: raw, type: 'text/plain' }))
65
+ registrar.onDispose(() => {
66
+ disposed += 1
67
+ })
68
+ router.installScope(scope)
69
+ }, () => signature)
70
+
71
+ router.mount()
72
+ check('api route mounted', routes.some((route) => route.path === '/gufa-code/api'))
73
+ check('asset route mounted', routes.some((route) => route.path === '/gufa-code/asset'))
74
+ check('ws upgrade mounted', upgrades.some((route) => route.path === '/gufa-code/ws'))
75
+
76
+ const first = await router.__dispatch('ping', { x: 42 })
77
+ check('lazy first load dispatched', first.pong === 42 && first.generation === 1, JSON.stringify(first))
78
+ check('load count after first request', loads === 1, String(loads))
79
+ let missing = false
80
+ try {
81
+ await router.__dispatch('nope')
82
+ } catch (error) {
83
+ missing = error.code === 'not-found'
84
+ }
85
+ check('unknown method rejected', missing)
86
+
87
+ signature = 'v2'
88
+ await router.__dispatch('ping', { x: 7 })
89
+ check('signature change reloads modules', loads === 2, String(loads))
90
+ check('old scope disposed on reload', disposed >= 1, String(disposed))
91
+ const third = await router.__dispatch('ping', {})
92
+ check('new generation serves requests', third.generation === 2, JSON.stringify(third))
93
+ const stats = router.__stats()
94
+ check('scope stats', stats.methods === 1 && stats.streams === 1 && stats.sockets === 1 && stats.assets === 1, JSON.stringify(stats))
95
+
96
+ // 卸载清理
97
+ for (const { dispose } of effects) dispose()
98
+ check('teardown runs scope disposers', disposed >= 2, String(disposed))
99
+
100
+ console.log(`\n${passed} passed, ${failed} failed`)
101
+ process.exit(failed === 0 ? 0 : 1)