@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.
Files changed (37) hide show
  1. package/package.json +19 -12
  2. package/vite.base.d.ts +7 -0
  3. package/vite.base.js +83 -0
  4. package/src/brand.test.ts +0 -22
  5. package/src/components/AgentSidebar.test.tsx +0 -127
  6. package/src/components/ChatHeader.test.tsx +0 -230
  7. package/src/components/LocalLlmSettingsModal.test.tsx +0 -372
  8. package/src/components/chat/ApprovalModal.test.tsx +0 -83
  9. package/src/components/chat/AskUserModalHost.test.tsx +0 -176
  10. package/src/components/chat/AssistantMessage.test.tsx +0 -68
  11. package/src/components/chat/ChatInput.test.tsx +0 -609
  12. package/src/components/chat/ExecPolicyPicker.test.tsx +0 -26
  13. package/src/components/chat/Markdown.test.tsx +0 -170
  14. package/src/components/chat/ModelPicker.test.tsx +0 -149
  15. package/src/components/chat/SessionTreeModal.test.tsx +0 -161
  16. package/src/components/chat/TaskOutcomeCards.test.tsx +0 -97
  17. package/src/components/chat/TaskPanelModal.test.tsx +0 -198
  18. package/src/components/chat/TaskProcessPanel.test.tsx +0 -111
  19. package/src/components/chat/TurnProcessGroup.test.tsx +0 -161
  20. package/src/components/chat/elapsed.test.ts +0 -42
  21. package/src/components/chat/executed-actions-model.test.ts +0 -134
  22. package/src/components/chat/orchestration-children-model.test.ts +0 -115
  23. package/src/components/chat/turn-timeline.test.ts +0 -97
  24. package/src/components/chat/useChatTasks.test.ts +0 -136
  25. package/src/components/settings/AgentsSettingsPanel.test.tsx +0 -229
  26. package/src/components/settings/InsightsSettingsPanel.test.tsx +0 -132
  27. package/src/components/settings/ModelIdCombobox.test.tsx +0 -88
  28. package/src/components/settings/SecuritySettingsPanel.test.tsx +0 -122
  29. package/src/components/settings/SkillsSettingsPanel.test.tsx +0 -62
  30. package/src/components/settings/WebSearchSettingsPanel.test.tsx +0 -90
  31. package/src/components/settings/compat-flags-model.test.ts +0 -71
  32. package/src/components/settings/llm-vendors.test.ts +0 -171
  33. package/src/components/settings/model-capabilities.test.ts +0 -88
  34. package/src/components/settings/preset-choice-model.test.ts +0 -96
  35. package/src/lib/pending-first-message.test.ts +0 -17
  36. package/src/pages/SettingsPage.test.tsx +0 -102
  37. package/vite.base.ts +0 -94
@@ -1,115 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- childrenToCardModel,
4
- foldOrchestrationChildEvents,
5
- type ChildInfo,
6
- } from './orchestration-children-model';
7
-
8
- // OrchestrationChildrenCard 的纯映射层测试:agent.child 生命周期事件累积出的
9
- // ChildInfo 列表 → OrchestrationPlanCard 的 payload + taskStatuses。卡片本身
10
- // 的点击展开/收起交互由框架 agent-ui 的 cards.test.tsx 覆盖;桌面端到端
11
- // (真实 Electron + CDP 模拟用户操作)由 scripts/desktop-canary.mjs 覆盖。
12
-
13
- describe('childrenToCardModel / P3.1 编排卡片映射', () => {
14
- it('空列表产出空任务面', () => {
15
- const { payload, taskStatuses } = childrenToCardModel([]);
16
- expect(payload.tasks).toEqual([]);
17
- expect(taskStatuses).toEqual({});
18
- });
19
-
20
- it('spawn 的子代理进 tasks,id/agentId 用 lineage childId', () => {
21
- const children: ChildInfo[] = [
22
- { childId: '0.1', task: '扫日志', depth: 1, status: 'running' },
23
- { childId: '0.2', task: '读配置', depth: 1, status: 'running' },
24
- ];
25
- const { payload } = childrenToCardModel(children);
26
- expect(payload.mode).toBe('parallel');
27
- expect(payload.tasks).toEqual([
28
- { id: '0.1', agentId: '0.1', prompt: '扫日志' },
29
- { id: '0.2', agentId: '0.2', prompt: '读配置' },
30
- ]);
31
- });
32
-
33
- it('生命周期状态映射到卡片状态点', () => {
34
- const children: ChildInfo[] = [
35
- { childId: '0.1', status: 'running' },
36
- { childId: '0.2', status: 'completed' },
37
- { childId: '0.3', status: 'failed' },
38
- { childId: '0.4', status: 'cancelled' },
39
- { childId: '0.5', status: 'interrupted' },
40
- ];
41
- const { taskStatuses } = childrenToCardModel(children);
42
- expect(taskStatuses).toEqual({
43
- '0.1': 'running',
44
- '0.2': 'ok',
45
- '0.3': 'failed',
46
- '0.4': 'skipped',
47
- // interrupted 是可恢复的暂停,不是终态
48
- '0.5': 'pending',
49
- });
50
- });
51
-
52
- it('未知状态退化为 pending;缺 task 时 prompt 为空串', () => {
53
- const { payload, taskStatuses } = childrenToCardModel([
54
- { childId: '0.1', status: 'mystery' },
55
- ]);
56
- expect(taskStatuses['0.1']).toBe('pending');
57
- expect(payload.tasks[0].prompt).toBe('');
58
- });
59
-
60
- it('delegate 子代理的 agentId 显示 profile 名(delegate-on-pool)', () => {
61
- // delegate_subagent 的 child_spawned 事件带 profile;卡片把 profile
62
- // 作为执行者名显示,无 profile 的编排子代理仍退回 lineage childId。
63
- const children: ChildInfo[] = [
64
- { childId: '0.1', task: '扫日志', profile: 'researcher', status: 'running' },
65
- { childId: '0.2', task: '读配置', profile: 'general-purpose', status: 'completed' },
66
- { childId: '0.3', task: '六件套子代理', status: 'running' },
67
- ];
68
- const { payload } = childrenToCardModel(children);
69
- expect(payload.tasks).toEqual([
70
- { id: '0.1', agentId: 'researcher', prompt: '扫日志' },
71
- { id: '0.2', agentId: 'general-purpose', prompt: '读配置' },
72
- { id: '0.3', agentId: '0.3', prompt: '六件套子代理' },
73
- ]);
74
- });
75
- });
76
-
77
- describe('foldOrchestrationChildEvents / 切回重建子代理列表', () => {
78
- it('空事件流产出空列表', () => {
79
- expect(foldOrchestrationChildEvents([])).toEqual([]);
80
- });
81
-
82
- it('spawn → completed → resumed 折叠为终态/恢复态', () => {
83
- const list = foldOrchestrationChildEvents([
84
- { kind: 'child_spawned', childId: '0.1', task: '扫日志', depth: 1 },
85
- { kind: 'child_completed', childId: '0.1', status: 'completed' },
86
- { kind: 'child_resumed', childId: '0.1', status: 'running' },
87
- ]);
88
- expect(list).toEqual([{ childId: '0.1', task: '扫日志', depth: 1, status: 'running' }]);
89
- });
90
-
91
- it('未 spawn 就出现的终端事件被跳过', () => {
92
- expect(
93
- foldOrchestrationChildEvents([{ kind: 'child_failed', childId: 'ghost' }]),
94
- ).toEqual([]);
95
- });
96
-
97
- it('重复 spawn 幂等,不会产生重复条目', () => {
98
- const list = foldOrchestrationChildEvents([
99
- { kind: 'child_spawned', childId: '0.1', task: 'A' },
100
- { kind: 'child_spawned', childId: '0.1', task: 'A' },
101
- ]);
102
- expect(list).toHaveLength(1);
103
- });
104
-
105
- it('非法条目(缺 kind/childId)被忽略', () => {
106
- // 刻意传入非结构化条目验证鲁棒性——运行时可从 JSON 收到任意形状。
107
- const list = foldOrchestrationChildEvents([
108
- { childId: '0.1' },
109
- { kind: 'child_spawned' },
110
- null,
111
- 42,
112
- ] as unknown as Parameters<typeof foldOrchestrationChildEvents>[0]);
113
- expect(list).toEqual([]);
114
- });
115
- });
@@ -1,97 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import type { ExecutedAction } from './ExecutedActionsCard';
3
- import {
4
- appendDelta,
5
- countProcessTools,
6
- fallbackTimeline,
7
- parseTurnBlocks,
8
- processHasReasoning,
9
- splitTurnProcess,
10
- syncTools,
11
- } from './turn-timeline';
12
-
13
- const tool = (name: string, result?: unknown): ExecutedAction => ({
14
- tool: name,
15
- arguments: { path: `/${name}` },
16
- ...(result === undefined ? {} : { result }),
17
- });
18
-
19
- describe('turn-timeline', () => {
20
- it('merges consecutive deltas of the same kind and splits on kind change', () => {
21
- let blocks = appendDelta([], 'reasoning', '想');
22
- blocks = appendDelta(blocks, 'reasoning', '一下');
23
- blocks = appendDelta(blocks, 'text', '先读文件');
24
- blocks = appendDelta(blocks, 'text', '。');
25
- expect(blocks).toEqual([
26
- { type: 'reasoning', content: '想一下' },
27
- { type: 'text', content: '先读文件。' },
28
- ]);
29
- });
30
-
31
- it('places tools after the text that preceded them, then more text', () => {
32
- let blocks = appendDelta([], 'text', '开始');
33
- blocks = syncTools(blocks, [tool('read')]);
34
- blocks = appendDelta(blocks, 'text', '接着写');
35
- blocks = syncTools(blocks, [tool('read'), tool('write')]);
36
- expect(blocks.map((b) => b.type)).toEqual(['text', 'tools', 'text', 'tools']);
37
- expect(blocks[1]).toEqual({ type: 'tools', actions: [tool('read')] });
38
- expect(blocks[3]).toEqual({ type: 'tools', actions: [tool('write')] });
39
- });
40
-
41
- it('groups consecutive tools and updates in-place results', () => {
42
- const running = tool('read');
43
- const done = tool('read', { success: true });
44
- let blocks = syncTools([], [running]);
45
- blocks = syncTools(blocks, [done]);
46
- blocks = syncTools(blocks, [done, tool('write')]);
47
- expect(blocks).toHaveLength(1);
48
- expect(blocks[0]).toEqual({ type: 'tools', actions: [done, tool('write')] });
49
- });
50
-
51
- it('ignores empty deltas', () => {
52
- expect(appendDelta([], 'text', '')).toEqual([]);
53
- });
54
-
55
- it('falls back to tools-then-text for history without a timeline', () => {
56
- expect(fallbackTimeline('答', [tool('read', { success: true })])).toEqual([
57
- { type: 'tools', actions: [tool('read', { success: true })] },
58
- { type: 'text', content: '答' },
59
- ]);
60
- expect(fallbackTimeline('', undefined)).toEqual([]);
61
- });
62
-
63
- it('splits trailing summary text from the think→act process', () => {
64
- const blocks = [
65
- { type: 'reasoning' as const, content: '想' },
66
- { type: 'tools' as const, actions: [tool('read')] },
67
- { type: 'text' as const, content: '中间说明' },
68
- { type: 'tools' as const, actions: [tool('write')] },
69
- { type: 'text' as const, content: '总结' },
70
- ];
71
- expect(splitTurnProcess(blocks)).toEqual({
72
- process: blocks.slice(0, 4),
73
- answer: [blocks[4]],
74
- });
75
- expect(splitTurnProcess([{ type: 'text', content: '只回答' }])).toEqual({
76
- process: [],
77
- answer: [{ type: 'text', content: '只回答' }],
78
- });
79
- expect(splitTurnProcess([{ type: 'tools', actions: [tool('read')] }])).toEqual({
80
- process: [{ type: 'tools', actions: [tool('read')] }],
81
- answer: [],
82
- });
83
- expect(countProcessTools(blocks.slice(0, 4))).toBe(2);
84
- expect(processHasReasoning(blocks.slice(0, 4))).toBe(true);
85
- });
86
-
87
- it('parses persisted timeline JSON and rejects unknown kinds', () => {
88
- const parsed = parseTurnBlocks([
89
- { type: 'reasoning', content: 'hmm' },
90
- { type: 'tools', actions: [tool('read')] },
91
- { type: 'text', content: 'ok' },
92
- ]);
93
- expect(parsed).toHaveLength(3);
94
- expect(parseTurnBlocks([{ type: 'image' }])).toBeNull();
95
- expect(parseTurnBlocks([])).toBeNull();
96
- });
97
- });
@@ -1,136 +0,0 @@
1
- /**
2
- * useChatTasks 契约:
3
- * - 拉 GET /chats/:id/tasks,`task-updated` 推送后重拉;
4
- * - `finished` 只收本次挂载期间跑到终态的任务——首屏已经终态的历史任务
5
- * 不算「刚跑完」,否则每次打开对话都会重弹一遍;
6
- * - 两次刷新之间从建到完的任务算刚跑完(用户同样没看见它运行过);
7
- * - `dismissFinished` 只隐藏通知,不影响 `tasks` 与 `summary`。
8
- */
9
- import { act, renderHook, waitFor } from '@testing-library/react';
10
- import { afterEach, describe, expect, it, vi } from 'vitest';
11
- import type { LocalTask } from '@/lib/local-api';
12
- import { useChatTasks } from './useChatTasks';
13
-
14
- afterEach(() => {
15
- delete (window as { electron?: unknown }).electron;
16
- });
17
-
18
- function makeTask(overrides: Partial<LocalTask> = {}): LocalTask {
19
- return {
20
- id: 'task-1',
21
- chatId: 'chat_1',
22
- task: '跑测试',
23
- status: 'running',
24
- answer: null,
25
- error: null,
26
- worktreePath: null,
27
- worktreeBranch: null,
28
- worktreeState: null,
29
- createdAt: '2026-09-13T01:00:00.000Z',
30
- updatedAt: '2026-09-13T01:01:00.000Z',
31
- ...overrides,
32
- };
33
- }
34
-
35
- function installBridge(initial: LocalTask[]) {
36
- let current = initial;
37
- const listeners: Array<(p: { chatId: string; taskId: string; status: string }) => void> = [];
38
- (window as { electron?: unknown }).electron = {
39
- localBackend: {
40
- request: vi.fn((input: { method: string; path: string }) => {
41
- if (input.method === 'GET' && input.path.endsWith('/tasks')) {
42
- return Promise.resolve({ tasks: current });
43
- }
44
- return Promise.reject(new Error(`unexpected request: ${input.path}`));
45
- }),
46
- },
47
- onTaskUpdated: (cb: (p: { chatId: string; taskId: string; status: string }) => void) => {
48
- listeners.push(cb);
49
- return () => listeners.splice(listeners.indexOf(cb), 1);
50
- },
51
- };
52
- return {
53
- async push(next: LocalTask[]) {
54
- current = next;
55
- await act(async () => {
56
- for (const cb of listeners) cb({ chatId: 'chat_1', taskId: 'task-1', status: 'x' });
57
- });
58
- },
59
- };
60
- }
61
-
62
- describe('useChatTasks', () => {
63
- it('首屏已终态的历史任务不算刚跑完', async () => {
64
- installBridge([makeTask({ id: 'a', status: 'completed' })]);
65
- const { result } = renderHook(() => useChatTasks('chat_1'));
66
-
67
- await waitFor(() => expect(result.current.tasks).toHaveLength(1));
68
- expect(result.current.finished).toEqual([]);
69
- expect(result.current.summary.total).toBe(1);
70
- });
71
-
72
- it('运行中的任务跑到终态时进 finished', async () => {
73
- const bridge = installBridge([makeTask({ id: 'a', status: 'running' })]);
74
- const { result } = renderHook(() => useChatTasks('chat_1'));
75
-
76
- await waitFor(() => expect(result.current.tasks).toHaveLength(1));
77
- expect(result.current.finished).toEqual([]);
78
-
79
- const done = makeTask({ id: 'a', status: 'completed', answer: '42 行' });
80
- await bridge.push([done]);
81
-
82
- await waitFor(() => expect(result.current.finished).toEqual([done]));
83
- });
84
-
85
- it('两次刷新之间从建到完的任务也算刚跑完', async () => {
86
- const bridge = installBridge([]);
87
- const { result } = renderHook(() => useChatTasks('chat_1'));
88
-
89
- await waitFor(() => expect(result.current.tasks).toEqual([]));
90
-
91
- const flash = makeTask({ id: 'a', status: 'failed', error: '炸了' });
92
- await bridge.push([flash]);
93
-
94
- await waitFor(() => expect(result.current.finished).toEqual([flash]));
95
- });
96
-
97
- it('同一个终态不会重复进 finished', async () => {
98
- const bridge = installBridge([makeTask({ id: 'a', status: 'running' })]);
99
- const { result } = renderHook(() => useChatTasks('chat_1'));
100
-
101
- await waitFor(() => expect(result.current.tasks).toHaveLength(1));
102
-
103
- const done = makeTask({ id: 'a', status: 'completed' });
104
- await bridge.push([done]);
105
- await waitFor(() => expect(result.current.finished).toHaveLength(1));
106
-
107
- await bridge.push([done]);
108
- expect(result.current.finished).toHaveLength(1);
109
- });
110
-
111
- it('dismissFinished 只隐藏通知,任务本身还在', async () => {
112
- const bridge = installBridge([makeTask({ id: 'a', status: 'running' })]);
113
- const { result } = renderHook(() => useChatTasks('chat_1'));
114
-
115
- await waitFor(() => expect(result.current.tasks).toHaveLength(1));
116
- await bridge.push([makeTask({ id: 'a', status: 'failed', error: '炸了' })]);
117
- await waitFor(() => expect(result.current.finished).toHaveLength(1));
118
-
119
- act(() => result.current.dismissFinished('a'));
120
-
121
- expect(result.current.finished).toEqual([]);
122
- expect(result.current.tasks).toHaveLength(1);
123
- expect(result.current.summary.failed).toBe(1);
124
- });
125
-
126
- it('任务表读不到时按「没有任务」处理', async () => {
127
- (window as { electron?: unknown }).electron = {
128
- localBackend: { request: vi.fn(() => Promise.reject(new Error('sidecar 离线'))) },
129
- onTaskUpdated: () => () => {},
130
- };
131
- const { result } = renderHook(() => useChatTasks('chat_1'));
132
-
133
- await waitFor(() => expect(result.current.summary.total).toBe(0));
134
- expect(result.current.finished).toEqual([]);
135
- });
136
- });
@@ -1,229 +0,0 @@
1
- import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
2
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
- import type { LocalChatAgent } from '@/lib/local-api';
4
-
5
- const listChatAgents = vi.fn();
6
- const createChatAgent = vi.fn();
7
- const updateChatAgent = vi.fn();
8
- const archiveChatAgent = vi.fn();
9
- const listChatAgentSkills = vi.fn();
10
- const listChatAgentTools = vi.fn();
11
-
12
- vi.mock('@/lib/electron-bridge', () => ({
13
- isElectron: () => true,
14
- getElectronBridge: () => ({
15
- localBackend: { request: vi.fn() },
16
- local: {},
17
- }),
18
- }));
19
-
20
- vi.mock('@/lib/local-api', () => ({
21
- listChatAgents: (...args: unknown[]) => listChatAgents(...args),
22
- createChatAgent: (...args: unknown[]) => createChatAgent(...args),
23
- updateChatAgent: (...args: unknown[]) => updateChatAgent(...args),
24
- archiveChatAgent: (...args: unknown[]) => archiveChatAgent(...args),
25
- listChatAgentSkills: (...args: unknown[]) => listChatAgentSkills(...args),
26
- listChatAgentTools: (...args: unknown[]) => listChatAgentTools(...args),
27
- }));
28
-
29
- const { AgentsSettingsPanel } = await import('./AgentsSettingsPanel');
30
-
31
- const builtin: LocalChatAgent = {
32
- id: 'local-assistant',
33
- slug: 'local-assistant',
34
- name: '电脑操作员',
35
- icon: null,
36
- color: '#4f46e5',
37
- description: '默认本地助手',
38
- rolePrompt: '你是本地离线助手。',
39
- isBuiltin: true,
40
- sortOrder: 0,
41
- };
42
-
43
- const custom: LocalChatAgent = {
44
- id: 'geo-advisor',
45
- slug: null,
46
- name: '地质顾问',
47
- icon: null,
48
- color: '#16a34a',
49
- description: '答地质问题',
50
- rolePrompt: '你是地质顾问。',
51
- isBuiltin: false,
52
- sortOrder: 1,
53
- };
54
-
55
- beforeEach(() => {
56
- listChatAgents.mockReset();
57
- createChatAgent.mockReset();
58
- updateChatAgent.mockReset();
59
- archiveChatAgent.mockReset();
60
- listChatAgentSkills.mockReset();
61
- listChatAgentTools.mockReset();
62
- listChatAgents.mockResolvedValue({ agents: [builtin], total: 1 });
63
- createChatAgent.mockResolvedValue({ agent: custom });
64
- updateChatAgent.mockResolvedValue({ agent: custom });
65
- archiveChatAgent.mockResolvedValue({ id: custom.id, status: 'archived' });
66
- listChatAgentSkills.mockResolvedValue({
67
- skills: [
68
- {
69
- id: '90-csv-tools',
70
- name: 'csv-tools',
71
- displayName: 'CSV 工具',
72
- description: '处理本地 CSV 数据。',
73
- layer: 'catalog',
74
- },
75
- {
76
- id: '00-identity',
77
- name: 'identity',
78
- displayName: '',
79
- description: '基础身份。',
80
- layer: 'eager',
81
- },
82
- ],
83
- });
84
- listChatAgentTools.mockResolvedValue({
85
- tools: [
86
- { name: 'local_exec_shell', description: '执行本地命令。', category: 'local' },
87
- { name: 'local_read_file', description: '读取文件。', category: 'local' },
88
- ],
89
- });
90
- });
91
-
92
- afterEach(cleanup);
93
-
94
- /** 技能/工具勾选项的复选框(CheckRow 把 input 包在 label 里)。 */
95
- function checkbox(row: HTMLElement): HTMLInputElement {
96
- return row.querySelector('input') as HTMLInputElement;
97
- }
98
-
99
- describe('AgentsSettingsPanel', () => {
100
- it('lists builtin agents and hides archive on them', async () => {
101
- render(<AgentsSettingsPanel />);
102
- const row = await screen.findByTestId('agent-row-local-assistant');
103
- expect(row.textContent).toContain('电脑操作员');
104
- expect(row.textContent).toContain('内置');
105
- expect(screen.queryByTestId('agent-archive-local-assistant')).toBeNull();
106
- });
107
-
108
- it('creates a custom agent and refreshes the catalog', async () => {
109
- const onCatalogChange = vi.fn(async () => {});
110
- listChatAgents
111
- .mockResolvedValueOnce({ agents: [builtin], total: 1 })
112
- .mockResolvedValueOnce({ agents: [builtin, custom], total: 2 });
113
- render(<AgentsSettingsPanel onCatalogChange={onCatalogChange} />);
114
- await screen.findByTestId('agent-row-local-assistant');
115
- fireEvent.click(screen.getByTestId('agent-add'));
116
- fireEvent.change(screen.getByTestId('agent-form-name'), {
117
- target: { value: '地质顾问' },
118
- });
119
- fireEvent.change(screen.getByTestId('agent-form-role'), {
120
- target: { value: '你是地质顾问。' },
121
- });
122
- fireEvent.click(screen.getByTestId('agent-form-save'));
123
- await waitFor(() => {
124
- expect(createChatAgent).toHaveBeenCalledWith(
125
- expect.objectContaining({
126
- name: '地质顾问',
127
- rolePrompt: '你是地质顾问。',
128
- // 缺省能力面:不钉技能、不限制——新智能体不该一上手就少功能。
129
- skillIds: [],
130
- allowExternalSkills: true,
131
- loadAllSkills: false,
132
- toolPolicy: { mode: 'all', tools: [] },
133
- }),
134
- );
135
- });
136
- await screen.findByTestId('agent-row-geo-advisor');
137
- expect(onCatalogChange).toHaveBeenCalled();
138
- });
139
-
140
- it('保存勾选的技能与工具白名单', async () => {
141
- render(<AgentsSettingsPanel />);
142
- await screen.findByTestId('agent-row-local-assistant');
143
- fireEvent.click(screen.getByTestId('agent-add'));
144
- fireEvent.change(screen.getByTestId('agent-form-name'), {
145
- target: { value: '数据助手' },
146
- });
147
-
148
- const csvTools = await screen.findByTestId('agent-form-skill-90-csv-tools');
149
- fireEvent.click(checkbox(csvTools));
150
- fireEvent.click(screen.getByTestId('agent-form-allow-external-skills'));
151
- fireEvent.click(screen.getByTestId('agent-form-tool-mode-allowlist'));
152
- const readFile = await screen.findByTestId('agent-form-tool-local_read_file');
153
- fireEvent.click(checkbox(readFile));
154
-
155
- fireEvent.click(screen.getByTestId('agent-form-save'));
156
- await waitFor(() => {
157
- expect(createChatAgent).toHaveBeenCalledWith(
158
- expect.objectContaining({
159
- name: '数据助手',
160
- skillIds: ['90-csv-tools'],
161
- allowExternalSkills: false,
162
- toolPolicy: { mode: 'allowlist', tools: ['local_read_file'] },
163
- }),
164
- );
165
- });
166
- });
167
-
168
- it('编辑时回填已保存的能力面', async () => {
169
- const restricted: LocalChatAgent = {
170
- ...custom,
171
- skillIds: ['90-csv-tools'],
172
- allowExternalSkills: false,
173
- loadAllSkills: true,
174
- toolPolicy: { mode: 'denylist', tools: ['local_exec_shell'] },
175
- };
176
- listChatAgents.mockResolvedValue({ agents: [restricted], total: 1 });
177
- render(<AgentsSettingsPanel />);
178
- await screen.findByTestId('agent-row-geo-advisor');
179
- fireEvent.click(screen.getByTestId('agent-edit-geo-advisor'));
180
-
181
- const csvTools = await screen.findByTestId('agent-form-skill-90-csv-tools');
182
- expect(checkbox(csvTools).checked).toBe(true);
183
- expect(
184
- (screen.getByTestId('agent-form-allow-external-skills') as HTMLInputElement).checked,
185
- ).toBe(false);
186
- expect(
187
- (screen.getByTestId('agent-form-load-all-skills') as HTMLInputElement).checked,
188
- ).toBe(true);
189
- expect(
190
- screen.getByTestId('agent-form-tool-mode-denylist').getAttribute('aria-pressed'),
191
- ).toBe('true');
192
- const shell = await screen.findByTestId('agent-form-tool-local_exec_shell');
193
- expect(checkbox(shell).checked).toBe(true);
194
- });
195
-
196
- it('列表行摘要标出受限的智能体,缺省配置不占位', async () => {
197
- const restricted: LocalChatAgent = {
198
- ...custom,
199
- skillIds: ['90-csv-tools'],
200
- allowExternalSkills: false,
201
- toolPolicy: { mode: 'allowlist', tools: ['local_read_file'] },
202
- };
203
- listChatAgents.mockResolvedValue({ agents: [builtin, restricted], total: 2 });
204
- render(<AgentsSettingsPanel />);
205
- const summary = await screen.findByTestId('agent-capability-geo-advisor');
206
- expect(summary.textContent).toContain('技能 1');
207
- expect(summary.textContent).toContain('仅限所选技能');
208
- expect(summary.textContent).toContain('仅 1 个工具');
209
- expect(screen.queryByTestId('agent-capability-local-assistant')).toBeNull();
210
- });
211
-
212
- it('archives a custom agent after two-step confirm', async () => {
213
- listChatAgents
214
- .mockResolvedValueOnce({ agents: [builtin, custom], total: 2 })
215
- .mockResolvedValueOnce({ agents: [builtin], total: 1 });
216
- render(<AgentsSettingsPanel />);
217
- await screen.findByTestId('agent-row-geo-advisor');
218
- const archive = screen.getByTestId('agent-archive-geo-advisor');
219
- fireEvent.click(archive);
220
- expect(archiveChatAgent).not.toHaveBeenCalled();
221
- fireEvent.click(archive);
222
- await waitFor(() => {
223
- expect(archiveChatAgent).toHaveBeenCalledWith('geo-advisor');
224
- });
225
- await waitFor(() => {
226
- expect(screen.queryByTestId('agent-row-geo-advisor')).toBeNull();
227
- });
228
- });
229
- });