@steerable/agent-shell-web 0.6.15 → 0.6.16
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 +11 -9
- 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
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
const request = vi.fn();
|
|
5
|
-
|
|
6
|
-
vi.mock('@/lib/electron-bridge', () => ({
|
|
7
|
-
isElectron: () => true,
|
|
8
|
-
getElectronBridge: () => ({
|
|
9
|
-
localBackend: { request },
|
|
10
|
-
local: { saveTextFile: vi.fn() },
|
|
11
|
-
}),
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
const { InsightsConsentBanner, InsightsSettingsPanel } = await import('./InsightsSettingsPanel');
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
request.mockReset();
|
|
18
|
-
request.mockResolvedValue({
|
|
19
|
-
installId: '550e8400-e29b-41d4-a716-446655440000',
|
|
20
|
-
shareBehavior: false,
|
|
21
|
-
shareConversation: false,
|
|
22
|
-
shareProfile: false,
|
|
23
|
-
profile: { displayName: '', email: '', company: '', note: '' },
|
|
24
|
-
stats: { events: 2, turns: 1, profile: 0, pending: 3 },
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterEach(cleanup);
|
|
29
|
-
|
|
30
|
-
describe('InsightsConsentBanner', () => {
|
|
31
|
-
it('renders a simple upload-or-cancel first-run banner', async () => {
|
|
32
|
-
render(<InsightsConsentBanner />);
|
|
33
|
-
const banner = await screen.findByTestId('insights-consent-banner');
|
|
34
|
-
expect(banner.className).toContain('bg-agent-muted');
|
|
35
|
-
// 3.1 起品牌由产品注入;测试环境无注入 = shell 中性默认。
|
|
36
|
-
expect(screen.getByText('帮助改进Steerable Shell')).toBeTruthy();
|
|
37
|
-
expect(screen.getByText(/同意上传数据到服务器/)).toBeTruthy();
|
|
38
|
-
expect(screen.queryAllByRole('checkbox')).toHaveLength(0);
|
|
39
|
-
expect(screen.getByRole('button', { name: '保存' })).toBeTruthy();
|
|
40
|
-
expect(screen.getByRole('button', { name: '取消' })).toBeTruthy();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('save agrees to upload; cancel keeps data local', async () => {
|
|
44
|
-
render(<InsightsConsentBanner />);
|
|
45
|
-
await screen.findByTestId('insights-consent-banner');
|
|
46
|
-
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
47
|
-
await waitFor(() => {
|
|
48
|
-
expect(request).toHaveBeenCalledWith(
|
|
49
|
-
expect.objectContaining({
|
|
50
|
-
method: 'POST',
|
|
51
|
-
path: '/api/v2/local-settings/insights',
|
|
52
|
-
body: expect.objectContaining({
|
|
53
|
-
shareBehavior: true,
|
|
54
|
-
shareConversation: true,
|
|
55
|
-
shareProfile: true,
|
|
56
|
-
markPrompted: true,
|
|
57
|
-
}),
|
|
58
|
-
}),
|
|
59
|
-
);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('cancel declines upload and still dismisses the prompt', async () => {
|
|
64
|
-
render(<InsightsConsentBanner />);
|
|
65
|
-
await screen.findByTestId('insights-consent-banner');
|
|
66
|
-
fireEvent.click(screen.getByRole('button', { name: '取消' }));
|
|
67
|
-
await waitFor(() => {
|
|
68
|
-
expect(request).toHaveBeenCalledWith(
|
|
69
|
-
expect.objectContaining({
|
|
70
|
-
method: 'POST',
|
|
71
|
-
path: '/api/v2/local-settings/insights',
|
|
72
|
-
body: expect.objectContaining({
|
|
73
|
-
shareBehavior: false,
|
|
74
|
-
shareConversation: false,
|
|
75
|
-
shareProfile: false,
|
|
76
|
-
markPrompted: true,
|
|
77
|
-
}),
|
|
78
|
-
}),
|
|
79
|
-
);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('does not open after the prompt has been answered', async () => {
|
|
84
|
-
request.mockResolvedValueOnce({
|
|
85
|
-
installId: '550e8400-e29b-41d4-a716-446655440000',
|
|
86
|
-
shareBehavior: false,
|
|
87
|
-
shareConversation: false,
|
|
88
|
-
shareProfile: false,
|
|
89
|
-
promptedAt: '2026-09-12T00:00:00.000Z',
|
|
90
|
-
profile: { displayName: '', email: '', company: '', note: '' },
|
|
91
|
-
});
|
|
92
|
-
render(<InsightsConsentBanner />);
|
|
93
|
-
await waitFor(() => {
|
|
94
|
-
expect(request).toHaveBeenCalledWith(
|
|
95
|
-
expect.objectContaining({ method: 'GET', path: '/api/v2/local-settings/insights' }),
|
|
96
|
-
);
|
|
97
|
-
});
|
|
98
|
-
expect(screen.queryByTestId('insights-consent-banner')).toBeNull();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('InsightsSettingsPanel', () => {
|
|
103
|
-
it('shows separate toggles for behavior, conversation, and profile', async () => {
|
|
104
|
-
render(<InsightsSettingsPanel />);
|
|
105
|
-
expect(await screen.findByText(/帮助改进产品(行为 \/ 对话 \/ 用户信息 分开确认)/)).toBeTruthy();
|
|
106
|
-
expect(screen.getAllByText(/自动上传/).length).toBe(3);
|
|
107
|
-
expect(screen.getAllByRole('checkbox')).toHaveLength(3);
|
|
108
|
-
expect(screen.getByText(/导出文件发给开发者/)).toBeTruthy();
|
|
109
|
-
expect(screen.getByText(/现在上传本地记录/)).toBeTruthy();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('saves the three flags independently', async () => {
|
|
113
|
-
render(<InsightsSettingsPanel />);
|
|
114
|
-
await screen.findByText(/本机已记/);
|
|
115
|
-
const boxes = screen.getAllByRole('checkbox');
|
|
116
|
-
fireEvent.click(boxes[0]);
|
|
117
|
-
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
118
|
-
await waitFor(() => {
|
|
119
|
-
expect(request).toHaveBeenCalledWith(
|
|
120
|
-
expect.objectContaining({
|
|
121
|
-
method: 'POST',
|
|
122
|
-
path: '/api/v2/local-settings/insights',
|
|
123
|
-
body: expect.objectContaining({
|
|
124
|
-
shareBehavior: true,
|
|
125
|
-
shareConversation: false,
|
|
126
|
-
shareProfile: false,
|
|
127
|
-
}),
|
|
128
|
-
}),
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
});
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { ModelIdCombobox } from './ModelIdCombobox';
|
|
4
|
-
import type { ModelPickerRow } from './llm-vendors';
|
|
5
|
-
|
|
6
|
-
afterEach(cleanup);
|
|
7
|
-
|
|
8
|
-
const OPTIONS = ['deepseek-chat', 'deepseek-v4-flash', 'deepseek-v4-pro'];
|
|
9
|
-
|
|
10
|
-
const ROWS: ModelPickerRow[] = [
|
|
11
|
-
{
|
|
12
|
-
id: 'deepseek-v4-pro',
|
|
13
|
-
entry: {
|
|
14
|
-
id: 'deepseek-v4-pro',
|
|
15
|
-
name: 'DeepSeek V4 Pro',
|
|
16
|
-
window: 1_000_000,
|
|
17
|
-
modalities: ['text'],
|
|
18
|
-
reasoningLevels: ['high', 'max'],
|
|
19
|
-
pricing: null,
|
|
20
|
-
joinedFrom: 'deepseek/deepseek-v4-pro',
|
|
21
|
-
capabilities: 'known',
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
id: 'deepseek-flash',
|
|
26
|
-
entry: {
|
|
27
|
-
id: 'deepseek-flash',
|
|
28
|
-
name: null,
|
|
29
|
-
window: 131_072,
|
|
30
|
-
modalities: ['text'],
|
|
31
|
-
reasoningLevels: [],
|
|
32
|
-
pricing: null,
|
|
33
|
-
joinedFrom: null,
|
|
34
|
-
capabilities: 'unknown',
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
describe('ModelIdCombobox', () => {
|
|
40
|
-
it('focus 打开贴齐输入框的列表,点击写入模型 id', () => {
|
|
41
|
-
const onChange = vi.fn();
|
|
42
|
-
render(
|
|
43
|
-
<ModelIdCombobox
|
|
44
|
-
value="deepseek-chat"
|
|
45
|
-
options={OPTIONS}
|
|
46
|
-
placeholder="deepseek-chat"
|
|
47
|
-
onChange={onChange}
|
|
48
|
-
/>,
|
|
49
|
-
);
|
|
50
|
-
fireEvent.focus(screen.getByTestId('llm-model-input'));
|
|
51
|
-
const list = screen.getByTestId('llm-model-options');
|
|
52
|
-
expect(list.getAttribute('role')).toBe('listbox');
|
|
53
|
-
fireEvent.click(screen.getByRole('option', { name: 'deepseek-v4-flash' }));
|
|
54
|
-
expect(onChange).toHaveBeenCalledWith('deepseek-v4-flash');
|
|
55
|
-
expect(screen.queryByTestId('llm-model-options')).toBeNull();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('已选中某项时仍列出全部模型', () => {
|
|
59
|
-
render(
|
|
60
|
-
<ModelIdCombobox value="deepseek-v4-flash" options={OPTIONS} onChange={() => {}} />,
|
|
61
|
-
);
|
|
62
|
-
fireEvent.focus(screen.getByTestId('llm-model-input'));
|
|
63
|
-
expect(screen.getByRole('option', { name: 'deepseek-chat' })).toBeTruthy();
|
|
64
|
-
expect(screen.getByRole('option', { name: 'deepseek-v4-pro' })).toBeTruthy();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('手填部分文字时按子串过滤,并允许自定义 id', () => {
|
|
68
|
-
const onChange = vi.fn();
|
|
69
|
-
const { rerender } = render(
|
|
70
|
-
<ModelIdCombobox value="deepseek-chat" options={OPTIONS} onChange={onChange} />,
|
|
71
|
-
);
|
|
72
|
-
fireEvent.change(screen.getByTestId('llm-model-input'), { target: { value: 'flash' } });
|
|
73
|
-
expect(onChange).toHaveBeenCalledWith('flash');
|
|
74
|
-
rerender(<ModelIdCombobox value="flash" options={OPTIONS} onChange={onChange} />);
|
|
75
|
-
expect(screen.getByRole('option', { name: 'deepseek-v4-flash' })).toBeTruthy();
|
|
76
|
-
expect(screen.queryByRole('option', { name: 'deepseek-chat' })).toBeNull();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('shows verified capability chips and keeps option names as the model id', () => {
|
|
80
|
-
render(<ModelIdCombobox value="deepseek-v4-pro" options={ROWS} onChange={() => {}} />);
|
|
81
|
-
fireEvent.focus(screen.getByTestId('llm-model-input'));
|
|
82
|
-
expect(screen.getByRole('option', { name: 'deepseek-v4-pro' })).toBeTruthy();
|
|
83
|
-
expect(screen.getByText('思考')).toBeTruthy();
|
|
84
|
-
expect(screen.getByText('1M')).toBeTruthy();
|
|
85
|
-
expect(screen.getByText('未识别')).toBeTruthy();
|
|
86
|
-
expect(screen.queryByText('思考(high / max)')).toBeNull();
|
|
87
|
-
});
|
|
88
|
-
});
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { cleanup, render, screen } from '@testing-library/react';
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import type { SidecarSandboxPosture } from '@/lib/local-api';
|
|
4
|
-
|
|
5
|
-
// 沙箱诚实告知的渲染层锁定:收容失败必须说出「无法收容、已拒绝启动」,
|
|
6
|
-
// 而不是谎称进程仍在无隔离运行。posture/API 那一层由
|
|
7
|
-
// tests/sidecar/supervisor-sandbox.test.ts 与 sandbox-posture.e2e.test.ts 覆盖;
|
|
8
|
-
// 这里只管「用户看不看得见」。
|
|
9
|
-
|
|
10
|
-
const getSidecarSandboxPosture = vi.fn();
|
|
11
|
-
|
|
12
|
-
vi.mock('@/lib/electron-bridge', () => ({ isElectron: () => true }));
|
|
13
|
-
vi.mock('@/lib/local-api', () => ({
|
|
14
|
-
getSidecarSandboxPosture: () => getSidecarSandboxPosture(),
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
const { SecuritySettingsPanel } = await import('./SecuritySettingsPanel');
|
|
18
|
-
|
|
19
|
-
function posture(over: Partial<SidecarSandboxPosture>): SidecarSandboxPosture {
|
|
20
|
-
return { backend: 'none', enforcement: 'none', reason: 'platform_unsupported', ...over };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
beforeEach(() => {
|
|
24
|
-
getSidecarSandboxPosture.mockReset();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
afterEach(cleanup);
|
|
28
|
-
|
|
29
|
-
describe('SecuritySettingsPanel — layer-1 态势披露', () => {
|
|
30
|
-
it.each([
|
|
31
|
-
['platform_unsupported', '当前平台没有可用的进程沙箱后端'],
|
|
32
|
-
['seatbelt_missing', '未找到 /usr/bin/sandbox-exec'],
|
|
33
|
-
['profile_failed', '沙箱配置(Seatbelt profile)生成失败'],
|
|
34
|
-
['wrap_failed', 'Linux 进程沙箱'],
|
|
35
|
-
['helper_missing', '未找到 win-spawn-helper.exe'],
|
|
36
|
-
] as const)('收容失败 %s:告知已拒绝启动,不谎称仍在跑', async (reason, detail) => {
|
|
37
|
-
getSidecarSandboxPosture.mockResolvedValue({ posture: posture({ reason }) });
|
|
38
|
-
render(<SecuritySettingsPanel />);
|
|
39
|
-
|
|
40
|
-
const headline = await screen.findByText(/无法收容、已拒绝启动 —/);
|
|
41
|
-
expect(headline.textContent).toContain(detail);
|
|
42
|
-
expect(screen.getByText(/没有启动/)).toBeTruthy();
|
|
43
|
-
expect(screen.queryByText(/未沙箱 —/)).toBeNull();
|
|
44
|
-
expect(screen.getByText(/STEERABLE_SIDECAR_SANDBOX=0/)).toBeTruthy();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('手动关闭中性展示,不冒充告警也不谎称已隔离', async () => {
|
|
48
|
-
getSidecarSandboxPosture.mockResolvedValue({
|
|
49
|
-
posture: posture({ reason: 'disabled_by_env' }),
|
|
50
|
-
});
|
|
51
|
-
render(<SecuritySettingsPanel />);
|
|
52
|
-
|
|
53
|
-
expect((await screen.findByText(/已手动关闭 —/)).textContent).toContain(
|
|
54
|
-
'STEERABLE_SIDECAR_SANDBOX=0',
|
|
55
|
-
);
|
|
56
|
-
expect(screen.queryByText(/无法收容、已拒绝启动 —/)).toBeNull();
|
|
57
|
-
expect(screen.getByText(/无操作系统隔离运行/)).toBeTruthy();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('Seatbelt 生效时记为「部分」,不谎称完全强制', async () => {
|
|
61
|
-
getSidecarSandboxPosture.mockResolvedValue({
|
|
62
|
-
posture: posture({ backend: 'seatbelt', enforcement: 'partial', reason: 'active' }),
|
|
63
|
-
});
|
|
64
|
-
render(<SecuritySettingsPanel />);
|
|
65
|
-
|
|
66
|
-
expect(await screen.findByText(/Seatbelt · 部分强制/)).toBeTruthy();
|
|
67
|
-
expect(screen.getByText(/不识别主机名/)).toBeTruthy();
|
|
68
|
-
expect(screen.queryByText(/无法收容、已拒绝启动 —/)).toBeNull();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('bwrap 生效时记为部分强制', async () => {
|
|
72
|
-
getSidecarSandboxPosture.mockResolvedValue({
|
|
73
|
-
posture: posture({ backend: 'bwrap', enforcement: 'partial', reason: 'active' }),
|
|
74
|
-
});
|
|
75
|
-
render(<SecuritySettingsPanel />);
|
|
76
|
-
expect(await screen.findByText(/bwrap · 部分强制/)).toBeTruthy();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('sidecar 未就绪时不冒充任何一种姿态', async () => {
|
|
80
|
-
getSidecarSandboxPosture.mockRejectedValue(new Error('503'));
|
|
81
|
-
render(<SecuritySettingsPanel />);
|
|
82
|
-
|
|
83
|
-
expect(await screen.findByText(/sidecar 未就绪/)).toBeTruthy();
|
|
84
|
-
expect(screen.queryByText(/无法收容、已拒绝启动 —/)).toBeNull();
|
|
85
|
-
expect(screen.queryByText(/部分强制/)).toBeNull();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('出网退回端口级时披露原因(此前只有主进程日志可见)', async () => {
|
|
89
|
-
getSidecarSandboxPosture.mockResolvedValue({
|
|
90
|
-
posture: posture({ backend: 'seatbelt', enforcement: 'partial', reason: 'active' }),
|
|
91
|
-
egress: {
|
|
92
|
-
mode: 'port-only-fallback',
|
|
93
|
-
reason: '检测到系统/环境代理(127.0.0.1:7890),按主机管控已退回端口级',
|
|
94
|
-
},
|
|
95
|
-
});
|
|
96
|
-
render(<SecuritySettingsPanel />);
|
|
97
|
-
|
|
98
|
-
expect(await screen.findByText(/已退回端口级/)).toBeTruthy();
|
|
99
|
-
expect(screen.getByText(/127\.0\.0\.1:7890/)).toBeTruthy();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('按主机代理生效时说明逐项放行语义', async () => {
|
|
103
|
-
getSidecarSandboxPosture.mockResolvedValue({
|
|
104
|
-
posture: posture({ backend: 'seatbelt', enforcement: 'partial', reason: 'active' }),
|
|
105
|
-
egress: { mode: 'per-host-proxy', reason: null },
|
|
106
|
-
});
|
|
107
|
-
render(<SecuritySettingsPanel />);
|
|
108
|
-
|
|
109
|
-
expect(await screen.findByText(/按主机白名单代理生效中/)).toBeTruthy();
|
|
110
|
-
expect(screen.getByText(/逐项放行/)).toBeTruthy();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('无 egress 字段(旧后端)时不渲染出网行', async () => {
|
|
114
|
-
getSidecarSandboxPosture.mockResolvedValue({
|
|
115
|
-
posture: posture({ backend: 'seatbelt', enforcement: 'partial', reason: 'active' }),
|
|
116
|
-
});
|
|
117
|
-
render(<SecuritySettingsPanel />);
|
|
118
|
-
|
|
119
|
-
await screen.findByText(/Seatbelt · 部分强制/);
|
|
120
|
-
expect(screen.queryByText(/出网管控/)).toBeNull();
|
|
121
|
-
});
|
|
122
|
-
});
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { cleanup, render, screen } from '@testing-library/react';
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
const request = vi.fn();
|
|
5
|
-
|
|
6
|
-
vi.mock('@/lib/electron-bridge', () => ({
|
|
7
|
-
isElectron: () => true,
|
|
8
|
-
getElectronBridge: () => ({
|
|
9
|
-
localBackend: { request },
|
|
10
|
-
local: {},
|
|
11
|
-
}),
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
const { SkillsSettingsPanel } = await import('./SkillsSettingsPanel');
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
request.mockReset();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(cleanup);
|
|
21
|
-
|
|
22
|
-
describe('SkillsSettingsPanel', () => {
|
|
23
|
-
it('shows a 工作区 badge and hides uninstall for workspace skills', async () => {
|
|
24
|
-
request.mockResolvedValue({
|
|
25
|
-
skills: [
|
|
26
|
-
{
|
|
27
|
-
name: 'compute-recursion',
|
|
28
|
-
displayName: '递归函数计算',
|
|
29
|
-
description: '算递推第 n 项',
|
|
30
|
-
origin: 'workspace',
|
|
31
|
-
isBuiltin: false,
|
|
32
|
-
layer: 'catalog',
|
|
33
|
-
modelInvocable: false,
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
});
|
|
37
|
-
render(<SkillsSettingsPanel />);
|
|
38
|
-
const row = await screen.findByTestId('skill-row-compute-recursion');
|
|
39
|
-
expect(row.textContent).toContain('工作区');
|
|
40
|
-
expect(row.textContent).not.toContain('内置');
|
|
41
|
-
expect(screen.queryByTestId('skill-uninstall-compute-recursion')).toBeNull();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('keeps uninstall on imported user skills', async () => {
|
|
45
|
-
request.mockResolvedValue({
|
|
46
|
-
skills: [
|
|
47
|
-
{
|
|
48
|
-
name: 'frontend-design',
|
|
49
|
-
displayName: '',
|
|
50
|
-
description: 'UI skill',
|
|
51
|
-
origin: 'user',
|
|
52
|
-
isBuiltin: false,
|
|
53
|
-
layer: 'catalog',
|
|
54
|
-
modelInvocable: true,
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
});
|
|
58
|
-
render(<SkillsSettingsPanel />);
|
|
59
|
-
await screen.findByTestId('skill-row-frontend-design');
|
|
60
|
-
expect(screen.getByTestId('skill-uninstall-frontend-design')).toBeTruthy();
|
|
61
|
-
});
|
|
62
|
-
});
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
|
|
4
|
-
const request = vi.fn();
|
|
5
|
-
|
|
6
|
-
vi.mock('@/lib/electron-bridge', () => ({
|
|
7
|
-
isElectron: () => true,
|
|
8
|
-
getElectronBridge: () => ({
|
|
9
|
-
localBackend: { request },
|
|
10
|
-
}),
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
const { WebSearchSettingsPanel } = await import('./WebSearchSettingsPanel');
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
request.mockReset();
|
|
17
|
-
request.mockResolvedValue({ provider: 'tavily', apiKey: '' });
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
afterEach(cleanup);
|
|
21
|
-
|
|
22
|
-
describe('WebSearchSettingsPanel', () => {
|
|
23
|
-
it('loads Tavily by default and hides the key field on the free backend', async () => {
|
|
24
|
-
render(<WebSearchSettingsPanel />);
|
|
25
|
-
await screen.findByTestId('web-search-provider-ddg');
|
|
26
|
-
expect(screen.getByText('Tavily API Key')).toBeTruthy();
|
|
27
|
-
|
|
28
|
-
fireEvent.click(screen.getByTestId('web-search-provider-ddg'));
|
|
29
|
-
expect(screen.queryByText('Tavily API Key')).toBeNull();
|
|
30
|
-
expect(screen.getByText(/DuckDuckGo 公开搜索页/)).toBeTruthy();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('POSTs provider=ddg and shows the restart copy', async () => {
|
|
34
|
-
request
|
|
35
|
-
.mockResolvedValueOnce({ provider: 'tavily' })
|
|
36
|
-
.mockResolvedValueOnce({ provider: 'ddg' });
|
|
37
|
-
|
|
38
|
-
render(<WebSearchSettingsPanel />);
|
|
39
|
-
await screen.findByTestId('web-search-provider-ddg');
|
|
40
|
-
fireEvent.click(screen.getByTestId('web-search-provider-ddg'));
|
|
41
|
-
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
42
|
-
|
|
43
|
-
await waitFor(() => {
|
|
44
|
-
expect(request).toHaveBeenCalledWith(
|
|
45
|
-
expect.objectContaining({
|
|
46
|
-
method: 'POST',
|
|
47
|
-
path: '/api/v2/local-settings/web-search',
|
|
48
|
-
body: { provider: 'ddg', apiKey: '' },
|
|
49
|
-
}),
|
|
50
|
-
);
|
|
51
|
-
});
|
|
52
|
-
expect(await screen.findByText(/重启应用后 sidecar 会注册免费搜索/)).toBeTruthy();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('keeps 免费 selected when the save echo omits provider', async () => {
|
|
56
|
-
request.mockResolvedValueOnce({ provider: 'tavily' }).mockResolvedValueOnce({});
|
|
57
|
-
|
|
58
|
-
render(<WebSearchSettingsPanel />);
|
|
59
|
-
await screen.findByTestId('web-search-provider-ddg');
|
|
60
|
-
fireEvent.click(screen.getByTestId('web-search-provider-ddg'));
|
|
61
|
-
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
62
|
-
|
|
63
|
-
expect(await screen.findByText(/重启应用后 sidecar 会注册免费搜索/)).toBeTruthy();
|
|
64
|
-
expect(screen.queryByText('Tavily API Key')).toBeNull();
|
|
65
|
-
expect(screen.getByTestId('web-search-provider-ddg').className).toContain('border-agent-foreground/40');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('POSTs a Tavily key and keeps the key field', async () => {
|
|
69
|
-
request
|
|
70
|
-
.mockResolvedValueOnce({ provider: 'tavily' })
|
|
71
|
-
.mockResolvedValueOnce({ provider: 'tavily', apiKey: 'tvly-k' });
|
|
72
|
-
|
|
73
|
-
render(<WebSearchSettingsPanel />);
|
|
74
|
-
const keyInput = await screen.findByPlaceholderText('tvly-...(留空 = 不注册 Tavily)');
|
|
75
|
-
fireEvent.change(keyInput, { target: { value: ' tvly-k ' } });
|
|
76
|
-
fireEvent.click(screen.getByRole('button', { name: '保存' }));
|
|
77
|
-
|
|
78
|
-
await waitFor(() => {
|
|
79
|
-
expect(request).toHaveBeenCalledWith(
|
|
80
|
-
expect.objectContaining({
|
|
81
|
-
method: 'POST',
|
|
82
|
-
path: '/api/v2/local-settings/web-search',
|
|
83
|
-
body: { provider: 'tavily', apiKey: 'tvly-k' },
|
|
84
|
-
}),
|
|
85
|
-
);
|
|
86
|
-
});
|
|
87
|
-
expect(await screen.findByText(/重启应用后 sidecar 会注册 web_search/)).toBeTruthy();
|
|
88
|
-
expect(screen.getByText('Tavily API Key')).toBeTruthy();
|
|
89
|
-
});
|
|
90
|
-
});
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
COMPAT_AUTO,
|
|
4
|
-
formStateFromOverrides,
|
|
5
|
-
overridesFromFormState,
|
|
6
|
-
} from './compat-flags-model';
|
|
7
|
-
import type { CompatFlagDescriptor } from '@/lib/local-api';
|
|
8
|
-
|
|
9
|
-
// 与框架 compat.py `_FLAG_WIRE_SPEC` 同形的描述符夹具。
|
|
10
|
-
const FLAGS: CompatFlagDescriptor[] = [
|
|
11
|
-
{ key: 'supportsUsageInStreaming', field: 'supports_usage_in_streaming', kind: 'bool', default: true, description: '' },
|
|
12
|
-
{ key: 'maxTokensField', field: 'max_tokens_field', kind: 'enum:max_tokens,max_completion_tokens', default: 'max_tokens', description: '' },
|
|
13
|
-
{ key: 'supportsTemperature', field: 'supports_temperature', kind: 'bool', default: true, description: '' },
|
|
14
|
-
{ key: 'reasoningDeltaFields', field: 'reasoning_delta_fields', kind: 'string-list', default: ['reasoning_content', 'reasoning'], description: '' },
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
describe('compat-flags-model (W1.3.2)', () => {
|
|
18
|
-
it('unset overrides render as auto for every flag', () => {
|
|
19
|
-
expect(formStateFromOverrides(undefined, FLAGS)).toEqual({
|
|
20
|
-
supportsUsageInStreaming: COMPAT_AUTO,
|
|
21
|
-
maxTokensField: COMPAT_AUTO,
|
|
22
|
-
supportsTemperature: COMPAT_AUTO,
|
|
23
|
-
reasoningDeltaFields: COMPAT_AUTO,
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('persisted overrides map to their form values', () => {
|
|
28
|
-
const state = formStateFromOverrides(
|
|
29
|
-
{ supportsTemperature: false, reasoningDeltaFields: ['reasoning_content'] },
|
|
30
|
-
FLAGS,
|
|
31
|
-
);
|
|
32
|
-
expect(state.supportsTemperature).toBe('false');
|
|
33
|
-
expect(state.reasoningDeltaFields).toBe('reasoning_content');
|
|
34
|
-
expect(state.maxTokensField).toBe(COMPAT_AUTO);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('all-auto form state produces no overrides (auto-detect fallback)', () => {
|
|
38
|
-
const state = formStateFromOverrides(undefined, FLAGS);
|
|
39
|
-
expect(overridesFromFormState(state, FLAGS)).toBeUndefined();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('form state builds a typed overrides payload', () => {
|
|
43
|
-
const overrides = overridesFromFormState(
|
|
44
|
-
{
|
|
45
|
-
supportsTemperature: 'false',
|
|
46
|
-
maxTokensField: 'max_completion_tokens',
|
|
47
|
-
reasoningDeltaFields: 'reasoning_content, reasoning',
|
|
48
|
-
supportsUsageInStreaming: COMPAT_AUTO,
|
|
49
|
-
},
|
|
50
|
-
FLAGS,
|
|
51
|
-
);
|
|
52
|
-
expect(overrides).toEqual({
|
|
53
|
-
supportsTemperature: false,
|
|
54
|
-
maxTokensField: 'max_completion_tokens',
|
|
55
|
-
reasoningDeltaFields: ['reasoning_content', 'reasoning'],
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('invalid enum values and empty lists are dropped', () => {
|
|
60
|
-
const overrides = overridesFromFormState(
|
|
61
|
-
{ maxTokensField: 'bogus_field', reasoningDeltaFields: ' ' },
|
|
62
|
-
FLAGS,
|
|
63
|
-
);
|
|
64
|
-
expect(overrides).toBeUndefined();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('round-trips: overrides → form → overrides', () => {
|
|
68
|
-
const original = { supportsTemperature: false as const, reasoningDeltaFields: ['reasoning'] };
|
|
69
|
-
expect(overridesFromFormState(formStateFromOverrides(original, FLAGS), FLAGS)).toEqual(original);
|
|
70
|
-
});
|
|
71
|
-
});
|