zen-gitsync 2.17.42 → 2.17.43
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 +7 -1
- package/src/ui/public/assets/AgentEngineSelector-BwFdXZnh.js +2 -0
- package/src/ui/public/assets/AgentView-CMQ7TzwN.js +11 -0
- package/src/ui/public/assets/AppVersionBadge-Bk4XGXki.js +2 -0
- package/src/ui/public/assets/BranchSelector-DL37Ho4j.js +1 -0
- package/src/ui/public/assets/CommitForm-BGpFU-fp.css +1 -0
- package/src/ui/public/assets/CommitForm-DdvYlUtA.js +14 -0
- package/src/ui/public/assets/{CommonDialog-q8bffY57.js → CommonDialog-BT6e135Y.js} +1 -1
- package/src/ui/public/assets/EditorView-CeZ-EvP1.js +37 -0
- package/src/ui/public/assets/{EditorView-QROmuz4O.css → EditorView-X6M5DS2w.css} +1 -1
- package/src/ui/public/assets/{FlowExecutionViewer-DMxdFp7T.js → FlowExecutionViewer-DIlgernH.js} +1 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-BIFFckMG.js +21 -0
- package/src/ui/public/assets/LogList-CYU0YVnC.js +7 -0
- package/src/ui/public/assets/MindmapView-B2Oz9nCS.css +1 -0
- package/src/ui/public/assets/MindmapView-DYB3_94z.js +2 -0
- package/src/ui/public/assets/MonitorView-CTtuMls4.css +1 -0
- package/src/ui/public/assets/MonitorView-DKXSFbyq.js +3 -0
- package/src/ui/public/assets/ProjectStartupButton-BMICmCA_.js +1 -0
- package/src/ui/public/assets/RecentDirectoriesChat-LOD3UBFM.js +3 -0
- package/src/ui/public/assets/RemoteManagerDialog-S3xLgN02.js +4 -0
- package/src/ui/public/assets/{RemoteRepoCard-muY6G96M.js → RemoteRepoCard-BLb8tACY.js} +2 -2
- package/src/ui/public/assets/{SourceMapView-CLrtq2ey.js → SourceMapView-BA7E3JX7.js} +3 -3
- package/src/ui/public/assets/{SvgIcon-CVenF0Ye.js → SvgIcon-Dd0dgVcd.js} +1 -1
- package/src/ui/public/assets/UserInputNode-B0YtQfhh.js +1 -0
- package/src/ui/public/assets/WorkbenchView-lgVbWRyY.js +287 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-BUtBXJ4L.js → _plugin-vue_export-helper-DibQ4qNS.js} +3 -3
- package/src/ui/public/assets/{agentConversations-COO5r7ra.js → agentConversations-CyGrxt-h.js} +4 -4
- package/src/ui/public/assets/{configStore-B8_uxBp2.js → configStore-DQ92GVaR.js} +1 -1
- package/src/ui/public/assets/{css.worker-BDCmgL4U.js → css.worker-DFdt6_zu.js} +9 -9
- package/src/ui/public/assets/{dagre-BWDqZwmv.js → dagre-BPEW99Ym.js} +4 -4
- package/src/ui/public/assets/editor.worker-O-CH8JUO.js +26 -0
- package/src/ui/public/assets/element-plus-C8IhrglD.js +48 -0
- package/src/ui/public/assets/element-plus-DJ3ZQa98.css +1 -0
- package/src/ui/public/assets/{flow-mindmap-CY72TleH.js → flow-mindmap-BQUUIfaR.js} +1 -1
- package/src/ui/public/assets/{html.worker-DG8hnSJR.js → html.worker-JUzsMP-j.js} +8 -8
- package/src/ui/public/assets/index-DavZzOom.css +1 -0
- package/src/ui/public/assets/index-ED00NaPI.js +198 -0
- package/src/ui/public/assets/json.worker-B4dEhy0e.js +58 -0
- package/src/ui/public/assets/{monaco-CpT1sgBA.js → monaco-kXz8a-yc.js} +6 -6
- package/src/ui/public/assets/{office-docx-COuLzG9K.js → office-docx-4ZL-nECj.js} +1 -1
- package/src/ui/public/assets/{office-excel-BTtyMAdK.js → office-excel-BlPftvU4.js} +1 -1
- package/src/ui/public/assets/{office-pptx-BB2cWQB-.js → office-pptx-CVP1caLm.js} +3 -3
- package/src/ui/public/assets/{ts.worker-DgFMIcEs.js → ts.worker-CmEKzkmF.js} +49 -49
- package/src/ui/public/assets/{vendor-JPxCsYZA.js → vendor-CazUJJkv.js} +8 -8
- package/src/ui/public/assets/{vue-flow-DinCMZbP.js → vue-flow-BABZEIJA.js} +1 -1
- package/src/ui/public/index.html +14 -26
- package/src/ui/server/routes/instances.js +42 -13
- package/src/ui/server/routes/instances.test.js +55 -32
- package/src/ui/server/routes/workbench/executorModels.test.js +326 -326
- package/src/ui/public/assets/AgentEngineSelector-CLJ4rGGb.js +0 -2
- package/src/ui/public/assets/AgentView-C7tbE1pr.js +0 -11
- package/src/ui/public/assets/AppVersionBadge-CjrqnPHZ.js +0 -2
- package/src/ui/public/assets/BranchSelector-B1RkRjkT.js +0 -1
- package/src/ui/public/assets/CommitForm-Dlr6QTba.js +0 -6
- package/src/ui/public/assets/CommitForm-DoR9UNBD.css +0 -1
- package/src/ui/public/assets/EditorView-WQIQHdqS.js +0 -37
- package/src/ui/public/assets/FileDiffViewer-CFJFN_xQ.js +0 -15
- package/src/ui/public/assets/FileDiffViewer-D1aR4rba.css +0 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-vBWYTGeM.js +0 -21
- package/src/ui/public/assets/IconButton-Ca_axHSP.css +0 -1
- package/src/ui/public/assets/IconButton-Chpl6IGZ.js +0 -1
- package/src/ui/public/assets/LogList-CinD-VGc.js +0 -7
- package/src/ui/public/assets/MindmapView-BXCi5cvH.css +0 -1
- package/src/ui/public/assets/MindmapView-CAod7lPv.js +0 -2
- package/src/ui/public/assets/MonitorView-B2YVWNrs.css +0 -1
- package/src/ui/public/assets/MonitorView-B5sOfRgx.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-ZzSQN2oO.js +0 -1
- package/src/ui/public/assets/RecentDirectoriesChat-DiiaL2lT.js +0 -3
- package/src/ui/public/assets/RemoteManagerDialog-D8pMeIHt.js +0 -4
- package/src/ui/public/assets/TemplateManager-Bnek7hoV.css +0 -1
- package/src/ui/public/assets/TemplateManager-DTQ0Oir3.js +0 -1
- package/src/ui/public/assets/UserInputNode-CGKrY_Y8.js +0 -1
- package/src/ui/public/assets/WorkbenchView-BnHSL8Lr.js +0 -287
- package/src/ui/public/assets/editor.worker-GCPm6rMv.js +0 -26
- package/src/ui/public/assets/element-plus-0wIX-Gk-.js +0 -48
- package/src/ui/public/assets/element-plus-C1xzgNSR.css +0 -1
- package/src/ui/public/assets/fileIcon-B5p5pGN9.js +0 -1
- package/src/ui/public/assets/gitStore-bZysdieB.js +0 -6
- package/src/ui/public/assets/index-BetEwMCp.js +0 -162
- package/src/ui/public/assets/index-JVjC2bGh.css +0 -1
- package/src/ui/public/assets/json.worker-CxwlJRnL.js +0 -58
- package/src/ui/public/assets/previewSandbox-BcYj-0Up.css +0 -1
- package/src/ui/public/assets/previewSandbox-CgcL-OLc.js +0 -9
- package/src/ui/public/assets/toolsStore-CofRox1m.js +0 -1
- package/src/ui/public/assets/useThemeObserver-D0lx06N9.js +0 -1
|
@@ -1,326 +1,326 @@
|
|
|
1
|
-
// 执行器模型探测的单测。
|
|
2
|
-
//
|
|
3
|
-
// 为什么这些用例值得写:这个模块的全部价值是"读三种格式的配置文件",
|
|
4
|
-
// 而三种格式的解析都是**手写的**(不引 TOML / JSONC 依赖,理由见模块注释)——
|
|
5
|
-
// 手写解析最容易在两个地方悄悄错:注释/空行的截断、以及多文件兜底被丢掉。
|
|
6
|
-
// 这两处都已经真出过 bug(见下面标 [回归] 的用例),所以它们必须钉住。
|
|
7
|
-
//
|
|
8
|
-
// 所有用例都在**临时沙箱 home** 里跑,绝不读真实 ~/ —— 否则用例会随开发机
|
|
9
|
-
// 装没装 claude、配没配模型而时红时绿。
|
|
10
|
-
import { test } from 'node:test'
|
|
11
|
-
import assert from 'node:assert/strict'
|
|
12
|
-
import fs from 'fs/promises'
|
|
13
|
-
import os from 'os'
|
|
14
|
-
import path from 'path'
|
|
15
|
-
import {
|
|
16
|
-
readTomlTopLevel,
|
|
17
|
-
stripJsonComments,
|
|
18
|
-
detectExecutorModels,
|
|
19
|
-
formatExecutorModel,
|
|
20
|
-
} from './executorModels.js'
|
|
21
|
-
|
|
22
|
-
const sandboxes = []
|
|
23
|
-
|
|
24
|
-
/** 造一个只属于本用例的 home 目录,按 {相对路径: 内容} 落文件 */
|
|
25
|
-
async function makeHome(files = {}) {
|
|
26
|
-
const home = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-executor-models-'))
|
|
27
|
-
sandboxes.push(home)
|
|
28
|
-
for (const [rel, content] of Object.entries(files)) {
|
|
29
|
-
const full = path.join(home, rel)
|
|
30
|
-
await fs.mkdir(path.dirname(full), { recursive: true })
|
|
31
|
-
await fs.writeFile(full, content, 'utf8')
|
|
32
|
-
}
|
|
33
|
-
return home
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
test.after(async () => {
|
|
37
|
-
for (const home of sandboxes) await fs.rm(home, { recursive: true, force: true })
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
// ── readTomlTopLevel ─────────────────────────────────────────────────────────
|
|
41
|
-
|
|
42
|
-
test('readTomlTopLevel: 注释行与空行不截断顶层读取 [回归]', () => {
|
|
43
|
-
// 老实现把「空行 / 注释行 / 段头」三类一起 break,于是文件第一行是注释时
|
|
44
|
-
// 顶层 model 整个读空 —— 表现为界面上 codex 永远显示"未配置"。
|
|
45
|
-
const toml = [
|
|
46
|
-
'# codex 配置说明',
|
|
47
|
-
'# 第二行注释',
|
|
48
|
-
'model = "gpt-6-astra"',
|
|
49
|
-
'',
|
|
50
|
-
'model_provider = "custom"',
|
|
51
|
-
'',
|
|
52
|
-
'[model_providers.custom]',
|
|
53
|
-
'name = "kakouai"',
|
|
54
|
-
].join('\n')
|
|
55
|
-
assert.deepEqual(readTomlTopLevel(toml, ['model', 'model_provider']), {
|
|
56
|
-
model: 'gpt-6-astra',
|
|
57
|
-
model_provider: 'custom',
|
|
58
|
-
})
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
test('readTomlTopLevel: 段内的同名键不会被当成顶层值', () => {
|
|
62
|
-
const toml = 'model = "top"\n\n[projects."c:/x"]\nmodel = "inner"\n'
|
|
63
|
-
assert.equal(readTomlTopLevel(toml, ['model']).model, 'top')
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
test('readTomlTopLevel: 只取白名单里的键(其余含密钥的键一律不取)', () => {
|
|
67
|
-
const toml = 'model = "m"\nexperimental_bearer_token = "tok"\napi_key = "k"\n'
|
|
68
|
-
assert.deepEqual(readTomlTopLevel(toml, ['model']), { model: 'm' })
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('readTomlTopLevel: 行尾注释不污染值;单引号值也认', () => {
|
|
72
|
-
const toml = "model = 'm-1' # 备注\nmodel_provider = \"custom\"#紧贴的注释\n"
|
|
73
|
-
assert.deepEqual(readTomlTopLevel(toml, ['model', 'model_provider']), {
|
|
74
|
-
model: 'm-1',
|
|
75
|
-
model_provider: 'custom',
|
|
76
|
-
})
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
test('readTomlTopLevel: 段头里带方括号的路径不被误判成值', () => {
|
|
80
|
-
const toml = 'model = "m"\n\n[projects.\'c:\\weird[path]\']\nmodel = "inner"\n'
|
|
81
|
-
assert.equal(readTomlTopLevel(toml, ['model']).model, 'm')
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
// ── stripJsonComments ────────────────────────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
test('stripJsonComments: 字符串里的 // 不被当注释(base_url 场景)', () => {
|
|
87
|
-
const jsonc = '{ "url": "https://api.example.com/v1", // 注释\n "n": 1 }'
|
|
88
|
-
assert.equal(JSON.parse(stripJsonComments(jsonc)).url, 'https://api.example.com/v1')
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
test('stripJsonComments: 剥块注释与尾逗号后仍能 JSON.parse', () => {
|
|
92
|
-
const jsonc = '{\n /* 块\n 注释 */\n "model": "a/b",\n}'
|
|
93
|
-
assert.deepEqual(JSON.parse(stripJsonComments(jsonc)), { model: 'a/b' })
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
test('stripJsonComments: 转义引号不影响"在不在字符串里"的判断', () => {
|
|
97
|
-
const jsonc = '{ "s": "a\\"//b", "n": 1 } // 尾注释'
|
|
98
|
-
assert.deepEqual(JSON.parse(stripJsonComments(jsonc)), { s: 'a"//b', n: 1 })
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
// ── detectExecutorModels(沙箱 home)─────────────────────────────────────────
|
|
102
|
-
|
|
103
|
-
const CLAUDE_JSON = JSON.stringify({
|
|
104
|
-
env: {
|
|
105
|
-
ANTHROPIC_BASE_URL: 'http://127.0.0.1:15721',
|
|
106
|
-
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-5[1M]',
|
|
107
|
-
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME: 'deepseek-v4.1-flash',
|
|
108
|
-
ANTHROPIC_API_KEY: 'sk-should-never-leak',
|
|
109
|
-
},
|
|
110
|
-
})
|
|
111
|
-
const CODEX_TOML = [
|
|
112
|
-
'# 顶层注释',
|
|
113
|
-
'model = "gpt-6-astra"',
|
|
114
|
-
'model_provider = "custom"',
|
|
115
|
-
'',
|
|
116
|
-
'[model_providers.custom]',
|
|
117
|
-
'name = "kakouai"',
|
|
118
|
-
'experimental_bearer_token = "tok-should-never-leak"',
|
|
119
|
-
'',
|
|
120
|
-
'[projects."c:/x"]',
|
|
121
|
-
'model = "inner-model"',
|
|
122
|
-
].join('\n')
|
|
123
|
-
const OPENCODE_JSONC = '{\n // 注释\n "model": "anthropic/claude-x",\n}'
|
|
124
|
-
|
|
125
|
-
test('detectExecutorModels: 三家配置齐全时各自读出模型', async () => {
|
|
126
|
-
const home = await makeHome({
|
|
127
|
-
'.claude/settings.json': CLAUDE_JSON,
|
|
128
|
-
'.codex/config.toml': CODEX_TOML,
|
|
129
|
-
'.config/opencode/opencode.jsonc': OPENCODE_JSONC,
|
|
130
|
-
})
|
|
131
|
-
const out = await detectExecutorModels({ homeDir: home })
|
|
132
|
-
|
|
133
|
-
assert.deepEqual(out.claude, {
|
|
134
|
-
model: 'claude-sonnet-5[1M]',
|
|
135
|
-
display: 'deepseek-v4.1-flash',
|
|
136
|
-
provider: 'http://127.0.0.1:15721',
|
|
137
|
-
})
|
|
138
|
-
// 顶层注释不再截断 → model_provider 读得到 → 服务商名也能查出来
|
|
139
|
-
assert.deepEqual(out.codex, { model: 'gpt-6-astra', display: null, provider: 'kakouai' })
|
|
140
|
-
// 只有 .jsonc(没有 .json)时也要读到:早先那个 promise 被解构丢掉,这里是死代码
|
|
141
|
-
assert.deepEqual(out.opencode, {
|
|
142
|
-
model: 'anthropic/claude-x', display: null, provider: null, source: 'config',
|
|
143
|
-
})
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
test('detectExecutorModels: 只有 .jsonc 时 opencode 仍读得到 [回归]', async () => {
|
|
147
|
-
const home = await makeHome({ '.config/opencode/opencode.jsonc': OPENCODE_JSONC })
|
|
148
|
-
const out = await detectExecutorModels({ homeDir: home })
|
|
149
|
-
assert.equal(out.opencode?.model, 'anthropic/claude-x')
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
test('detectExecutorModels: .json 与 .jsonc 同时存在时 .json 优先', async () => {
|
|
153
|
-
const home = await makeHome({
|
|
154
|
-
'.config/opencode/opencode.json': '{"model":"from-json"}',
|
|
155
|
-
'.config/opencode/opencode.jsonc': '{"model":"from-jsonc"}',
|
|
156
|
-
})
|
|
157
|
-
const out = await detectExecutorModels({ homeDir: home })
|
|
158
|
-
assert.equal(out.opencode?.model, 'from-json')
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
test('detectExecutorModels: opencode 的 model 是对象时带上 variant', async () => {
|
|
162
|
-
const home = await makeHome({
|
|
163
|
-
'.config/opencode/opencode.json': '{"model":{"model":"openai/gpt-y","variant":"high"}}',
|
|
164
|
-
})
|
|
165
|
-
const out = await detectExecutorModels({ homeDir: home })
|
|
166
|
-
assert.equal(out.opencode?.model, 'openai/gpt-y (high)')
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
// ── opencode 的第二处来源:TUI 状态 ──────────────────────────────────────────
|
|
170
|
-
//
|
|
171
|
-
// opencode 的 TUI 允许直接选模型,而它把选择**存在自己的 state 里、不回写配置文件**。
|
|
172
|
-
// 只读配置文件的话,对一个明明在跑的模型显示"未在配置中指定" —— 用户会去翻配置,
|
|
173
|
-
// 配置里确实没有,白折腾一场。这组用例钉住"配置文件优先、没有则退回 state"。
|
|
174
|
-
|
|
175
|
-
const OPENCODE_STATE = JSON.stringify({
|
|
176
|
-
recent: [
|
|
177
|
-
{ providerID: 'opencode-go', modelID: 'space-bunny-free' },
|
|
178
|
-
{ providerID: 'opencode-go', modelID: 'deepseek-v4.1-flash' },
|
|
179
|
-
],
|
|
180
|
-
favorite: [],
|
|
181
|
-
variant: { 'opencode-go/space-bunny-free': 'max', 'opencode-go/deepseek-v4.1-flash': 'max' },
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
test('detectExecutorModels: 配置里没写 model 时,退回 TUI 状态里的「最近使用」', async () => {
|
|
185
|
-
const home = await makeHome({
|
|
186
|
-
// 本机真实情形:配置文件里只有一个 $schema
|
|
187
|
-
'.config/opencode/opencode.jsonc': '{\n "$schema": "https://opencode.ai/config.json"\n}',
|
|
188
|
-
'.local/state/opencode/model.json': OPENCODE_STATE,
|
|
189
|
-
})
|
|
190
|
-
const out = await detectExecutorModels({ homeDir: home })
|
|
191
|
-
assert.deepEqual(out.opencode, {
|
|
192
|
-
model: 'opencode-go/space-bunny-free (max)', // recent[0] + variant 表里的 max
|
|
193
|
-
display: null,
|
|
194
|
-
provider: null, // 模型名本身已是 provider/model,不再重复报一次
|
|
195
|
-
source: 'state',
|
|
196
|
-
})
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
test('detectExecutorModels: 配置文件里写了 model 就以它为准(state 只是兜底)', async () => {
|
|
200
|
-
const home = await makeHome({
|
|
201
|
-
'.config/opencode/opencode.json': '{"model":"anthropic/claude-x"}',
|
|
202
|
-
'.local/state/opencode/model.json': OPENCODE_STATE,
|
|
203
|
-
})
|
|
204
|
-
const out = await detectExecutorModels({ homeDir: home })
|
|
205
|
-
assert.deepEqual(out.opencode, {
|
|
206
|
-
model: 'anthropic/claude-x', display: null, provider: null, source: 'config',
|
|
207
|
-
})
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
test('detectExecutorModels: recent 项自带 variant 时优先于顶层 variant 表', async () => {
|
|
211
|
-
const home = await makeHome({
|
|
212
|
-
'.config/opencode/opencode.jsonc': '',
|
|
213
|
-
'.local/state/opencode/model.json': JSON.stringify({
|
|
214
|
-
recent: [{ providerID: 'p', modelID: 'm', variant: 'own' }],
|
|
215
|
-
variant: { 'p/m': 'mapped' },
|
|
216
|
-
}),
|
|
217
|
-
})
|
|
218
|
-
const out = await detectExecutorModels({ homeDir: home })
|
|
219
|
-
assert.equal(out.opencode?.model, 'p/m (own)')
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
test('detectExecutorModels: state 里没有 recent / 缺字段时仍是 null(不猜)', async () => {
|
|
223
|
-
const empty = await makeHome({ '.local/state/opencode/model.json': '{"recent":[],"favorite":[]}' })
|
|
224
|
-
assert.equal((await detectExecutorModels({ homeDir: empty })).opencode, null)
|
|
225
|
-
|
|
226
|
-
const partial = await makeHome({
|
|
227
|
-
'.local/state/opencode/model.json': '{"recent":[{"providerID":"p"}]}',
|
|
228
|
-
})
|
|
229
|
-
assert.equal((await detectExecutorModels({ homeDir: partial })).opencode, null)
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
test('detectExecutorModels: 两处都没有 model 才是真的"未在配置中指定"', async () => {
|
|
233
|
-
const home = await makeHome({ '.config/opencode/opencode.json': '{"model":""}' })
|
|
234
|
-
assert.equal((await detectExecutorModels({ homeDir: home })).opencode, null)
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
test('detectExecutorModels: claude 显式 ANTHROPIC_MODEL 优先于档位别名', async () => {
|
|
238
|
-
const home = await makeHome({
|
|
239
|
-
'.claude/settings.json': JSON.stringify({
|
|
240
|
-
env: {
|
|
241
|
-
ANTHROPIC_MODEL: 'my-explicit-model',
|
|
242
|
-
ANTHROPIC_BASE_URL: 'http://proxy.local',
|
|
243
|
-
ANTHROPIC_DEFAULT_SONNET_MODEL: 'sonnet-alias',
|
|
244
|
-
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME: 'sonnet-real',
|
|
245
|
-
},
|
|
246
|
-
}),
|
|
247
|
-
})
|
|
248
|
-
const out = await detectExecutorModels({ homeDir: home })
|
|
249
|
-
// 显式指定时没有"别名背后"的第二层,但 base_url 仍要带出来
|
|
250
|
-
assert.deepEqual(out.claude, {
|
|
251
|
-
model: 'my-explicit-model',
|
|
252
|
-
display: null,
|
|
253
|
-
provider: 'http://proxy.local',
|
|
254
|
-
})
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
test('detectExecutorModels: 一个配置文件都没有时三项全 null 且不抛', async () => {
|
|
258
|
-
const home = await makeHome()
|
|
259
|
-
const out = await detectExecutorModels({ homeDir: home })
|
|
260
|
-
assert.deepEqual(out, { claude: null, codex: null, opencode: null })
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
test('detectExecutorModels: 配置残缺/是目录/内容不是 JSON 时只让自己变 null', async () => {
|
|
264
|
-
const home = await makeHome({
|
|
265
|
-
'.claude/settings.json': '{ 这不是 JSON', // 解析失败 → null
|
|
266
|
-
'.codex/config.toml': '# 只有注释,没有 model\n', // 无 model → null
|
|
267
|
-
'.config/opencode/opencode.json': '{"model":""}', // 空串 → null
|
|
268
|
-
})
|
|
269
|
-
const out = await detectExecutorModels({ homeDir: home })
|
|
270
|
-
assert.deepEqual(out, { claude: null, codex: null, opencode: null })
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
test('detectExecutorModels: 返回值里绝不出现任何密钥', async () => {
|
|
274
|
-
const home = await makeHome({
|
|
275
|
-
'.claude/settings.json': CLAUDE_JSON,
|
|
276
|
-
'.codex/config.toml': CODEX_TOML,
|
|
277
|
-
})
|
|
278
|
-
const serialized = JSON.stringify(await detectExecutorModels({ homeDir: home }))
|
|
279
|
-
assert.equal(serialized.includes('sk-should-never-leak'), false)
|
|
280
|
-
assert.equal(serialized.includes('tok-should-never-leak'), false)
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
// ── formatExecutorModel ──────────────────────────────────────────────────────
|
|
284
|
-
|
|
285
|
-
test('formatExecutorModel: display 优先,别名落到 detail', () => {
|
|
286
|
-
assert.deepEqual(
|
|
287
|
-
formatExecutorModel({
|
|
288
|
-
model: 'claude-sonnet-5[1M]',
|
|
289
|
-
display: 'deepseek-v4.1-flash',
|
|
290
|
-
provider: 'http://127.0.0.1:15721',
|
|
291
|
-
}),
|
|
292
|
-
{ name: 'deepseek-v4.1-flash', detail: 'claude-sonnet-5[1M]', provider: 'http://127.0.0.1:15721', source: null },
|
|
293
|
-
)
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
test('formatExecutorModel: 两个名字相同则不重复进 detail', () => {
|
|
297
|
-
assert.deepEqual(
|
|
298
|
-
formatExecutorModel({ model: 'same', display: 'same', provider: null }),
|
|
299
|
-
{ name: 'same', detail: null, provider: null, source: null },
|
|
300
|
-
)
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
test('formatExecutorModel: 只有别名时就用别名,detail 为空', () => {
|
|
304
|
-
assert.deepEqual(
|
|
305
|
-
formatExecutorModel({ model: 'only-alias', display: null, provider: null }),
|
|
306
|
-
{ name: 'only-alias', detail: null, provider: null, source: null },
|
|
307
|
-
)
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
test('formatExecutorModel: 来源(config / state)照实透传出去', () => {
|
|
311
|
-
assert.equal(
|
|
312
|
-
formatExecutorModel({ model: 'm', display: null, provider: null, source: 'state' }).source,
|
|
313
|
-
'state',
|
|
314
|
-
)
|
|
315
|
-
assert.equal(
|
|
316
|
-
formatExecutorModel({ model: 'm', display: null, provider: null, source: 'config' }).source,
|
|
317
|
-
'config',
|
|
318
|
-
)
|
|
319
|
-
})
|
|
320
|
-
|
|
321
|
-
test('formatExecutorModel: null / 空对象一律 null(前端据此走"未在配置中指定")', () => {
|
|
322
|
-
assert.equal(formatExecutorModel(null), null)
|
|
323
|
-
assert.equal(formatExecutorModel(undefined), null)
|
|
324
|
-
assert.equal(formatExecutorModel({ model: null, display: null, provider: null }), null)
|
|
325
|
-
assert.equal(formatExecutorModel({ model: '', display: '', provider: null }), null)
|
|
326
|
-
})
|
|
1
|
+
// 执行器模型探测的单测。
|
|
2
|
+
//
|
|
3
|
+
// 为什么这些用例值得写:这个模块的全部价值是"读三种格式的配置文件",
|
|
4
|
+
// 而三种格式的解析都是**手写的**(不引 TOML / JSONC 依赖,理由见模块注释)——
|
|
5
|
+
// 手写解析最容易在两个地方悄悄错:注释/空行的截断、以及多文件兜底被丢掉。
|
|
6
|
+
// 这两处都已经真出过 bug(见下面标 [回归] 的用例),所以它们必须钉住。
|
|
7
|
+
//
|
|
8
|
+
// 所有用例都在**临时沙箱 home** 里跑,绝不读真实 ~/ —— 否则用例会随开发机
|
|
9
|
+
// 装没装 claude、配没配模型而时红时绿。
|
|
10
|
+
import { test } from 'node:test'
|
|
11
|
+
import assert from 'node:assert/strict'
|
|
12
|
+
import fs from 'fs/promises'
|
|
13
|
+
import os from 'os'
|
|
14
|
+
import path from 'path'
|
|
15
|
+
import {
|
|
16
|
+
readTomlTopLevel,
|
|
17
|
+
stripJsonComments,
|
|
18
|
+
detectExecutorModels,
|
|
19
|
+
formatExecutorModel,
|
|
20
|
+
} from './executorModels.js'
|
|
21
|
+
|
|
22
|
+
const sandboxes = []
|
|
23
|
+
|
|
24
|
+
/** 造一个只属于本用例的 home 目录,按 {相对路径: 内容} 落文件 */
|
|
25
|
+
async function makeHome(files = {}) {
|
|
26
|
+
const home = await fs.mkdtemp(path.join(os.tmpdir(), 'zen-executor-models-'))
|
|
27
|
+
sandboxes.push(home)
|
|
28
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
29
|
+
const full = path.join(home, rel)
|
|
30
|
+
await fs.mkdir(path.dirname(full), { recursive: true })
|
|
31
|
+
await fs.writeFile(full, content, 'utf8')
|
|
32
|
+
}
|
|
33
|
+
return home
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test.after(async () => {
|
|
37
|
+
for (const home of sandboxes) await fs.rm(home, { recursive: true, force: true })
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// ── readTomlTopLevel ─────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
test('readTomlTopLevel: 注释行与空行不截断顶层读取 [回归]', () => {
|
|
43
|
+
// 老实现把「空行 / 注释行 / 段头」三类一起 break,于是文件第一行是注释时
|
|
44
|
+
// 顶层 model 整个读空 —— 表现为界面上 codex 永远显示"未配置"。
|
|
45
|
+
const toml = [
|
|
46
|
+
'# codex 配置说明',
|
|
47
|
+
'# 第二行注释',
|
|
48
|
+
'model = "gpt-6-astra"',
|
|
49
|
+
'',
|
|
50
|
+
'model_provider = "custom"',
|
|
51
|
+
'',
|
|
52
|
+
'[model_providers.custom]',
|
|
53
|
+
'name = "kakouai"',
|
|
54
|
+
].join('\n')
|
|
55
|
+
assert.deepEqual(readTomlTopLevel(toml, ['model', 'model_provider']), {
|
|
56
|
+
model: 'gpt-6-astra',
|
|
57
|
+
model_provider: 'custom',
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('readTomlTopLevel: 段内的同名键不会被当成顶层值', () => {
|
|
62
|
+
const toml = 'model = "top"\n\n[projects."c:/x"]\nmodel = "inner"\n'
|
|
63
|
+
assert.equal(readTomlTopLevel(toml, ['model']).model, 'top')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('readTomlTopLevel: 只取白名单里的键(其余含密钥的键一律不取)', () => {
|
|
67
|
+
const toml = 'model = "m"\nexperimental_bearer_token = "tok"\napi_key = "k"\n'
|
|
68
|
+
assert.deepEqual(readTomlTopLevel(toml, ['model']), { model: 'm' })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('readTomlTopLevel: 行尾注释不污染值;单引号值也认', () => {
|
|
72
|
+
const toml = "model = 'm-1' # 备注\nmodel_provider = \"custom\"#紧贴的注释\n"
|
|
73
|
+
assert.deepEqual(readTomlTopLevel(toml, ['model', 'model_provider']), {
|
|
74
|
+
model: 'm-1',
|
|
75
|
+
model_provider: 'custom',
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('readTomlTopLevel: 段头里带方括号的路径不被误判成值', () => {
|
|
80
|
+
const toml = 'model = "m"\n\n[projects.\'c:\\weird[path]\']\nmodel = "inner"\n'
|
|
81
|
+
assert.equal(readTomlTopLevel(toml, ['model']).model, 'm')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// ── stripJsonComments ────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
test('stripJsonComments: 字符串里的 // 不被当注释(base_url 场景)', () => {
|
|
87
|
+
const jsonc = '{ "url": "https://api.example.com/v1", // 注释\n "n": 1 }'
|
|
88
|
+
assert.equal(JSON.parse(stripJsonComments(jsonc)).url, 'https://api.example.com/v1')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('stripJsonComments: 剥块注释与尾逗号后仍能 JSON.parse', () => {
|
|
92
|
+
const jsonc = '{\n /* 块\n 注释 */\n "model": "a/b",\n}'
|
|
93
|
+
assert.deepEqual(JSON.parse(stripJsonComments(jsonc)), { model: 'a/b' })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('stripJsonComments: 转义引号不影响"在不在字符串里"的判断', () => {
|
|
97
|
+
const jsonc = '{ "s": "a\\"//b", "n": 1 } // 尾注释'
|
|
98
|
+
assert.deepEqual(JSON.parse(stripJsonComments(jsonc)), { s: 'a"//b', n: 1 })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// ── detectExecutorModels(沙箱 home)─────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
const CLAUDE_JSON = JSON.stringify({
|
|
104
|
+
env: {
|
|
105
|
+
ANTHROPIC_BASE_URL: 'http://127.0.0.1:15721',
|
|
106
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-5[1M]',
|
|
107
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME: 'deepseek-v4.1-flash',
|
|
108
|
+
ANTHROPIC_API_KEY: 'sk-should-never-leak',
|
|
109
|
+
},
|
|
110
|
+
})
|
|
111
|
+
const CODEX_TOML = [
|
|
112
|
+
'# 顶层注释',
|
|
113
|
+
'model = "gpt-6-astra"',
|
|
114
|
+
'model_provider = "custom"',
|
|
115
|
+
'',
|
|
116
|
+
'[model_providers.custom]',
|
|
117
|
+
'name = "kakouai"',
|
|
118
|
+
'experimental_bearer_token = "tok-should-never-leak"',
|
|
119
|
+
'',
|
|
120
|
+
'[projects."c:/x"]',
|
|
121
|
+
'model = "inner-model"',
|
|
122
|
+
].join('\n')
|
|
123
|
+
const OPENCODE_JSONC = '{\n // 注释\n "model": "anthropic/claude-x",\n}'
|
|
124
|
+
|
|
125
|
+
test('detectExecutorModels: 三家配置齐全时各自读出模型', async () => {
|
|
126
|
+
const home = await makeHome({
|
|
127
|
+
'.claude/settings.json': CLAUDE_JSON,
|
|
128
|
+
'.codex/config.toml': CODEX_TOML,
|
|
129
|
+
'.config/opencode/opencode.jsonc': OPENCODE_JSONC,
|
|
130
|
+
})
|
|
131
|
+
const out = await detectExecutorModels({ homeDir: home })
|
|
132
|
+
|
|
133
|
+
assert.deepEqual(out.claude, {
|
|
134
|
+
model: 'claude-sonnet-5[1M]',
|
|
135
|
+
display: 'deepseek-v4.1-flash',
|
|
136
|
+
provider: 'http://127.0.0.1:15721',
|
|
137
|
+
})
|
|
138
|
+
// 顶层注释不再截断 → model_provider 读得到 → 服务商名也能查出来
|
|
139
|
+
assert.deepEqual(out.codex, { model: 'gpt-6-astra', display: null, provider: 'kakouai' })
|
|
140
|
+
// 只有 .jsonc(没有 .json)时也要读到:早先那个 promise 被解构丢掉,这里是死代码
|
|
141
|
+
assert.deepEqual(out.opencode, {
|
|
142
|
+
model: 'anthropic/claude-x', display: null, provider: null, source: 'config',
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
test('detectExecutorModels: 只有 .jsonc 时 opencode 仍读得到 [回归]', async () => {
|
|
147
|
+
const home = await makeHome({ '.config/opencode/opencode.jsonc': OPENCODE_JSONC })
|
|
148
|
+
const out = await detectExecutorModels({ homeDir: home })
|
|
149
|
+
assert.equal(out.opencode?.model, 'anthropic/claude-x')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('detectExecutorModels: .json 与 .jsonc 同时存在时 .json 优先', async () => {
|
|
153
|
+
const home = await makeHome({
|
|
154
|
+
'.config/opencode/opencode.json': '{"model":"from-json"}',
|
|
155
|
+
'.config/opencode/opencode.jsonc': '{"model":"from-jsonc"}',
|
|
156
|
+
})
|
|
157
|
+
const out = await detectExecutorModels({ homeDir: home })
|
|
158
|
+
assert.equal(out.opencode?.model, 'from-json')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('detectExecutorModels: opencode 的 model 是对象时带上 variant', async () => {
|
|
162
|
+
const home = await makeHome({
|
|
163
|
+
'.config/opencode/opencode.json': '{"model":{"model":"openai/gpt-y","variant":"high"}}',
|
|
164
|
+
})
|
|
165
|
+
const out = await detectExecutorModels({ homeDir: home })
|
|
166
|
+
assert.equal(out.opencode?.model, 'openai/gpt-y (high)')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// ── opencode 的第二处来源:TUI 状态 ──────────────────────────────────────────
|
|
170
|
+
//
|
|
171
|
+
// opencode 的 TUI 允许直接选模型,而它把选择**存在自己的 state 里、不回写配置文件**。
|
|
172
|
+
// 只读配置文件的话,对一个明明在跑的模型显示"未在配置中指定" —— 用户会去翻配置,
|
|
173
|
+
// 配置里确实没有,白折腾一场。这组用例钉住"配置文件优先、没有则退回 state"。
|
|
174
|
+
|
|
175
|
+
const OPENCODE_STATE = JSON.stringify({
|
|
176
|
+
recent: [
|
|
177
|
+
{ providerID: 'opencode-go', modelID: 'space-bunny-free' },
|
|
178
|
+
{ providerID: 'opencode-go', modelID: 'deepseek-v4.1-flash' },
|
|
179
|
+
],
|
|
180
|
+
favorite: [],
|
|
181
|
+
variant: { 'opencode-go/space-bunny-free': 'max', 'opencode-go/deepseek-v4.1-flash': 'max' },
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
test('detectExecutorModels: 配置里没写 model 时,退回 TUI 状态里的「最近使用」', async () => {
|
|
185
|
+
const home = await makeHome({
|
|
186
|
+
// 本机真实情形:配置文件里只有一个 $schema
|
|
187
|
+
'.config/opencode/opencode.jsonc': '{\n "$schema": "https://opencode.ai/config.json"\n}',
|
|
188
|
+
'.local/state/opencode/model.json': OPENCODE_STATE,
|
|
189
|
+
})
|
|
190
|
+
const out = await detectExecutorModels({ homeDir: home })
|
|
191
|
+
assert.deepEqual(out.opencode, {
|
|
192
|
+
model: 'opencode-go/space-bunny-free (max)', // recent[0] + variant 表里的 max
|
|
193
|
+
display: null,
|
|
194
|
+
provider: null, // 模型名本身已是 provider/model,不再重复报一次
|
|
195
|
+
source: 'state',
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
test('detectExecutorModels: 配置文件里写了 model 就以它为准(state 只是兜底)', async () => {
|
|
200
|
+
const home = await makeHome({
|
|
201
|
+
'.config/opencode/opencode.json': '{"model":"anthropic/claude-x"}',
|
|
202
|
+
'.local/state/opencode/model.json': OPENCODE_STATE,
|
|
203
|
+
})
|
|
204
|
+
const out = await detectExecutorModels({ homeDir: home })
|
|
205
|
+
assert.deepEqual(out.opencode, {
|
|
206
|
+
model: 'anthropic/claude-x', display: null, provider: null, source: 'config',
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('detectExecutorModels: recent 项自带 variant 时优先于顶层 variant 表', async () => {
|
|
211
|
+
const home = await makeHome({
|
|
212
|
+
'.config/opencode/opencode.jsonc': '',
|
|
213
|
+
'.local/state/opencode/model.json': JSON.stringify({
|
|
214
|
+
recent: [{ providerID: 'p', modelID: 'm', variant: 'own' }],
|
|
215
|
+
variant: { 'p/m': 'mapped' },
|
|
216
|
+
}),
|
|
217
|
+
})
|
|
218
|
+
const out = await detectExecutorModels({ homeDir: home })
|
|
219
|
+
assert.equal(out.opencode?.model, 'p/m (own)')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('detectExecutorModels: state 里没有 recent / 缺字段时仍是 null(不猜)', async () => {
|
|
223
|
+
const empty = await makeHome({ '.local/state/opencode/model.json': '{"recent":[],"favorite":[]}' })
|
|
224
|
+
assert.equal((await detectExecutorModels({ homeDir: empty })).opencode, null)
|
|
225
|
+
|
|
226
|
+
const partial = await makeHome({
|
|
227
|
+
'.local/state/opencode/model.json': '{"recent":[{"providerID":"p"}]}',
|
|
228
|
+
})
|
|
229
|
+
assert.equal((await detectExecutorModels({ homeDir: partial })).opencode, null)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
test('detectExecutorModels: 两处都没有 model 才是真的"未在配置中指定"', async () => {
|
|
233
|
+
const home = await makeHome({ '.config/opencode/opencode.json': '{"model":""}' })
|
|
234
|
+
assert.equal((await detectExecutorModels({ homeDir: home })).opencode, null)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('detectExecutorModels: claude 显式 ANTHROPIC_MODEL 优先于档位别名', async () => {
|
|
238
|
+
const home = await makeHome({
|
|
239
|
+
'.claude/settings.json': JSON.stringify({
|
|
240
|
+
env: {
|
|
241
|
+
ANTHROPIC_MODEL: 'my-explicit-model',
|
|
242
|
+
ANTHROPIC_BASE_URL: 'http://proxy.local',
|
|
243
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: 'sonnet-alias',
|
|
244
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL_NAME: 'sonnet-real',
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
})
|
|
248
|
+
const out = await detectExecutorModels({ homeDir: home })
|
|
249
|
+
// 显式指定时没有"别名背后"的第二层,但 base_url 仍要带出来
|
|
250
|
+
assert.deepEqual(out.claude, {
|
|
251
|
+
model: 'my-explicit-model',
|
|
252
|
+
display: null,
|
|
253
|
+
provider: 'http://proxy.local',
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test('detectExecutorModels: 一个配置文件都没有时三项全 null 且不抛', async () => {
|
|
258
|
+
const home = await makeHome()
|
|
259
|
+
const out = await detectExecutorModels({ homeDir: home })
|
|
260
|
+
assert.deepEqual(out, { claude: null, codex: null, opencode: null })
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('detectExecutorModels: 配置残缺/是目录/内容不是 JSON 时只让自己变 null', async () => {
|
|
264
|
+
const home = await makeHome({
|
|
265
|
+
'.claude/settings.json': '{ 这不是 JSON', // 解析失败 → null
|
|
266
|
+
'.codex/config.toml': '# 只有注释,没有 model\n', // 无 model → null
|
|
267
|
+
'.config/opencode/opencode.json': '{"model":""}', // 空串 → null
|
|
268
|
+
})
|
|
269
|
+
const out = await detectExecutorModels({ homeDir: home })
|
|
270
|
+
assert.deepEqual(out, { claude: null, codex: null, opencode: null })
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
test('detectExecutorModels: 返回值里绝不出现任何密钥', async () => {
|
|
274
|
+
const home = await makeHome({
|
|
275
|
+
'.claude/settings.json': CLAUDE_JSON,
|
|
276
|
+
'.codex/config.toml': CODEX_TOML,
|
|
277
|
+
})
|
|
278
|
+
const serialized = JSON.stringify(await detectExecutorModels({ homeDir: home }))
|
|
279
|
+
assert.equal(serialized.includes('sk-should-never-leak'), false)
|
|
280
|
+
assert.equal(serialized.includes('tok-should-never-leak'), false)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
// ── formatExecutorModel ──────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
test('formatExecutorModel: display 优先,别名落到 detail', () => {
|
|
286
|
+
assert.deepEqual(
|
|
287
|
+
formatExecutorModel({
|
|
288
|
+
model: 'claude-sonnet-5[1M]',
|
|
289
|
+
display: 'deepseek-v4.1-flash',
|
|
290
|
+
provider: 'http://127.0.0.1:15721',
|
|
291
|
+
}),
|
|
292
|
+
{ name: 'deepseek-v4.1-flash', detail: 'claude-sonnet-5[1M]', provider: 'http://127.0.0.1:15721', source: null },
|
|
293
|
+
)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test('formatExecutorModel: 两个名字相同则不重复进 detail', () => {
|
|
297
|
+
assert.deepEqual(
|
|
298
|
+
formatExecutorModel({ model: 'same', display: 'same', provider: null }),
|
|
299
|
+
{ name: 'same', detail: null, provider: null, source: null },
|
|
300
|
+
)
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
test('formatExecutorModel: 只有别名时就用别名,detail 为空', () => {
|
|
304
|
+
assert.deepEqual(
|
|
305
|
+
formatExecutorModel({ model: 'only-alias', display: null, provider: null }),
|
|
306
|
+
{ name: 'only-alias', detail: null, provider: null, source: null },
|
|
307
|
+
)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
test('formatExecutorModel: 来源(config / state)照实透传出去', () => {
|
|
311
|
+
assert.equal(
|
|
312
|
+
formatExecutorModel({ model: 'm', display: null, provider: null, source: 'state' }).source,
|
|
313
|
+
'state',
|
|
314
|
+
)
|
|
315
|
+
assert.equal(
|
|
316
|
+
formatExecutorModel({ model: 'm', display: null, provider: null, source: 'config' }).source,
|
|
317
|
+
'config',
|
|
318
|
+
)
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('formatExecutorModel: null / 空对象一律 null(前端据此走"未在配置中指定")', () => {
|
|
322
|
+
assert.equal(formatExecutorModel(null), null)
|
|
323
|
+
assert.equal(formatExecutorModel(undefined), null)
|
|
324
|
+
assert.equal(formatExecutorModel({ model: null, display: null, provider: null }), null)
|
|
325
|
+
assert.equal(formatExecutorModel({ model: '', display: '', provider: null }), null)
|
|
326
|
+
})
|