@steerable/agent-shell-web 0.6.15 → 0.6.17
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 +19 -12
- package/vite.base.d.ts +7 -0
- package/vite.base.js +83 -0
- package/src/brand.test.ts +0 -22
- package/src/components/AgentSidebar.test.tsx +0 -127
- package/src/components/ChatHeader.test.tsx +0 -230
- package/src/components/LocalLlmSettingsModal.test.tsx +0 -372
- package/src/components/chat/ApprovalModal.test.tsx +0 -83
- package/src/components/chat/AskUserModalHost.test.tsx +0 -176
- package/src/components/chat/AssistantMessage.test.tsx +0 -68
- package/src/components/chat/ChatInput.test.tsx +0 -609
- package/src/components/chat/ExecPolicyPicker.test.tsx +0 -26
- package/src/components/chat/Markdown.test.tsx +0 -170
- package/src/components/chat/ModelPicker.test.tsx +0 -149
- package/src/components/chat/SessionTreeModal.test.tsx +0 -161
- package/src/components/chat/TaskOutcomeCards.test.tsx +0 -97
- package/src/components/chat/TaskPanelModal.test.tsx +0 -198
- package/src/components/chat/TaskProcessPanel.test.tsx +0 -111
- package/src/components/chat/TurnProcessGroup.test.tsx +0 -161
- package/src/components/chat/elapsed.test.ts +0 -42
- package/src/components/chat/executed-actions-model.test.ts +0 -134
- package/src/components/chat/orchestration-children-model.test.ts +0 -115
- package/src/components/chat/turn-timeline.test.ts +0 -97
- package/src/components/chat/useChatTasks.test.ts +0 -136
- package/src/components/settings/AgentsSettingsPanel.test.tsx +0 -229
- package/src/components/settings/InsightsSettingsPanel.test.tsx +0 -132
- package/src/components/settings/ModelIdCombobox.test.tsx +0 -88
- package/src/components/settings/SecuritySettingsPanel.test.tsx +0 -122
- package/src/components/settings/SkillsSettingsPanel.test.tsx +0 -62
- package/src/components/settings/WebSearchSettingsPanel.test.tsx +0 -90
- package/src/components/settings/compat-flags-model.test.ts +0 -71
- package/src/components/settings/llm-vendors.test.ts +0 -171
- package/src/components/settings/model-capabilities.test.ts +0 -88
- package/src/components/settings/preset-choice-model.test.ts +0 -96
- package/src/lib/pending-first-message.test.ts +0 -17
- package/src/pages/SettingsPage.test.tsx +0 -102
- package/vite.base.ts +0 -94
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LocalLlmSettingsModal 的厂商参数预制交互(R16 配置入口):
|
|
3
|
-
* - openai-compat 显示预制区,ollama 不显示;
|
|
4
|
-
* - 「自动」档按 baseUrl+model 向框架 resolve 并预览命中参数;
|
|
5
|
-
* - 「关闭」保存 {enabled:false};「指定预制」下拉选中后保存 override;
|
|
6
|
-
* - Temperature 自动/手动切换:自动 = 不下发(undefined),手动滑杆持久化显式值。
|
|
7
|
-
* 进程/wire 层由 tests/e2e/presets.e2e.test.ts 覆盖;这里验用户看得见的交互。
|
|
8
|
-
*/
|
|
9
|
-
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
10
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
-
import type { LlmSettings, ProviderPresetDescriptor } from '@/lib/local-api';
|
|
12
|
-
|
|
13
|
-
const getLlmSettings = vi.fn();
|
|
14
|
-
const setLlmSettings = vi.fn();
|
|
15
|
-
const getCompatFlags = vi.fn();
|
|
16
|
-
const getProviderPresets = vi.fn();
|
|
17
|
-
const resolveProviderPreset = vi.fn();
|
|
18
|
-
const getCatalogProviders = vi.fn();
|
|
19
|
-
const getLlmModels = vi.fn();
|
|
20
|
-
|
|
21
|
-
vi.mock('@/lib/electron-bridge', () => ({
|
|
22
|
-
isElectron: () => true,
|
|
23
|
-
getElectronBridge: () => undefined,
|
|
24
|
-
}));
|
|
25
|
-
vi.mock('@/lib/local-api', () => ({
|
|
26
|
-
getLlmSettings: () => getLlmSettings(),
|
|
27
|
-
setLlmSettings: (s: LlmSettings) => setLlmSettings(s),
|
|
28
|
-
getCompatFlags: () => getCompatFlags(),
|
|
29
|
-
getProviderPresets: () => getProviderPresets(),
|
|
30
|
-
resolveProviderPreset: (baseUrl?: string, model?: string) =>
|
|
31
|
-
resolveProviderPreset(baseUrl, model),
|
|
32
|
-
getCatalogProviders: () => getCatalogProviders(),
|
|
33
|
-
getLlmModels: (draft?: {
|
|
34
|
-
baseUrl?: string;
|
|
35
|
-
apiKey?: string;
|
|
36
|
-
provider?: string;
|
|
37
|
-
refresh?: boolean;
|
|
38
|
-
}) => getLlmModels(draft),
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
const { LocalLlmSettingsModal } = await import('./LocalLlmSettingsModal');
|
|
42
|
-
|
|
43
|
-
const DEEPSEEK: ProviderPresetDescriptor = {
|
|
44
|
-
host: 'api.deepseek.com',
|
|
45
|
-
modelPrefix: 'deepseek',
|
|
46
|
-
temperature: 0.0,
|
|
47
|
-
topP: null,
|
|
48
|
-
maxTokens: null,
|
|
49
|
-
reasoningEffort: null,
|
|
50
|
-
extraBody: null,
|
|
51
|
-
};
|
|
52
|
-
const QWEN: ProviderPresetDescriptor = {
|
|
53
|
-
host: null,
|
|
54
|
-
modelPrefix: 'qwen3',
|
|
55
|
-
temperature: 0.6,
|
|
56
|
-
topP: 0.95,
|
|
57
|
-
maxTokens: null,
|
|
58
|
-
reasoningEffort: null,
|
|
59
|
-
extraBody: { top_k: 20 },
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const BASE_SETTINGS: LlmSettings = {
|
|
63
|
-
provider: 'openai-compat',
|
|
64
|
-
model: 'deepseek-chat',
|
|
65
|
-
baseUrl: 'https://api.deepseek.com',
|
|
66
|
-
apiKey: 'sk-test',
|
|
67
|
-
maxTotalTokens: 60000,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
function presetSection(): HTMLElement {
|
|
71
|
-
return screen.getByText('厂商参数预制').closest('div')!.parentElement as HTMLElement;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function renderOpen(settings: Partial<LlmSettings> = {}) {
|
|
75
|
-
getLlmSettings.mockResolvedValue({ ...BASE_SETTINGS, ...settings });
|
|
76
|
-
const utils = render(<LocalLlmSettingsModal open onClose={() => {}} />);
|
|
77
|
-
// 等 reload 完成:预制区出现即数据就绪。
|
|
78
|
-
await screen.findByText('厂商参数预制');
|
|
79
|
-
return utils;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
beforeEach(() => {
|
|
83
|
-
vi.clearAllMocks();
|
|
84
|
-
getCompatFlags.mockResolvedValue({ flags: [] });
|
|
85
|
-
getProviderPresets.mockResolvedValue({ presets: [DEEPSEEK, QWEN] });
|
|
86
|
-
resolveProviderPreset.mockResolvedValue({ preset: { temperature: 0.0 } });
|
|
87
|
-
getCatalogProviders.mockResolvedValue({
|
|
88
|
-
providers: [
|
|
89
|
-
{
|
|
90
|
-
id: 'deepseek',
|
|
91
|
-
apiBaseUrl: 'https://api.deepseek.com',
|
|
92
|
-
envVars: ['DEEPSEEK_API_KEY'],
|
|
93
|
-
wireKind: 'openai_compat',
|
|
94
|
-
models: ['deepseek-chat', 'deepseek-v4-flash'],
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
id: 'anthropic',
|
|
98
|
-
apiBaseUrl: 'https://api.anthropic.com',
|
|
99
|
-
envVars: ['ANTHROPIC_API_KEY'],
|
|
100
|
-
wireKind: 'anthropic',
|
|
101
|
-
models: ['claude-sonnet-4-6', 'claude-opus-4-7'],
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
});
|
|
105
|
-
getLlmModels.mockResolvedValue({ models: [], catalogStatus: 'offline' });
|
|
106
|
-
setLlmSettings.mockImplementation(async (s: LlmSettings) => s);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
afterEach(cleanup);
|
|
110
|
-
|
|
111
|
-
describe('LocalLlmSettingsModal — 遮罩关闭', () => {
|
|
112
|
-
it('点在遮罩上关闭;面板内按下后在遮罩弹起不关', async () => {
|
|
113
|
-
const onClose = vi.fn();
|
|
114
|
-
getLlmSettings.mockResolvedValue(BASE_SETTINGS);
|
|
115
|
-
render(<LocalLlmSettingsModal open onClose={onClose} />);
|
|
116
|
-
const dialog = await screen.findByTestId('llm-settings-dialog');
|
|
117
|
-
const panel = dialog.firstElementChild as HTMLElement;
|
|
118
|
-
|
|
119
|
-
fireEvent.mouseDown(panel);
|
|
120
|
-
fireEvent.mouseUp(dialog);
|
|
121
|
-
fireEvent.click(dialog);
|
|
122
|
-
expect(onClose).not.toHaveBeenCalled();
|
|
123
|
-
|
|
124
|
-
fireEvent.mouseDown(dialog);
|
|
125
|
-
expect(onClose).toHaveBeenCalledTimes(1);
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe('LocalLlmSettingsModal — 厂商参数预制', () => {
|
|
130
|
-
it('openai-compat 显示预制区;ollama 不显示', async () => {
|
|
131
|
-
const { unmount } = await renderOpen();
|
|
132
|
-
expect(screen.getByText('厂商参数预制')).toBeTruthy();
|
|
133
|
-
expect(screen.queryByText('本地技能管理')).toBeNull();
|
|
134
|
-
expect(screen.queryByText('MCP 服务')).toBeNull();
|
|
135
|
-
unmount();
|
|
136
|
-
|
|
137
|
-
getLlmSettings.mockResolvedValue({
|
|
138
|
-
...BASE_SETTINGS,
|
|
139
|
-
provider: 'ollama',
|
|
140
|
-
model: 'llama3.1:8b',
|
|
141
|
-
baseUrl: 'http://127.0.0.1:11434',
|
|
142
|
-
});
|
|
143
|
-
render(<LocalLlmSettingsModal open onClose={() => {}} />);
|
|
144
|
-
await screen.findByText('Temperature');
|
|
145
|
-
expect(screen.queryByText('厂商参数预制')).toBeNull();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('「自动」档预览命中参数,保存不写 presets(注册表自动匹配)', async () => {
|
|
149
|
-
await renderOpen();
|
|
150
|
-
// 防抖 300ms 后展示 resolve 结果。
|
|
151
|
-
const preview = await screen.findByText(/命中预制:temperature 0/, undefined, {
|
|
152
|
-
timeout: 3000,
|
|
153
|
-
});
|
|
154
|
-
expect(preview).toBeTruthy();
|
|
155
|
-
expect(resolveProviderPreset).toHaveBeenCalledWith(
|
|
156
|
-
'https://api.deepseek.com',
|
|
157
|
-
'deepseek-chat',
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
fireEvent.click(screen.getByText('保存'));
|
|
161
|
-
await waitFor(() => expect(setLlmSettings).toHaveBeenCalled());
|
|
162
|
-
expect(setLlmSettings.mock.calls[0][0].presets).toBeUndefined();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('「关闭」保存 {enabled:false},预览说明不下发', async () => {
|
|
166
|
-
await renderOpen();
|
|
167
|
-
fireEvent.click(document.querySelector('[data-preset-mode="off"]')!);
|
|
168
|
-
expect(screen.getByText('已关闭:不下发厂商预制参数。')).toBeTruthy();
|
|
169
|
-
|
|
170
|
-
fireEvent.click(screen.getByText('保存'));
|
|
171
|
-
await waitFor(() => expect(setLlmSettings).toHaveBeenCalled());
|
|
172
|
-
expect(setLlmSettings.mock.calls[0][0].presets).toEqual({ enabled: false });
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('「指定预制」下拉选中注册表条目,保存其 override', async () => {
|
|
176
|
-
await renderOpen();
|
|
177
|
-
fireEvent.click(document.querySelector('[data-preset-mode="pinned"]')!);
|
|
178
|
-
|
|
179
|
-
const select = presetSection().querySelector('select')!;
|
|
180
|
-
expect(select).toBeTruthy();
|
|
181
|
-
// 选中 qwen3 行(注册表第二个条目)。
|
|
182
|
-
fireEvent.change(select, { target: { value: '1' } });
|
|
183
|
-
expect(
|
|
184
|
-
screen.getByText(/生效参数:temperature 0.6 · top_p 0.95 · top_k 20/),
|
|
185
|
-
).toBeTruthy();
|
|
186
|
-
|
|
187
|
-
fireEvent.click(screen.getByText('保存'));
|
|
188
|
-
await waitFor(() => expect(setLlmSettings).toHaveBeenCalled());
|
|
189
|
-
expect(setLlmSettings.mock.calls[0][0].presets).toEqual({
|
|
190
|
-
override: { temperature: 0.6, topP: 0.95, extraBody: { top_k: 20 } },
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('已保存的钉死覆盖在注册表命中时回选对应行', async () => {
|
|
195
|
-
await renderOpen({
|
|
196
|
-
presets: { override: { temperature: 0.6, topP: 0.95, extraBody: { top_k: 20 } } },
|
|
197
|
-
});
|
|
198
|
-
// pinned 档直接展示下拉且选中 qwen3 行(下标 1)。
|
|
199
|
-
const select = presetSection().querySelector('select')!;
|
|
200
|
-
expect(select.value).toBe('1');
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('Temperature 默认自动(无滑杆、保存不下发),切手动出滑杆并持久化', async () => {
|
|
204
|
-
await renderOpen();
|
|
205
|
-
// 自动:提示文案在,滑杆不在;命中预览带入当前温度。
|
|
206
|
-
await screen.findByText(/自动:命中厂商预制时用预制温度/, undefined, {
|
|
207
|
-
timeout: 3000,
|
|
208
|
-
});
|
|
209
|
-
await screen.findByText(/(当前命中:0)/, undefined, { timeout: 3000 });
|
|
210
|
-
expect(document.querySelector('input[type="range"]')).toBeNull();
|
|
211
|
-
|
|
212
|
-
fireEvent.click(screen.getByText('保存'));
|
|
213
|
-
await waitFor(() => expect(setLlmSettings).toHaveBeenCalled());
|
|
214
|
-
expect(setLlmSettings.mock.calls[0][0].temperature).toBeUndefined();
|
|
215
|
-
|
|
216
|
-
// 手动:滑杆出现,初值取命中预制的温度(0.0)。
|
|
217
|
-
setLlmSettings.mockClear();
|
|
218
|
-
fireEvent.click(document.querySelector('[data-temperature-mode="manual"]')!);
|
|
219
|
-
const slider = document.querySelector('input[type="range"]') as HTMLInputElement;
|
|
220
|
-
expect(slider).toBeTruthy();
|
|
221
|
-
expect(slider.value).toBe('0');
|
|
222
|
-
|
|
223
|
-
fireEvent.change(slider, { target: { value: '0.5' } });
|
|
224
|
-
fireEvent.click(screen.getByText('保存'));
|
|
225
|
-
await waitFor(() => expect(setLlmSettings).toHaveBeenCalled());
|
|
226
|
-
expect(setLlmSettings.mock.calls[0][0].temperature).toBe(0.5);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('选择 Anthropic 自动填 URL 并隐藏 OpenAI 预制区', async () => {
|
|
230
|
-
await renderOpen();
|
|
231
|
-
fireEvent.change(screen.getByTestId('llm-vendor-select'), { target: { value: 'anthropic' } });
|
|
232
|
-
const url = screen.getByPlaceholderText('https://api.anthropic.com') as HTMLInputElement;
|
|
233
|
-
expect(url.value).toBe('https://api.anthropic.com');
|
|
234
|
-
expect(screen.queryByText('厂商参数预制')).toBeNull();
|
|
235
|
-
expect((screen.getByTestId('llm-model-input') as HTMLInputElement).value).toBe(
|
|
236
|
-
'claude-sonnet-4-6',
|
|
237
|
-
);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('选择自定义后 URL 留空供手填', async () => {
|
|
241
|
-
await renderOpen();
|
|
242
|
-
fireEvent.change(screen.getByTestId('llm-vendor-select'), { target: { value: 'custom' } });
|
|
243
|
-
const url = screen.getByPlaceholderText('https://your-gateway.example/v1') as HTMLInputElement;
|
|
244
|
-
expect(url.value).toBe('');
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('保存时写入 vendorId 与对应的 sidecar provider', async () => {
|
|
248
|
-
await renderOpen();
|
|
249
|
-
fireEvent.change(screen.getByTestId('llm-vendor-select'), { target: { value: 'anthropic' } });
|
|
250
|
-
fireEvent.click(screen.getByText('保存'));
|
|
251
|
-
await waitFor(() => expect(setLlmSettings).toHaveBeenCalled());
|
|
252
|
-
expect(setLlmSettings.mock.calls[0][0]).toMatchObject({
|
|
253
|
-
provider: 'anthropic',
|
|
254
|
-
vendorId: 'anthropic',
|
|
255
|
-
baseUrl: 'https://api.anthropic.com',
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it('模型下拉以网关实时目录为准,不混入服务商内置列表', async () => {
|
|
260
|
-
getLlmModels.mockResolvedValue({
|
|
261
|
-
models: [{ id: 'deepseek-chat' }, { id: 'deepseek-reasoner' }],
|
|
262
|
-
catalogStatus: 'live',
|
|
263
|
-
});
|
|
264
|
-
await renderOpen();
|
|
265
|
-
await waitFor(() =>
|
|
266
|
-
expect(screen.getByText(/已从网关拉取 2 个模型/)).toBeTruthy(),
|
|
267
|
-
);
|
|
268
|
-
fireEvent.focus(screen.getByTestId('llm-model-input'));
|
|
269
|
-
expect(screen.getByRole('option', { name: 'deepseek-reasoner' })).toBeTruthy();
|
|
270
|
-
expect(screen.queryByRole('option', { name: 'deepseek-v4-flash' })).toBeNull();
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('刷新按钮绕过缓存重新拉取网关目录', async () => {
|
|
274
|
-
getLlmModels.mockResolvedValue({
|
|
275
|
-
models: [{ id: 'deepseek-chat' }],
|
|
276
|
-
catalogStatus: 'live',
|
|
277
|
-
});
|
|
278
|
-
await renderOpen();
|
|
279
|
-
await waitFor(() =>
|
|
280
|
-
expect(screen.getByText(/已从网关拉取 1 个模型/)).toBeTruthy(),
|
|
281
|
-
);
|
|
282
|
-
getLlmModels.mockClear();
|
|
283
|
-
getLlmModels.mockResolvedValue({
|
|
284
|
-
models: [{ id: 'deepseek-flash' }, { id: 'deepseek-v4-pro' }],
|
|
285
|
-
catalogStatus: 'live',
|
|
286
|
-
});
|
|
287
|
-
fireEvent.click(screen.getByTestId('llm-model-refresh'));
|
|
288
|
-
await waitFor(() => expect(getLlmModels).toHaveBeenCalled());
|
|
289
|
-
expect(getLlmModels.mock.calls.at(-1)?.[0]).toMatchObject({ refresh: true });
|
|
290
|
-
await waitFor(() =>
|
|
291
|
-
expect(screen.getByText(/已从网关拉取 2 个模型/)).toBeTruthy(),
|
|
292
|
-
);
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('测试按钮用当前凭证验证网关并回报结果', async () => {
|
|
296
|
-
getLlmModels.mockResolvedValue({
|
|
297
|
-
models: [{ id: 'deepseek-chat' }],
|
|
298
|
-
catalogStatus: 'live',
|
|
299
|
-
});
|
|
300
|
-
await renderOpen();
|
|
301
|
-
await waitFor(() => expect(screen.getByTestId('llm-key-test')).toBeTruthy());
|
|
302
|
-
getLlmModels.mockClear();
|
|
303
|
-
getLlmModels.mockResolvedValue({
|
|
304
|
-
models: [{ id: 'deepseek-flash' }, { id: 'deepseek-v4-pro' }],
|
|
305
|
-
catalogStatus: 'live',
|
|
306
|
-
});
|
|
307
|
-
fireEvent.click(screen.getByTestId('llm-key-test'));
|
|
308
|
-
await waitFor(() => expect(getLlmModels).toHaveBeenCalled());
|
|
309
|
-
expect(getLlmModels.mock.calls.at(-1)?.[0]).toMatchObject({ refresh: true });
|
|
310
|
-
await waitFor(() =>
|
|
311
|
-
expect(screen.getByText(/凭证可用,网关返回 2 个模型/)).toBeTruthy(),
|
|
312
|
-
);
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it('测试失败时展示网关错误', async () => {
|
|
316
|
-
getLlmModels.mockResolvedValue({
|
|
317
|
-
models: [],
|
|
318
|
-
catalogStatus: 'offline',
|
|
319
|
-
error: '401 unauthorized',
|
|
320
|
-
});
|
|
321
|
-
await renderOpen();
|
|
322
|
-
await waitFor(() => expect(screen.getByTestId('llm-key-test')).toBeTruthy());
|
|
323
|
-
fireEvent.click(screen.getByTestId('llm-key-test'));
|
|
324
|
-
await waitFor(() =>
|
|
325
|
-
expect(screen.getByText(/验证失败:401 unauthorized/)).toBeTruthy(),
|
|
326
|
-
);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it('选中模型展示核实过的思考档位,未 join 的 id 只标未识别', async () => {
|
|
330
|
-
getLlmSettings.mockResolvedValue({
|
|
331
|
-
...BASE_SETTINGS,
|
|
332
|
-
model: 'deepseek-v4-pro',
|
|
333
|
-
});
|
|
334
|
-
getLlmModels.mockResolvedValue({
|
|
335
|
-
models: [
|
|
336
|
-
{
|
|
337
|
-
id: 'deepseek-v4-pro',
|
|
338
|
-
name: 'DeepSeek V4 Pro',
|
|
339
|
-
window: 1_000_000,
|
|
340
|
-
modalities: ['text'],
|
|
341
|
-
reasoningLevels: ['high', 'max'],
|
|
342
|
-
pricing: null,
|
|
343
|
-
joinedFrom: 'deepseek/deepseek-v4-pro',
|
|
344
|
-
capabilities: 'known',
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
id: 'deepseek-flash',
|
|
348
|
-
name: null,
|
|
349
|
-
window: 131_072,
|
|
350
|
-
modalities: ['text'],
|
|
351
|
-
reasoningLevels: [],
|
|
352
|
-
pricing: null,
|
|
353
|
-
joinedFrom: null,
|
|
354
|
-
capabilities: 'unknown',
|
|
355
|
-
},
|
|
356
|
-
],
|
|
357
|
-
catalogStatus: 'live',
|
|
358
|
-
});
|
|
359
|
-
await renderOpen({ model: 'deepseek-v4-pro' });
|
|
360
|
-
const summary = await screen.findByTestId('llm-model-capabilities');
|
|
361
|
-
expect(summary.textContent).toContain('思考(high / max)');
|
|
362
|
-
expect(summary.textContent).toContain('1M');
|
|
363
|
-
expect(summary.textContent).not.toContain('未识别');
|
|
364
|
-
|
|
365
|
-
fireEvent.focus(screen.getByTestId('llm-model-input'));
|
|
366
|
-
fireEvent.click(screen.getByRole('option', { name: 'deepseek-flash' }));
|
|
367
|
-
await waitFor(() =>
|
|
368
|
-
expect(screen.getByTestId('llm-model-capabilities').textContent).toContain('未识别'),
|
|
369
|
-
);
|
|
370
|
-
expect(screen.getByTestId('llm-model-capabilities').textContent).not.toContain('131K');
|
|
371
|
-
});
|
|
372
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ApprovalModalHost 的网络出口(network_egress)分支契约(W-egress-ask):
|
|
3
|
-
* - category=network_egress 的请示以「请求访问外网」标题 + host:port
|
|
4
|
-
* 展示,并带仿冒域名警示;
|
|
5
|
-
* - 「始终」变体对网络出口隐藏(代理白名单是会话寿命的进程级状态,
|
|
6
|
-
* 持久放行要走设置的出网白名单,不是这个模态);
|
|
7
|
-
* - 普通工具调用的 7 变体行为不回归。
|
|
8
|
-
*/
|
|
9
|
-
import { act, cleanup, render, screen } from '@testing-library/react';
|
|
10
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
-
import type { ApprovalPromptRequest } from '@/lib/electron-bridge';
|
|
12
|
-
import { ApprovalModalHost } from './ApprovalModal';
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
cleanup();
|
|
16
|
-
delete (window as { electron?: unknown }).electron;
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
function installBridge() {
|
|
20
|
-
const listeners = new Set<(request: ApprovalPromptRequest) => void>();
|
|
21
|
-
const decide = vi.fn().mockResolvedValue(undefined);
|
|
22
|
-
(window as { electron?: unknown }).electron = {
|
|
23
|
-
approval: {
|
|
24
|
-
onRequest: (callback: (request: ApprovalPromptRequest) => void) => {
|
|
25
|
-
listeners.add(callback);
|
|
26
|
-
return () => listeners.delete(callback);
|
|
27
|
-
},
|
|
28
|
-
decide,
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
return {
|
|
32
|
-
decide,
|
|
33
|
-
emit: (request: ApprovalPromptRequest) => {
|
|
34
|
-
for (const callback of Array.from(listeners)) callback(request);
|
|
35
|
-
},
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const EGRESS_REQUEST: ApprovalPromptRequest = {
|
|
40
|
-
requestId: 'req-egress',
|
|
41
|
-
toolName: 'network_egress',
|
|
42
|
-
arguments: { host: 'cdn.example.com', port: 443, url: 'https://cdn.example.com/x.js' },
|
|
43
|
-
mode: 'other',
|
|
44
|
-
category: 'network_egress',
|
|
45
|
-
round: 0,
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const SHELL_REQUEST: ApprovalPromptRequest = {
|
|
49
|
-
requestId: 'req-shell',
|
|
50
|
-
toolName: 'bash',
|
|
51
|
-
arguments: { command: 'rm -rf /tmp/x' },
|
|
52
|
-
mode: 'destructive',
|
|
53
|
-
category: 'bash',
|
|
54
|
-
round: 0,
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
describe('ApprovalModalHost 网络出口分支(W-egress-ask)', () => {
|
|
58
|
-
it('以 host:port 标题 + 仿冒警示展示,并隐藏「始终」变体', () => {
|
|
59
|
-
const bridge = installBridge();
|
|
60
|
-
render(<ApprovalModalHost />);
|
|
61
|
-
act(() => bridge.emit(EGRESS_REQUEST));
|
|
62
|
-
|
|
63
|
-
expect(screen.getByText('Agent 请求访问外网')).toBeTruthy();
|
|
64
|
-
expect(screen.getByText('cdn.example.com:443')).toBeTruthy();
|
|
65
|
-
expect(screen.getByText(/仿冒域名/)).toBeTruthy();
|
|
66
|
-
// 会话档保留,「始终」档隐藏。
|
|
67
|
-
expect(screen.getByText('允许一次')).toBeTruthy();
|
|
68
|
-
expect(screen.getByText('本次会话允许')).toBeTruthy();
|
|
69
|
-
expect(screen.queryByText('始终允许')).toBeNull();
|
|
70
|
-
expect(screen.queryByText('始终拒绝')).toBeNull();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('普通工具调用的 7 变体不回归', () => {
|
|
74
|
-
const bridge = installBridge();
|
|
75
|
-
render(<ApprovalModalHost />);
|
|
76
|
-
act(() => bridge.emit(SHELL_REQUEST));
|
|
77
|
-
|
|
78
|
-
expect(screen.getByText('Agent 请求执行')).toBeTruthy();
|
|
79
|
-
expect(screen.getByText('始终允许')).toBeTruthy();
|
|
80
|
-
expect(screen.getByText('始终拒绝')).toBeTruthy();
|
|
81
|
-
expect(screen.getByText(/工作区沙箱/)).toBeTruthy();
|
|
82
|
-
});
|
|
83
|
-
});
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AskUserModalHost 的接线契约(W8):
|
|
3
|
-
* - `ask-user:request` 广播到达时渲染分步菜单卡片(intro + 问题菜单);
|
|
4
|
-
* - 单选点击/Enter 直接提交并进入下一题;文本题 Enter 进入下一题;
|
|
5
|
-
* - 「其他 / 自定义」展开输入框,输入直接作为该题答案(不产生新问题);
|
|
6
|
-
* - 提交把 requestId + 答案映射经 bridge.askUser.answer 送回主进程;
|
|
7
|
-
* - 「交给我决定」应答空映射(fail-open:模型自行推进);
|
|
8
|
-
* - 应答后卡片关闭,队列中的下一组问题顶上来。
|
|
9
|
-
*/
|
|
10
|
-
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
11
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
-
import type { AskUserPromptRequest } from '@/lib/electron-bridge';
|
|
13
|
-
import { AskUserModalHost } from './AskUserModalHost';
|
|
14
|
-
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
cleanup();
|
|
17
|
-
delete (window as { electron?: unknown }).electron;
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
function installBridge() {
|
|
21
|
-
const listeners = new Set<(request: AskUserPromptRequest) => void>();
|
|
22
|
-
const answer = vi.fn().mockResolvedValue(undefined);
|
|
23
|
-
(window as { electron?: unknown }).electron = {
|
|
24
|
-
askUser: {
|
|
25
|
-
onRequest: (callback: (request: AskUserPromptRequest) => void) => {
|
|
26
|
-
listeners.add(callback);
|
|
27
|
-
return () => listeners.delete(callback);
|
|
28
|
-
},
|
|
29
|
-
answer,
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
return {
|
|
33
|
-
answer,
|
|
34
|
-
emit: (request: AskUserPromptRequest) => {
|
|
35
|
-
for (const callback of Array.from(listeners)) callback(request);
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const REQUEST: AskUserPromptRequest = {
|
|
41
|
-
requestId: 'req-1',
|
|
42
|
-
intro: '部署前确认',
|
|
43
|
-
questions: [
|
|
44
|
-
{ id: 'env', text: '目标环境?', type: 'select', options: ['staging', 'prod'], multiSelect: false },
|
|
45
|
-
],
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const REQUEST_TWO: AskUserPromptRequest = {
|
|
49
|
-
requestId: 'req-two',
|
|
50
|
-
intro: '部署前确认',
|
|
51
|
-
questions: [
|
|
52
|
-
{ id: 'env', text: '目标环境?', type: 'select', options: ['staging', 'prod'] },
|
|
53
|
-
{ id: 'note', text: '备注', type: 'text' },
|
|
54
|
-
],
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
describe('AskUserModalHost(W8)', () => {
|
|
58
|
-
it('无请求时不渲染;请求到达后渲染问题卡片', () => {
|
|
59
|
-
const bridge = installBridge();
|
|
60
|
-
render(<AskUserModalHost />);
|
|
61
|
-
expect(screen.queryByRole('dialog')).toBeNull();
|
|
62
|
-
|
|
63
|
-
act(() => bridge.emit(REQUEST));
|
|
64
|
-
expect(screen.getByRole('dialog', { name: 'Agent 提问' })).toBeTruthy();
|
|
65
|
-
expect(screen.getByText('部署前确认')).toBeTruthy();
|
|
66
|
-
expect(screen.getByText(/目标环境?/)).toBeTruthy();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('单选点击选项即提交:answer 收到 requestId 与答案映射,卡片关闭', () => {
|
|
70
|
-
const bridge = installBridge();
|
|
71
|
-
render(<AskUserModalHost />);
|
|
72
|
-
act(() => bridge.emit(REQUEST));
|
|
73
|
-
|
|
74
|
-
fireEvent.click(screen.getByRole('radio', { name: /staging/ }));
|
|
75
|
-
|
|
76
|
-
expect(bridge.answer).toHaveBeenCalledWith({
|
|
77
|
-
requestId: 'req-1',
|
|
78
|
-
answers: { env: 'staging' },
|
|
79
|
-
});
|
|
80
|
-
expect(screen.queryByRole('dialog')).toBeNull();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('「交给我决定」应答空映射', () => {
|
|
84
|
-
const bridge = installBridge();
|
|
85
|
-
render(<AskUserModalHost />);
|
|
86
|
-
act(() => bridge.emit(REQUEST));
|
|
87
|
-
|
|
88
|
-
fireEvent.click(screen.getByRole('button', { name: /交给我决定/ }));
|
|
89
|
-
expect(bridge.answer).toHaveBeenCalledWith({ requestId: 'req-1', answers: {} });
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('FIFO:应答第一组后队列中的第二组顶上来', () => {
|
|
93
|
-
const bridge = installBridge();
|
|
94
|
-
render(<AskUserModalHost />);
|
|
95
|
-
act(() => {
|
|
96
|
-
bridge.emit(REQUEST);
|
|
97
|
-
bridge.emit({ ...REQUEST, requestId: 'req-2', intro: '第二组问题' });
|
|
98
|
-
});
|
|
99
|
-
expect(screen.getByText('部署前确认')).toBeTruthy();
|
|
100
|
-
|
|
101
|
-
fireEvent.click(screen.getByRole('button', { name: /交给我决定/ }));
|
|
102
|
-
expect(screen.getByText('第二组问题')).toBeTruthy();
|
|
103
|
-
expect(bridge.answer).toHaveBeenCalledTimes(1);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('bridge 没有 askUser 能力时不渲染也不报错', () => {
|
|
107
|
-
(window as { electron?: unknown }).electron = {};
|
|
108
|
-
render(<AskUserModalHost />);
|
|
109
|
-
expect(screen.queryByRole('dialog')).toBeNull();
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe('AskUserModalHost 分步菜单(一次一题)', () => {
|
|
114
|
-
it('第一题选择后进入第二题,文本输入后一次性提交全部答案', () => {
|
|
115
|
-
const bridge = installBridge();
|
|
116
|
-
render(<AskUserModalHost />);
|
|
117
|
-
act(() => bridge.emit(REQUEST_TWO));
|
|
118
|
-
|
|
119
|
-
expect(screen.getByText(/问题 1 \/ 2/)).toBeTruthy();
|
|
120
|
-
expect(screen.getByText(/目标环境?/)).toBeTruthy();
|
|
121
|
-
|
|
122
|
-
fireEvent.click(screen.getByRole('radio', { name: /staging/ }));
|
|
123
|
-
|
|
124
|
-
// 不提交,而是进入同一请求的下一题
|
|
125
|
-
expect(bridge.answer).not.toHaveBeenCalled();
|
|
126
|
-
expect(screen.getByText(/问题 2 \/ 2/)).toBeTruthy();
|
|
127
|
-
expect(screen.getByText('备注')).toBeTruthy();
|
|
128
|
-
|
|
129
|
-
const input = screen.getByPlaceholderText('输入你的回答...');
|
|
130
|
-
fireEvent.change(input, { target: { value: '周三窗口' } });
|
|
131
|
-
fireEvent.keyDown(input, { key: 'Enter' });
|
|
132
|
-
|
|
133
|
-
expect(bridge.answer).toHaveBeenCalledWith({
|
|
134
|
-
requestId: 'req-two',
|
|
135
|
-
answers: { env: 'staging', note: '周三窗口' },
|
|
136
|
-
});
|
|
137
|
-
expect(screen.queryByRole('dialog')).toBeNull();
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('「其他 / 自定义」输入直接作为该题答案,不会生成新的问题', () => {
|
|
141
|
-
const bridge = installBridge();
|
|
142
|
-
render(<AskUserModalHost />);
|
|
143
|
-
act(() => bridge.emit(REQUEST));
|
|
144
|
-
|
|
145
|
-
fireEvent.click(screen.getByRole('radio', { name: /其他/ }));
|
|
146
|
-
const input = screen.getByPlaceholderText('输入你的回答...');
|
|
147
|
-
fireEvent.change(input, { target: { value: '蓝绿发布' } });
|
|
148
|
-
fireEvent.keyDown(input, { key: 'Enter' });
|
|
149
|
-
|
|
150
|
-
expect(bridge.answer).toHaveBeenCalledWith({
|
|
151
|
-
requestId: 'req-1',
|
|
152
|
-
answers: { env: '蓝绿发布' },
|
|
153
|
-
});
|
|
154
|
-
expect(screen.queryByRole('dialog')).toBeNull();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('↑↓ 在菜单项间移动焦点,Enter 选中并提交', () => {
|
|
158
|
-
const bridge = installBridge();
|
|
159
|
-
render(<AskUserModalHost />);
|
|
160
|
-
act(() => bridge.emit(REQUEST));
|
|
161
|
-
|
|
162
|
-
const staging = screen.getByRole('radio', { name: /staging/ });
|
|
163
|
-
const prod = screen.getByRole('radio', { name: /prod/ });
|
|
164
|
-
const group = screen.getByRole('radiogroup');
|
|
165
|
-
|
|
166
|
-
act(() => staging.focus());
|
|
167
|
-
fireEvent.keyDown(group, { key: 'ArrowDown' });
|
|
168
|
-
expect(document.activeElement).toBe(prod);
|
|
169
|
-
|
|
170
|
-
fireEvent.keyDown(prod, { key: 'Enter' });
|
|
171
|
-
expect(bridge.answer).toHaveBeenCalledWith({
|
|
172
|
-
requestId: 'req-1',
|
|
173
|
-
answers: { env: 'prod' },
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
});
|