zen-gitsync 2.16.7 → 2.16.8
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/package.json +1 -1
- package/src/cli/ai/modelSetup.js +594 -594
- package/src/cli/ai/modelSetup.test.js +496 -496
- package/src/cli/ai/platformGuard.js +200 -200
- package/src/cli/ai/platformGuard.test.js +253 -253
- package/src/gitCommit.js +484 -484
- package/src/ui/public/assets/AgentView-DSmUynN-.js +4 -0
- package/src/ui/public/assets/AppVersionBadge-CcFagCnq.js +2 -0
- package/src/ui/public/assets/BranchSelector-DcYVBQJh.js +1 -0
- package/src/ui/public/assets/CommitForm-CkTI8Xyz.js +6 -0
- package/src/ui/public/assets/CommonDialog-BFyhw0Qj.js +1 -0
- package/src/ui/public/assets/EditorView-DuHkBSc3.js +0 -0
- package/src/ui/public/assets/{FlowExecutionViewer-DjtFCci0.js → FlowExecutionViewer-C063Qjmk.js} +1 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BeJRId5G.js +21 -0
- package/src/ui/public/assets/LogList-GSeER4cv.js +15 -0
- package/src/ui/public/assets/MindmapView-p7szLCtQ.js +2 -0
- package/src/ui/public/assets/MonitorView-Be6KEUCe.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-F1k5Km2p.js +1 -0
- package/src/ui/public/assets/{RemoteRepoCard-Yxfc0pxB.js → RemoteRepoCard-CSMFlWDl.js} +2 -2
- package/src/ui/public/assets/SourceMapView-Dg5NP_gR.js +3 -0
- package/src/ui/public/assets/{SvgIcon-32uxY152.js → SvgIcon-CYZGHQAE.js} +1 -1
- package/src/ui/public/assets/UserInputNode-3OAXiGVh.js +1 -0
- package/src/ui/public/assets/WorkbenchView-8xyxdWAg.js +28 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-Dvk21zJS.js → _plugin-vue_export-helper-DgLDQTWK.js} +1 -1
- package/src/ui/public/assets/{configStore-Bmoxm5fk.js → configStore-D5t05s4G.js} +1 -1
- package/src/ui/public/assets/element-plus-CyGhXd7X.js +25 -0
- package/src/ui/public/assets/element-plus-D3Dn7nX0.css +1 -0
- package/src/ui/public/assets/{flow-mindmap-E8TqjpXN.js → flow-mindmap-CUvJaYn2.js} +1 -1
- package/src/ui/public/assets/index-DDGvh1mp.js +68 -0
- package/src/ui/public/assets/{socket-io-Bcae2P7B.js → socket-io-BSgn8Jx2.js} +1 -1
- package/src/ui/public/assets/vendor-DVvq7qtS.css +1 -0
- package/src/ui/public/assets/{vendor-D79HTB0H.js → vendor-jF--I87-.js} +85 -41
- package/src/ui/public/assets/{vue-flow-BYcesAAH.js → vue-flow-CWmfpfPN.js} +1 -1
- package/src/ui/public/favicon.svg +75 -75
- package/src/ui/public/index.html +28 -28
- package/src/ui/public/logo.svg +74 -74
- package/src/ui/server/middleware/requestLogger.js +53 -53
- package/src/ui/server/routes/branchStatus.js +105 -105
- package/src/ui/server/routes/fileOpen.js +3 -2
- package/src/ui/server/routes/fs.js +791 -791
- package/src/ui/server/routes/git/diff.js +426 -426
- package/src/ui/server/routes/git/diffUtils.js +149 -149
- package/src/ui/server/routes/git/stash.js +550 -550
- package/src/ui/server/routes/git/tags.js +176 -176
- package/src/ui/server/routes/git.js +181 -181
- package/src/ui/server/routes/gitOps.js +1255 -1255
- package/src/ui/server/routes/process.js +86 -86
- package/src/ui/server/routes/status.js +67 -67
- package/src/ui/server/routes/workbench/pdfText.js +145 -145
- package/src/ui/server/utils/createSavePortToFile.js +48 -48
- package/src/ui/server/utils/instanceRegistry.js +282 -282
- package/src/ui/server/utils/startServerOnAvailablePort.js +116 -116
- package/src/utils/index.js +1173 -1173
- package/src/ui/public/assets/AgentView-DEUMkOst.js +0 -4
- package/src/ui/public/assets/AppVersionBadge-oo55wrtg.js +0 -2
- package/src/ui/public/assets/BranchSelector--__REaeG.js +0 -1
- package/src/ui/public/assets/CommitForm-CpUP5zax.js +0 -6
- package/src/ui/public/assets/CommonDialog-DbPF7rKY.js +0 -1
- package/src/ui/public/assets/EditorView-LZ_WHmfb.js +0 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-CKMLLhKM.js +0 -21
- package/src/ui/public/assets/LogList--c3ACWbL.js +0 -15
- package/src/ui/public/assets/MindmapView-B_VWP6CS.js +0 -2
- package/src/ui/public/assets/MonitorView-C9BEPazj.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-qLmpoh5m.js +0 -1
- package/src/ui/public/assets/SourceMapView-BXqR3S2Y.js +0 -3
- package/src/ui/public/assets/UserInputNode-B0xDrmk2.js +0 -1
- package/src/ui/public/assets/WorkbenchView-COtdz_io.js +0 -28
- package/src/ui/public/assets/ai-chat-ui-BsH9JBEz.js +0 -45
- package/src/ui/public/assets/ai-chat-ui-BwlJovCq.css +0 -1
- package/src/ui/public/assets/element-plus-CQrjg9us.css +0 -1
- package/src/ui/public/assets/element-plus-C_2iiXUS.js +0 -32
- package/src/ui/public/assets/index-Zn4T8oXe.js +0 -68
- package/src/ui/public/assets/vendor-CdOLCu7-.css +0 -1
- package/src/ui/server/.claude/codediff.txt +0 -8
|
@@ -1,253 +1,253 @@
|
|
|
1
|
-
// Copyright 2026 xz333221
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
//
|
|
15
|
-
// src/cli/ai/platformGuard.js 单元测试
|
|
16
|
-
// 覆盖纯函数(splitCommandSegments / extractCommandName / detectUnixOnlyCommands)
|
|
17
|
-
// 和异步主接口(guardCommand,注入 mock exec 避免 PATH 依赖)。
|
|
18
|
-
import { test, beforeEach } from 'node:test'
|
|
19
|
-
import assert from 'node:assert/strict'
|
|
20
|
-
import {
|
|
21
|
-
splitCommandSegments,
|
|
22
|
-
extractCommandName,
|
|
23
|
-
detectUnixOnlyCommands,
|
|
24
|
-
guardCommand,
|
|
25
|
-
checkCommandAvailability,
|
|
26
|
-
_clearAvailabilityCache,
|
|
27
|
-
UNIX_ONLY_COMMANDS,
|
|
28
|
-
} from './platformGuard.js'
|
|
29
|
-
|
|
30
|
-
// ── splitCommandSegments ──────────────────────
|
|
31
|
-
|
|
32
|
-
test('splitCommandSegments: 按管道切分', () => {
|
|
33
|
-
assert.deepEqual(
|
|
34
|
-
splitCommandSegments('npm test | tail -60'),
|
|
35
|
-
['npm test', 'tail -60'],
|
|
36
|
-
)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test('splitCommandSegments: 按 && 切分', () => {
|
|
40
|
-
assert.deepEqual(
|
|
41
|
-
splitCommandSegments('echo a && echo b'),
|
|
42
|
-
['echo a', 'echo b'],
|
|
43
|
-
)
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test('splitCommandSegments: 按 || 和 ; 切分', () => {
|
|
47
|
-
assert.deepEqual(
|
|
48
|
-
splitCommandSegments('echo a || echo b ; echo c'),
|
|
49
|
-
['echo a', 'echo b', 'echo c'],
|
|
50
|
-
)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test('splitCommandSegments: 空串/纯空白返回空数组', () => {
|
|
54
|
-
assert.deepEqual(splitCommandSegments(''), [])
|
|
55
|
-
assert.deepEqual(splitCommandSegments(' '), [])
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test('splitCommandSegments: 无操作符的命令返回单段', () => {
|
|
59
|
-
assert.deepEqual(splitCommandSegments('echo hello'), ['echo hello'])
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
// ── extractCommandName ────────────────────────
|
|
63
|
-
|
|
64
|
-
test('extractCommandName: 提取第一个 token 并小写', () => {
|
|
65
|
-
assert.equal(extractCommandName('TAIL -60'), 'tail')
|
|
66
|
-
assert.equal(extractCommandName('node -e "code"'), 'node')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test('extractCommandName: 跳过 sudo/doas 前缀', () => {
|
|
70
|
-
assert.equal(extractCommandName('sudo tail -f log'), 'tail')
|
|
71
|
-
assert.equal(extractCommandName('doas grep pattern'), 'grep')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test('extractCommandName: 空串返回空', () => {
|
|
75
|
-
assert.equal(extractCommandName(''), '')
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
// ── detectUnixOnlyCommands ────────────────────
|
|
79
|
-
|
|
80
|
-
test('detectUnixOnlyCommands: 检测管道中的 Unix 命令', () => {
|
|
81
|
-
// 复现用户踩坑场景:npm test | tail -60
|
|
82
|
-
assert.deepEqual(detectUnixOnlyCommands('npm test | tail -60'), ['tail'])
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
test('detectUnixOnlyCommands: 检测多个不同的 Unix 命令', () => {
|
|
86
|
-
assert.deepEqual(
|
|
87
|
-
detectUnixOnlyCommands('cat file.txt | grep foo | wc -l'),
|
|
88
|
-
['cat', 'grep', 'wc'],
|
|
89
|
-
)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test('detectUnixOnlyCommands: 同一命令去重', () => {
|
|
93
|
-
assert.deepEqual(
|
|
94
|
-
detectUnixOnlyCommands('grep a && grep b'),
|
|
95
|
-
['grep'],
|
|
96
|
-
)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('detectUnixOnlyCommands: 没有 Unix 命令时返回空数组', () => {
|
|
100
|
-
assert.deepEqual(detectUnixOnlyCommands('npm test'), [])
|
|
101
|
-
assert.deepEqual(detectUnixOnlyCommands('node -e "console.log(1)"'), [])
|
|
102
|
-
assert.deepEqual(detectUnixOnlyCommands('git status'), [])
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
test('detectUnixOnlyCommands: 带路径前缀的命令也能检测', () => {
|
|
106
|
-
// /usr/bin/tail → tail
|
|
107
|
-
assert.deepEqual(detectUnixOnlyCommands('/usr/bin/tail -f log'), ['tail'])
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test('detectUnixOnlyCommands: 空输入返回空数组', () => {
|
|
111
|
-
assert.deepEqual(detectUnixOnlyCommands(''), [])
|
|
112
|
-
assert.deepEqual(detectUnixOnlyCommands(null), [])
|
|
113
|
-
assert.deepEqual(detectUnixOnlyCommands(undefined), [])
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
test('detectUnixOnlyCommands: 不误判 Windows 自带命令', () => {
|
|
117
|
-
// sort/find/where 在 Windows 上存在(行为不同但不是 Unix-only)
|
|
118
|
-
assert.deepEqual(detectUnixOnlyCommands('sort file.txt'), [])
|
|
119
|
-
assert.deepEqual(detectUnixOnlyCommands('where node'), [])
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
// ── guardCommand:非 Windows 平台 ─────────────
|
|
123
|
-
|
|
124
|
-
test('guardCommand: 非 Windows 平台直接放行', async () => {
|
|
125
|
-
const r = await guardCommand('tail -f log', { platform: 'linux' })
|
|
126
|
-
assert.equal(r.blocked, false)
|
|
127
|
-
assert.equal(r.reason, null)
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
test('guardCommand: macOS 也放行', async () => {
|
|
131
|
-
const r = await guardCommand('cat file | grep foo', { platform: 'darwin' })
|
|
132
|
-
assert.equal(r.blocked, false)
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
test('guardCommand: 空/非字符串命令放行', async () => {
|
|
136
|
-
assert.equal((await guardCommand('', { platform: 'win32' })).blocked, false)
|
|
137
|
-
assert.equal((await guardCommand(null, { platform: 'win32' })).blocked, false)
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
// ── guardCommand:Windows + mock exec ──────────
|
|
141
|
-
//
|
|
142
|
-
// 注入 mock execFn 模拟 `where` 的成功/失败,避免依赖真实 PATH。
|
|
143
|
-
// 每个测试前清空缓存,确保 mock 不互相污染。
|
|
144
|
-
|
|
145
|
-
// mock exec:commands 里列出的命令名返回"找到"(无 err),其余返回"找不到"(err)
|
|
146
|
-
function createMockExec(notFoundCommands = []) {
|
|
147
|
-
const notFound = new Set(notFoundCommands)
|
|
148
|
-
return (cmd, _opts, cb) => {
|
|
149
|
-
// 提取 `where <cmd>` 中的 cmd 名
|
|
150
|
-
const m = cmd.match(/^where\s+(\S+)/)
|
|
151
|
-
const name = m?.[1] || ''
|
|
152
|
-
if (notFound.has(name)) {
|
|
153
|
-
cb(new Error('not found'), '')
|
|
154
|
-
} else {
|
|
155
|
-
cb(null, `C:\\fake\\${name}.exe`)
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
beforeEach(() => {
|
|
161
|
-
_clearAvailabilityCache()
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
test('guardCommand: Windows 上 tail 不存在 → 拦截并给出替代方案', async () => {
|
|
165
|
-
const r = await guardCommand('npm test | tail -60', {
|
|
166
|
-
platform: 'win32',
|
|
167
|
-
execFn: createMockExec(['tail']),
|
|
168
|
-
})
|
|
169
|
-
assert.equal(r.blocked, true)
|
|
170
|
-
assert.match(r.reason, /tail/)
|
|
171
|
-
assert.match(r.reason, /Select-Object/)
|
|
172
|
-
assert.match(r.reason, /npm test \| tail -60/)
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
test('guardCommand: Windows 上 tail 存在(如 Git Bash) → 放行', async () => {
|
|
176
|
-
const r = await guardCommand('npm test | tail -60', {
|
|
177
|
-
platform: 'win32',
|
|
178
|
-
execFn: createMockExec([]), // 所有命令都"找到"
|
|
179
|
-
})
|
|
180
|
-
assert.equal(r.blocked, false)
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
test('guardCommand: Windows 上无 Unix 命令 → 放行', async () => {
|
|
184
|
-
const r = await guardCommand('npm test', {
|
|
185
|
-
platform: 'win32',
|
|
186
|
-
execFn: createMockExec(),
|
|
187
|
-
})
|
|
188
|
-
assert.equal(r.blocked, false)
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
test('guardCommand: 多个 Unix 命令都不存在 → 全部列出', async () => {
|
|
192
|
-
const r = await guardCommand('cat file | grep foo | wc -l', {
|
|
193
|
-
platform: 'win32',
|
|
194
|
-
execFn: createMockExec(['cat', 'grep', 'wc']),
|
|
195
|
-
})
|
|
196
|
-
assert.equal(r.blocked, true)
|
|
197
|
-
assert.match(r.reason, /cat/)
|
|
198
|
-
assert.match(r.reason, /grep/)
|
|
199
|
-
assert.match(r.reason, /wc/)
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
test('guardCommand: 多个 Unix 命令,部分存在部分不存在 → 只拦截不存在的', async () => {
|
|
203
|
-
// grep 存在(Git Bash),cat/wc 不存在
|
|
204
|
-
const r = await guardCommand('cat file | grep foo | wc -l', {
|
|
205
|
-
platform: 'win32',
|
|
206
|
-
execFn: createMockExec(['cat', 'wc']),
|
|
207
|
-
})
|
|
208
|
-
assert.equal(r.blocked, true)
|
|
209
|
-
assert.match(r.reason, /cat/)
|
|
210
|
-
assert.doesNotMatch(r.reason, /• grep/)
|
|
211
|
-
assert.match(r.reason, /wc/)
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
test('guardCommand: 拦截原因包含原始命令', async () => {
|
|
215
|
-
const cmd = 'echo hello | tail -5'
|
|
216
|
-
const r = await guardCommand(cmd, {
|
|
217
|
-
platform: 'win32',
|
|
218
|
-
execFn: createMockExec(['tail']),
|
|
219
|
-
})
|
|
220
|
-
assert.match(r.reason, new RegExp(cmd.replace(/\|/g, '\\|')))
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
// ── checkCommandAvailability:缓存行为 ────────
|
|
224
|
-
|
|
225
|
-
test('checkCommandAvailability: 缓存结果,第二次不调 exec', async () => {
|
|
226
|
-
let callCount = 0
|
|
227
|
-
const execFn = (cmd, _opts, cb) => {
|
|
228
|
-
callCount++
|
|
229
|
-
cb(new Error('not found'), '')
|
|
230
|
-
}
|
|
231
|
-
_clearAvailabilityCache()
|
|
232
|
-
const a1 = await checkCommandAvailability('tail', { execFn })
|
|
233
|
-
const a2 = await checkCommandAvailability('tail', { execFn })
|
|
234
|
-
assert.equal(a1, false)
|
|
235
|
-
assert.equal(a2, false)
|
|
236
|
-
assert.equal(callCount, 1, '第二次应命中缓存,不调 exec')
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
// ── 数据完整性 ────────────────────────────────
|
|
240
|
-
|
|
241
|
-
test('UNIX_ONLY_COMMANDS: 每个条目都有非空的替代方案说明', () => {
|
|
242
|
-
for (const [name, hint] of Object.entries(UNIX_ONLY_COMMANDS)) {
|
|
243
|
-
assert.ok(typeof name === 'string' && name.length > 0)
|
|
244
|
-
assert.ok(typeof hint === 'string' && hint.length > 10, `${name} 的替代方案说明过短`)
|
|
245
|
-
}
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
test('UNIX_ONLY_COMMANDS: 不包含 Windows 自带命令(防误杀)', () => {
|
|
249
|
-
const windowsBuiltins = ['sort', 'find', 'where', 'dir', 'type', 'copy', 'move', 'del']
|
|
250
|
-
for (const w of windowsBuiltins) {
|
|
251
|
-
assert.ok(!UNIX_ONLY_COMMANDS[w], `${w} 是 Windows 自带命令,不应在 Unix-only 清单中`)
|
|
252
|
-
}
|
|
253
|
-
})
|
|
1
|
+
// Copyright 2026 xz333221
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
// src/cli/ai/platformGuard.js 单元测试
|
|
16
|
+
// 覆盖纯函数(splitCommandSegments / extractCommandName / detectUnixOnlyCommands)
|
|
17
|
+
// 和异步主接口(guardCommand,注入 mock exec 避免 PATH 依赖)。
|
|
18
|
+
import { test, beforeEach } from 'node:test'
|
|
19
|
+
import assert from 'node:assert/strict'
|
|
20
|
+
import {
|
|
21
|
+
splitCommandSegments,
|
|
22
|
+
extractCommandName,
|
|
23
|
+
detectUnixOnlyCommands,
|
|
24
|
+
guardCommand,
|
|
25
|
+
checkCommandAvailability,
|
|
26
|
+
_clearAvailabilityCache,
|
|
27
|
+
UNIX_ONLY_COMMANDS,
|
|
28
|
+
} from './platformGuard.js'
|
|
29
|
+
|
|
30
|
+
// ── splitCommandSegments ──────────────────────
|
|
31
|
+
|
|
32
|
+
test('splitCommandSegments: 按管道切分', () => {
|
|
33
|
+
assert.deepEqual(
|
|
34
|
+
splitCommandSegments('npm test | tail -60'),
|
|
35
|
+
['npm test', 'tail -60'],
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('splitCommandSegments: 按 && 切分', () => {
|
|
40
|
+
assert.deepEqual(
|
|
41
|
+
splitCommandSegments('echo a && echo b'),
|
|
42
|
+
['echo a', 'echo b'],
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('splitCommandSegments: 按 || 和 ; 切分', () => {
|
|
47
|
+
assert.deepEqual(
|
|
48
|
+
splitCommandSegments('echo a || echo b ; echo c'),
|
|
49
|
+
['echo a', 'echo b', 'echo c'],
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('splitCommandSegments: 空串/纯空白返回空数组', () => {
|
|
54
|
+
assert.deepEqual(splitCommandSegments(''), [])
|
|
55
|
+
assert.deepEqual(splitCommandSegments(' '), [])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('splitCommandSegments: 无操作符的命令返回单段', () => {
|
|
59
|
+
assert.deepEqual(splitCommandSegments('echo hello'), ['echo hello'])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// ── extractCommandName ────────────────────────
|
|
63
|
+
|
|
64
|
+
test('extractCommandName: 提取第一个 token 并小写', () => {
|
|
65
|
+
assert.equal(extractCommandName('TAIL -60'), 'tail')
|
|
66
|
+
assert.equal(extractCommandName('node -e "code"'), 'node')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('extractCommandName: 跳过 sudo/doas 前缀', () => {
|
|
70
|
+
assert.equal(extractCommandName('sudo tail -f log'), 'tail')
|
|
71
|
+
assert.equal(extractCommandName('doas grep pattern'), 'grep')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('extractCommandName: 空串返回空', () => {
|
|
75
|
+
assert.equal(extractCommandName(''), '')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// ── detectUnixOnlyCommands ────────────────────
|
|
79
|
+
|
|
80
|
+
test('detectUnixOnlyCommands: 检测管道中的 Unix 命令', () => {
|
|
81
|
+
// 复现用户踩坑场景:npm test | tail -60
|
|
82
|
+
assert.deepEqual(detectUnixOnlyCommands('npm test | tail -60'), ['tail'])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('detectUnixOnlyCommands: 检测多个不同的 Unix 命令', () => {
|
|
86
|
+
assert.deepEqual(
|
|
87
|
+
detectUnixOnlyCommands('cat file.txt | grep foo | wc -l'),
|
|
88
|
+
['cat', 'grep', 'wc'],
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('detectUnixOnlyCommands: 同一命令去重', () => {
|
|
93
|
+
assert.deepEqual(
|
|
94
|
+
detectUnixOnlyCommands('grep a && grep b'),
|
|
95
|
+
['grep'],
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('detectUnixOnlyCommands: 没有 Unix 命令时返回空数组', () => {
|
|
100
|
+
assert.deepEqual(detectUnixOnlyCommands('npm test'), [])
|
|
101
|
+
assert.deepEqual(detectUnixOnlyCommands('node -e "console.log(1)"'), [])
|
|
102
|
+
assert.deepEqual(detectUnixOnlyCommands('git status'), [])
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('detectUnixOnlyCommands: 带路径前缀的命令也能检测', () => {
|
|
106
|
+
// /usr/bin/tail → tail
|
|
107
|
+
assert.deepEqual(detectUnixOnlyCommands('/usr/bin/tail -f log'), ['tail'])
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
test('detectUnixOnlyCommands: 空输入返回空数组', () => {
|
|
111
|
+
assert.deepEqual(detectUnixOnlyCommands(''), [])
|
|
112
|
+
assert.deepEqual(detectUnixOnlyCommands(null), [])
|
|
113
|
+
assert.deepEqual(detectUnixOnlyCommands(undefined), [])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('detectUnixOnlyCommands: 不误判 Windows 自带命令', () => {
|
|
117
|
+
// sort/find/where 在 Windows 上存在(行为不同但不是 Unix-only)
|
|
118
|
+
assert.deepEqual(detectUnixOnlyCommands('sort file.txt'), [])
|
|
119
|
+
assert.deepEqual(detectUnixOnlyCommands('where node'), [])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// ── guardCommand:非 Windows 平台 ─────────────
|
|
123
|
+
|
|
124
|
+
test('guardCommand: 非 Windows 平台直接放行', async () => {
|
|
125
|
+
const r = await guardCommand('tail -f log', { platform: 'linux' })
|
|
126
|
+
assert.equal(r.blocked, false)
|
|
127
|
+
assert.equal(r.reason, null)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('guardCommand: macOS 也放行', async () => {
|
|
131
|
+
const r = await guardCommand('cat file | grep foo', { platform: 'darwin' })
|
|
132
|
+
assert.equal(r.blocked, false)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test('guardCommand: 空/非字符串命令放行', async () => {
|
|
136
|
+
assert.equal((await guardCommand('', { platform: 'win32' })).blocked, false)
|
|
137
|
+
assert.equal((await guardCommand(null, { platform: 'win32' })).blocked, false)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// ── guardCommand:Windows + mock exec ──────────
|
|
141
|
+
//
|
|
142
|
+
// 注入 mock execFn 模拟 `where` 的成功/失败,避免依赖真实 PATH。
|
|
143
|
+
// 每个测试前清空缓存,确保 mock 不互相污染。
|
|
144
|
+
|
|
145
|
+
// mock exec:commands 里列出的命令名返回"找到"(无 err),其余返回"找不到"(err)
|
|
146
|
+
function createMockExec(notFoundCommands = []) {
|
|
147
|
+
const notFound = new Set(notFoundCommands)
|
|
148
|
+
return (cmd, _opts, cb) => {
|
|
149
|
+
// 提取 `where <cmd>` 中的 cmd 名
|
|
150
|
+
const m = cmd.match(/^where\s+(\S+)/)
|
|
151
|
+
const name = m?.[1] || ''
|
|
152
|
+
if (notFound.has(name)) {
|
|
153
|
+
cb(new Error('not found'), '')
|
|
154
|
+
} else {
|
|
155
|
+
cb(null, `C:\\fake\\${name}.exe`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
beforeEach(() => {
|
|
161
|
+
_clearAvailabilityCache()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('guardCommand: Windows 上 tail 不存在 → 拦截并给出替代方案', async () => {
|
|
165
|
+
const r = await guardCommand('npm test | tail -60', {
|
|
166
|
+
platform: 'win32',
|
|
167
|
+
execFn: createMockExec(['tail']),
|
|
168
|
+
})
|
|
169
|
+
assert.equal(r.blocked, true)
|
|
170
|
+
assert.match(r.reason, /tail/)
|
|
171
|
+
assert.match(r.reason, /Select-Object/)
|
|
172
|
+
assert.match(r.reason, /npm test \| tail -60/)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('guardCommand: Windows 上 tail 存在(如 Git Bash) → 放行', async () => {
|
|
176
|
+
const r = await guardCommand('npm test | tail -60', {
|
|
177
|
+
platform: 'win32',
|
|
178
|
+
execFn: createMockExec([]), // 所有命令都"找到"
|
|
179
|
+
})
|
|
180
|
+
assert.equal(r.blocked, false)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('guardCommand: Windows 上无 Unix 命令 → 放行', async () => {
|
|
184
|
+
const r = await guardCommand('npm test', {
|
|
185
|
+
platform: 'win32',
|
|
186
|
+
execFn: createMockExec(),
|
|
187
|
+
})
|
|
188
|
+
assert.equal(r.blocked, false)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
test('guardCommand: 多个 Unix 命令都不存在 → 全部列出', async () => {
|
|
192
|
+
const r = await guardCommand('cat file | grep foo | wc -l', {
|
|
193
|
+
platform: 'win32',
|
|
194
|
+
execFn: createMockExec(['cat', 'grep', 'wc']),
|
|
195
|
+
})
|
|
196
|
+
assert.equal(r.blocked, true)
|
|
197
|
+
assert.match(r.reason, /cat/)
|
|
198
|
+
assert.match(r.reason, /grep/)
|
|
199
|
+
assert.match(r.reason, /wc/)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('guardCommand: 多个 Unix 命令,部分存在部分不存在 → 只拦截不存在的', async () => {
|
|
203
|
+
// grep 存在(Git Bash),cat/wc 不存在
|
|
204
|
+
const r = await guardCommand('cat file | grep foo | wc -l', {
|
|
205
|
+
platform: 'win32',
|
|
206
|
+
execFn: createMockExec(['cat', 'wc']),
|
|
207
|
+
})
|
|
208
|
+
assert.equal(r.blocked, true)
|
|
209
|
+
assert.match(r.reason, /cat/)
|
|
210
|
+
assert.doesNotMatch(r.reason, /• grep/)
|
|
211
|
+
assert.match(r.reason, /wc/)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('guardCommand: 拦截原因包含原始命令', async () => {
|
|
215
|
+
const cmd = 'echo hello | tail -5'
|
|
216
|
+
const r = await guardCommand(cmd, {
|
|
217
|
+
platform: 'win32',
|
|
218
|
+
execFn: createMockExec(['tail']),
|
|
219
|
+
})
|
|
220
|
+
assert.match(r.reason, new RegExp(cmd.replace(/\|/g, '\\|')))
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// ── checkCommandAvailability:缓存行为 ────────
|
|
224
|
+
|
|
225
|
+
test('checkCommandAvailability: 缓存结果,第二次不调 exec', async () => {
|
|
226
|
+
let callCount = 0
|
|
227
|
+
const execFn = (cmd, _opts, cb) => {
|
|
228
|
+
callCount++
|
|
229
|
+
cb(new Error('not found'), '')
|
|
230
|
+
}
|
|
231
|
+
_clearAvailabilityCache()
|
|
232
|
+
const a1 = await checkCommandAvailability('tail', { execFn })
|
|
233
|
+
const a2 = await checkCommandAvailability('tail', { execFn })
|
|
234
|
+
assert.equal(a1, false)
|
|
235
|
+
assert.equal(a2, false)
|
|
236
|
+
assert.equal(callCount, 1, '第二次应命中缓存,不调 exec')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// ── 数据完整性 ────────────────────────────────
|
|
240
|
+
|
|
241
|
+
test('UNIX_ONLY_COMMANDS: 每个条目都有非空的替代方案说明', () => {
|
|
242
|
+
for (const [name, hint] of Object.entries(UNIX_ONLY_COMMANDS)) {
|
|
243
|
+
assert.ok(typeof name === 'string' && name.length > 0)
|
|
244
|
+
assert.ok(typeof hint === 'string' && hint.length > 10, `${name} 的替代方案说明过短`)
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('UNIX_ONLY_COMMANDS: 不包含 Windows 自带命令(防误杀)', () => {
|
|
249
|
+
const windowsBuiltins = ['sort', 'find', 'where', 'dir', 'type', 'copy', 'move', 'del']
|
|
250
|
+
for (const w of windowsBuiltins) {
|
|
251
|
+
assert.ok(!UNIX_ONLY_COMMANDS[w], `${w} 是 Windows 自带命令,不应在 Unix-only 清单中`)
|
|
252
|
+
}
|
|
253
|
+
})
|