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