opc-agent 4.1.0 → 4.1.1

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 (245) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +20 -20
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +14 -14
  3. package/.github/PULL_REQUEST_TEMPLATE.md +13 -13
  4. package/CHANGELOG.md +48 -48
  5. package/CONTRIBUTING.md +36 -36
  6. package/README.zh-CN.md +497 -497
  7. package/dist/channels/wechat.js +6 -6
  8. package/dist/deploy/index.js +56 -56
  9. package/dist/studio/server.js +30 -1
  10. package/dist/studio-ui/index.html +230 -10
  11. package/dist/ui/components.js +105 -105
  12. package/examples/README.md +22 -22
  13. package/examples/basic-agent.ts +90 -90
  14. package/examples/brain-integration.ts +71 -71
  15. package/examples/multi-channel.ts +74 -74
  16. package/fix-sidebar.mjs +188 -188
  17. package/install.ps1 +154 -154
  18. package/install.sh +164 -164
  19. package/package.json +1 -1
  20. package/scripts/install.ps1 +31 -31
  21. package/scripts/install.sh +40 -40
  22. package/serve-studio.js +13 -13
  23. package/serve-test.js +25 -25
  24. package/src/channels/dingtalk.ts +46 -46
  25. package/src/channels/email.ts +351 -351
  26. package/src/channels/feishu.ts +349 -349
  27. package/src/channels/googlechat.ts +42 -42
  28. package/src/channels/imessage.ts +31 -31
  29. package/src/channels/irc.ts +82 -82
  30. package/src/channels/line.ts +32 -32
  31. package/src/channels/matrix.ts +33 -33
  32. package/src/channels/mattermost.ts +57 -57
  33. package/src/channels/msteams.ts +32 -32
  34. package/src/channels/nostr.ts +32 -32
  35. package/src/channels/qq.ts +33 -33
  36. package/src/channels/signal.ts +32 -32
  37. package/src/channels/sms.ts +33 -33
  38. package/src/channels/telegram.ts +616 -616
  39. package/src/channels/twitch.ts +65 -65
  40. package/src/channels/voice-call.ts +100 -100
  41. package/src/channels/websocket.ts +399 -399
  42. package/src/channels/wechat.ts +329 -329
  43. package/src/channels/whatsapp.ts +32 -32
  44. package/src/cli/chat.ts +99 -99
  45. package/src/cli/setup.ts +314 -314
  46. package/src/core/agent.ts +476 -476
  47. package/src/core/api-server.ts +277 -277
  48. package/src/core/audio.ts +98 -98
  49. package/src/core/collaboration.ts +275 -275
  50. package/src/core/context-discovery.ts +85 -85
  51. package/src/core/context-refs.ts +140 -140
  52. package/src/core/gateway.ts +106 -106
  53. package/src/core/heartbeat.ts +51 -51
  54. package/src/core/hooks.ts +105 -105
  55. package/src/core/ide-bridge.ts +133 -133
  56. package/src/core/node-network.ts +86 -86
  57. package/src/core/profiles.ts +122 -122
  58. package/src/core/scheduler.ts +187 -187
  59. package/src/core/session-manager.ts +137 -137
  60. package/src/core/subagent.ts +98 -98
  61. package/src/core/vision.ts +180 -180
  62. package/src/core/workflow-graph.ts +365 -365
  63. package/src/daemon.ts +96 -96
  64. package/src/deploy/index.ts +255 -255
  65. package/src/doctor.ts +156 -156
  66. package/src/eval/index.ts +211 -211
  67. package/src/eval/suites/basic.json +16 -16
  68. package/src/eval/suites/memory.json +12 -12
  69. package/src/eval/suites/safety.json +14 -14
  70. package/src/hub/brain-seed.ts +54 -54
  71. package/src/hub/client.ts +60 -60
  72. package/src/mcp/servers/calculator-mcp.ts +65 -65
  73. package/src/mcp/servers/crypto-mcp.ts +73 -73
  74. package/src/mcp/servers/database-mcp.ts +72 -72
  75. package/src/mcp/servers/datetime-mcp.ts +69 -69
  76. package/src/mcp/servers/filesystem.ts +66 -66
  77. package/src/mcp/servers/github-mcp.ts +58 -58
  78. package/src/mcp/servers/index.ts +63 -63
  79. package/src/mcp/servers/json-mcp.ts +102 -102
  80. package/src/mcp/servers/memory-mcp.ts +56 -56
  81. package/src/mcp/servers/regex-mcp.ts +53 -53
  82. package/src/mcp/servers/web-mcp.ts +49 -49
  83. package/src/memory/context-compressor.ts +189 -189
  84. package/src/memory/seed-loader.ts +212 -212
  85. package/src/memory/user-profiler.ts +215 -215
  86. package/src/plugins/content-filter.ts +23 -23
  87. package/src/plugins/logger.ts +18 -18
  88. package/src/plugins/rate-limiter.ts +38 -38
  89. package/src/protocols/a2a/client.ts +132 -132
  90. package/src/protocols/a2a/index.ts +8 -8
  91. package/src/protocols/a2a/server.ts +333 -333
  92. package/src/protocols/a2a/types.ts +88 -88
  93. package/src/protocols/a2a/utils.ts +50 -50
  94. package/src/protocols/agui/client.ts +83 -83
  95. package/src/protocols/agui/index.ts +4 -4
  96. package/src/protocols/agui/server.ts +218 -218
  97. package/src/protocols/agui/types.ts +153 -153
  98. package/src/protocols/index.ts +2 -2
  99. package/src/protocols/mcp/agent-tools.ts +134 -134
  100. package/src/protocols/mcp/index.ts +8 -8
  101. package/src/protocols/mcp/server.ts +262 -262
  102. package/src/protocols/mcp/types.ts +69 -69
  103. package/src/providers/index.ts +632 -632
  104. package/src/publish/index.ts +376 -376
  105. package/src/scheduler/cron-engine.ts +191 -191
  106. package/src/scheduler/index.ts +2 -2
  107. package/src/schema/oad.ts +217 -217
  108. package/src/security/approval.ts +131 -131
  109. package/src/security/approvals.ts +143 -143
  110. package/src/security/elevated.ts +105 -105
  111. package/src/security/guardrails.ts +248 -248
  112. package/src/security/index.ts +9 -9
  113. package/src/security/keys.ts +87 -87
  114. package/src/security/secrets.ts +129 -129
  115. package/src/skills/builtin/index.ts +408 -408
  116. package/src/skills/marketplace.ts +113 -113
  117. package/src/skills/types.ts +42 -42
  118. package/src/studio/server.ts +31 -1
  119. package/src/studio/templates-data.ts +178 -178
  120. package/src/studio-ui/index.html +230 -10
  121. package/src/telemetry/index.ts +324 -324
  122. package/src/tools/builtin/browser.ts +299 -299
  123. package/src/tools/builtin/datetime.ts +41 -41
  124. package/src/tools/builtin/file.ts +107 -107
  125. package/src/tools/builtin/home-assistant.ts +116 -116
  126. package/src/tools/builtin/rl-tools.ts +243 -243
  127. package/src/tools/builtin/shell.ts +43 -43
  128. package/src/tools/builtin/vision.ts +64 -64
  129. package/src/tools/builtin/web-search.ts +126 -126
  130. package/src/tools/builtin/web.ts +35 -35
  131. package/src/tools/document-processor.ts +213 -213
  132. package/src/tools/image-generator.ts +150 -150
  133. package/src/tools/integrations/calendar.ts +73 -73
  134. package/src/tools/integrations/code-exec.ts +39 -39
  135. package/src/tools/integrations/csv-analyzer.ts +92 -92
  136. package/src/tools/integrations/database.ts +44 -44
  137. package/src/tools/integrations/email-send.ts +76 -76
  138. package/src/tools/integrations/git-tool.ts +42 -42
  139. package/src/tools/integrations/github-tool.ts +76 -76
  140. package/src/tools/integrations/image-gen.ts +56 -56
  141. package/src/tools/integrations/index.ts +92 -92
  142. package/src/tools/integrations/jira.ts +83 -83
  143. package/src/tools/integrations/notion.ts +71 -71
  144. package/src/tools/integrations/npm-tool.ts +48 -48
  145. package/src/tools/integrations/pdf-reader.ts +58 -58
  146. package/src/tools/integrations/slack.ts +65 -65
  147. package/src/tools/integrations/summarizer.ts +49 -49
  148. package/src/tools/integrations/translator.ts +48 -48
  149. package/src/tools/integrations/trello.ts +60 -60
  150. package/src/tools/integrations/vector-search.ts +42 -42
  151. package/src/tools/integrations/web-scraper.ts +47 -47
  152. package/src/tools/integrations/web-search.ts +58 -58
  153. package/src/tools/integrations/webhook.ts +38 -38
  154. package/src/tools/mcp-client.ts +131 -131
  155. package/src/tools/web-scraper.ts +179 -179
  156. package/src/tools/web-search.ts +180 -180
  157. package/src/ui/components.ts +127 -127
  158. package/srv-out.txt +1 -1
  159. package/templates/ecommerce-assistant/README.md +45 -45
  160. package/templates/ecommerce-assistant/oad.yaml +47 -47
  161. package/templates/tech-support/README.md +43 -43
  162. package/templates/tech-support/oad.yaml +45 -45
  163. package/test-agent/Dockerfile +9 -9
  164. package/test-agent/README.md +50 -50
  165. package/test-agent/agent.yaml +23 -23
  166. package/test-agent/docker-compose.yml +11 -11
  167. package/test-agent/oad.yaml +31 -31
  168. package/test-agent/package-lock.json +1492 -1492
  169. package/test-agent/package.json +17 -17
  170. package/test-agent/src/index.ts +24 -24
  171. package/test-agent/src/skills/echo.ts +15 -15
  172. package/test-agent/tsconfig.json +24 -24
  173. package/test-full.js +43 -43
  174. package/test-sidebar.js +22 -22
  175. package/test-studio3.js +75 -75
  176. package/test-studio4.js +41 -41
  177. package/tests/a2a-protocol.test.ts +285 -285
  178. package/tests/agui-protocol.test.ts +246 -246
  179. package/tests/api-server.test.ts +148 -148
  180. package/tests/approvals.test.ts +89 -89
  181. package/tests/audio.test.ts +40 -40
  182. package/tests/brain-seed-extended.test.ts +490 -490
  183. package/tests/brain-seed.test.ts +239 -239
  184. package/tests/browser.test.ts +179 -179
  185. package/tests/channels/discord.test.ts +79 -79
  186. package/tests/channels/email.test.ts +148 -148
  187. package/tests/channels/feishu.test.ts +123 -123
  188. package/tests/channels/telegram.test.ts +129 -129
  189. package/tests/channels/websocket.test.ts +53 -53
  190. package/tests/channels/wechat.test.ts +170 -170
  191. package/tests/channels-extra.test.ts +45 -45
  192. package/tests/chat-cli.test.ts +160 -160
  193. package/tests/cli.test.ts +46 -46
  194. package/tests/context-compressor.test.ts +172 -172
  195. package/tests/context-refs.test.ts +121 -121
  196. package/tests/cron-engine.test.ts +101 -101
  197. package/tests/daemon.test.ts +135 -135
  198. package/tests/deepbrain-wire.test.ts +234 -234
  199. package/tests/deploy-and-dag.test.ts +196 -196
  200. package/tests/doctor.test.ts +38 -38
  201. package/tests/document-processor.test.ts +69 -69
  202. package/tests/e2e-nocode.test.ts +442 -442
  203. package/tests/elevated.test.ts +69 -69
  204. package/tests/eval.test.ts +173 -173
  205. package/tests/gateway.test.ts +63 -63
  206. package/tests/guardrails.test.ts +177 -177
  207. package/tests/home-assistant.test.ts +40 -40
  208. package/tests/hooks.test.ts +79 -79
  209. package/tests/ide-bridge.test.ts +38 -38
  210. package/tests/image-generator.test.ts +84 -84
  211. package/tests/init-role.test.ts +124 -124
  212. package/tests/integrations.test.ts +249 -249
  213. package/tests/mcp-client.test.ts +92 -92
  214. package/tests/mcp-server.test.ts +178 -178
  215. package/tests/mcp-servers.test.ts +260 -260
  216. package/tests/node-network.test.ts +74 -74
  217. package/tests/plugin-a2a-enhanced.test.ts +230 -230
  218. package/tests/profiles.test.ts +61 -61
  219. package/tests/publish.test.ts +231 -231
  220. package/tests/rl-tools.test.ts +93 -93
  221. package/tests/sandbox-manager.test.ts +46 -46
  222. package/tests/scheduler.test.ts +200 -200
  223. package/tests/secrets.test.ts +107 -107
  224. package/tests/security-enhanced.test.ts +233 -233
  225. package/tests/settings-api.test.ts +148 -148
  226. package/tests/setup.test.ts +73 -73
  227. package/tests/subagent.test.ts +193 -193
  228. package/tests/telegram-discord.test.ts +60 -60
  229. package/tests/telemetry.test.ts +186 -186
  230. package/tests/user-profiler.test.ts +169 -169
  231. package/tests/v090-features.test.ts +254 -254
  232. package/tests/vision.test.ts +61 -61
  233. package/tests/voice-call.test.ts +47 -47
  234. package/tests/voice-enhanced.test.ts +169 -169
  235. package/tests/voice-interaction.test.ts +38 -38
  236. package/tests/web-search.test.ts +155 -155
  237. package/tests/workflow-graph.test.ts +279 -279
  238. package/tutorial/customer-service-agent/README.md +612 -612
  239. package/tutorial/customer-service-agent/SOUL.md +26 -26
  240. package/tutorial/customer-service-agent/agent.yaml +63 -63
  241. package/tutorial/customer-service-agent/package.json +19 -19
  242. package/tutorial/customer-service-agent/src/index.ts +69 -69
  243. package/tutorial/customer-service-agent/src/skills/faq.ts +27 -27
  244. package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -22
  245. package/tutorial/customer-service-agent/tsconfig.json +14 -14
@@ -1,442 +1,442 @@
1
- /**
2
- * e2e-nocode.test.ts
3
- * 小白视角端到端测试 / End-to-end tests from a non-coder's perspective
4
- *
5
- * 模拟一个完全不懂代码的人使用 OPC Studio 的每一步操作。
6
- * Simulates every step a non-technical user would take in OPC Studio.
7
- *
8
- * All network calls (Ollama, external APIs) are mocked — no real infra needed.
9
- */
10
-
11
- import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
12
- import { StudioServer } from '../src/studio/server';
13
- import * as http from 'http';
14
- import * as fs from 'fs';
15
- import * as path from 'path';
16
- import * as os from 'os';
17
-
18
- // --------------- helpers ---------------
19
-
20
- const PORT = 19876;
21
- const FIXTURE_DIR = path.join(__dirname, '__e2e_nocode_fixture__');
22
- const STATIC_DIR = path.join(FIXTURE_DIR, 'studio-ui');
23
-
24
- /** Minimal HTTP fetch that works without node-fetch */
25
- function fetch(
26
- urlPath: string,
27
- method = 'GET',
28
- body?: string | object,
29
- ): Promise<{ status: number; headers: any; body: string; json: () => any }> {
30
- const bodyStr = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined;
31
- return new Promise((resolve, reject) => {
32
- const req = http.request(
33
- { hostname: '127.0.0.1', port: PORT, path: `/api/${urlPath}`, method, headers: body ? { 'Content-Type': 'application/json' } : {} },
34
- (res) => {
35
- let data = '';
36
- res.on('data', (c) => (data += c));
37
- res.on('end', () =>
38
- resolve({
39
- status: res.statusCode!,
40
- headers: res.headers,
41
- body: data,
42
- json: () => JSON.parse(data),
43
- }),
44
- );
45
- },
46
- );
47
- req.on('error', reject);
48
- if (bodyStr) req.write(bodyStr);
49
- req.end();
50
- });
51
- }
52
-
53
- /** Fetch raw path (not prefixed with /api/) */
54
- function fetchRaw(urlPath: string): Promise<{ status: number; body: string }> {
55
- return new Promise((resolve, reject) => {
56
- http.get({ hostname: '127.0.0.1', port: PORT, path: urlPath }, (res) => {
57
- let data = '';
58
- res.on('data', (c) => (data += c));
59
- res.on('end', () => resolve({ status: res.statusCode!, body: data }));
60
- }).on('error', reject);
61
- });
62
- }
63
-
64
- // --------------- setup / teardown ---------------
65
-
66
- let server: StudioServer;
67
-
68
- beforeAll(async () => {
69
- fs.mkdirSync(STATIC_DIR, { recursive: true });
70
- fs.writeFileSync(path.join(STATIC_DIR, 'index.html'), '<html><body>OPC Studio Dashboard</body></html>');
71
- fs.writeFileSync(
72
- path.join(FIXTURE_DIR, 'package.json'),
73
- JSON.stringify({ name: 'test-nocode', version: '1.0.0', description: 'E2E fixture' }),
74
- );
75
-
76
- server = new StudioServer({ port: PORT, agentDir: FIXTURE_DIR, staticDir: STATIC_DIR });
77
- await server.start();
78
- await new Promise((r) => setTimeout(r, 300));
79
- });
80
-
81
- afterAll(async () => {
82
- await server.stop();
83
- fs.rmSync(FIXTURE_DIR, { recursive: true, force: true });
84
- });
85
-
86
- // --------------- 场景 1:首次打开 / First Open ---------------
87
-
88
- describe('场景1: 首次打开 / First Open', () => {
89
- it('GET / 应该返回 Dashboard 页面 / should serve the dashboard page', async () => {
90
- const res = await fetchRaw('/');
91
- expect(res.status).toBe(200);
92
- expect(res.body).toContain('OPC Studio');
93
- });
94
-
95
- it('没有 Agent 时可以获取列表 / can fetch agent list when none created', async () => {
96
- const res = await fetch('agents');
97
- expect(res.status).toBe(200);
98
- const data = res.json();
99
- expect(Array.isArray(data) || typeof data === 'object').toBe(true);
100
- });
101
- });
102
-
103
- // --------------- 场景 2:浏览模板市场 / Browse Template Market ---------------
104
-
105
- describe('场景2: 浏览模板市场 / Template Market', () => {
106
- it('GET /api/templates 返回 100+ 模板 / returns 100+ templates', async () => {
107
- const res = await fetch('templates');
108
- expect(res.status).toBe(200);
109
- const data = res.json();
110
- expect(Array.isArray(data.templates || data)).toBe(true);
111
- const list = data.templates || data;
112
- expect(list.length).toBeGreaterThanOrEqual(100);
113
- });
114
-
115
- it('按行业筛选(technology)/ filter by industry', async () => {
116
- const res = await fetch('templates?industry=technology');
117
- expect(res.status).toBe(200);
118
- const data = res.json();
119
- const list = data.templates || data;
120
- expect(list.length).toBeGreaterThan(0);
121
- list.forEach((t: any) => expect(t.industry).toBe('technology'));
122
- });
123
-
124
- it('搜索关键词 / search by keyword', async () => {
125
- const res = await fetch('templates?search=support');
126
- expect(res.status).toBe(200);
127
- const data = res.json();
128
- const list = data.templates || data;
129
- expect(list.length).toBeGreaterThan(0);
130
- });
131
-
132
- it('每个模板有必要字段 / each template has required fields', async () => {
133
- const res = await fetch('templates');
134
- const data = res.json();
135
- const list = data.templates || data;
136
- const sample = list[0];
137
- expect(sample).toHaveProperty('name');
138
- expect(sample).toHaveProperty('description');
139
- expect(sample).toHaveProperty('industry');
140
- expect(sample).toHaveProperty('icon');
141
- });
142
- });
143
-
144
- // --------------- 场景 3:3 步创建 Agent / 3-Step Agent Creation ---------------
145
-
146
- describe('场景3: 3步创建Agent / 3-Step Agent Creation', () => {
147
- let createdId: string;
148
-
149
- it('Step1: 选模板创建 Agent / create agent with template', async () => {
150
- const res = await fetch('agents', 'POST', { template_id: 'tech-support', name: 'My First Agent' });
151
- expect([200, 201]).toContain(res.status);
152
- const data = res.json();
153
- expect(data.id).toBeTruthy();
154
- createdId = data.id;
155
- });
156
-
157
- it('Step2: 验证有默认值 / agent has defaults', async () => {
158
- expect(createdId).toBeTruthy();
159
- const res = await fetch(`agents/${createdId}`);
160
- expect([200, 201]).toContain(res.status);
161
- const data = res.json();
162
- expect(data.id || data.name).toBeTruthy();
163
- });
164
-
165
- it('Step3: Agent 数据已持久化 / agent data persisted', async () => {
166
- expect(createdId).toBeTruthy();
167
- const res = await fetch(`agents/${createdId}`);
168
- expect([200, 201]).toContain(res.status);
169
- });
170
-
171
- it('创建后在列表中可见 / appears in agent list after creation', async () => {
172
- const res = await fetch('agents');
173
- expect(res.status).toBe(200);
174
- const data = res.json();
175
- const list = Array.isArray(data) ? data : (data.agents || []);
176
- expect(list.some((a: any) => a.id === createdId)).toBe(true);
177
- });
178
-
179
- // cleanup
180
- afterAll(async () => {
181
- if (createdId) {
182
- await fetch(`agents/${createdId}`, 'DELETE');
183
- }
184
- });
185
- });
186
-
187
- // --------------- 场景 4:模型配置 / Model Configuration ---------------
188
-
189
- describe('场景4: 模型配置 / Model Configuration', () => {
190
- it('GET /api/settings/models 返回当前配置 / returns current model config', async () => {
191
- const res = await fetch('settings/models');
192
- expect(res.status).toBe(200);
193
- const data = res.json();
194
- // Should have default values
195
- expect(data.chatModel || data.mode).toBeTruthy();
196
- });
197
-
198
- it('GET /api/settings/models/local 检测本地 Ollama / detect local Ollama', async () => {
199
- const res = await fetch('settings/models/local');
200
- expect(res.status).toBe(200);
201
- const data = res.json();
202
- // Response should indicate Ollama running state and model list
203
- expect(data).toHaveProperty('running');
204
- expect(data).toHaveProperty('models');
205
- });
206
-
207
- it('默认值正确 / correct defaults: qwen2.5:7b + nomic-embed-text', async () => {
208
- const res = await fetch('settings/models');
209
- const data = res.json();
210
- expect(data.chatModel).toBe('qwen2.5:7b');
211
- expect(data.embeddingModel).toBe('nomic-embed-text');
212
- });
213
-
214
- it('PUT /api/settings/models 保存配置 / saves model config', async () => {
215
- const res = await fetch('settings/models', 'PUT', {
216
- mode: 'local',
217
- provider: 'ollama',
218
- chatModel: 'llama3:8b',
219
- embeddingModel: 'nomic-embed-text',
220
- });
221
- expect(res.status).toBe(200);
222
-
223
- // Read back
224
- const res2 = await fetch('settings/models');
225
- const data = res2.json();
226
- expect(data.chatModel).toBe('llama3:8b');
227
-
228
- // Restore default
229
- await fetch('settings/models', 'PUT', {
230
- mode: 'local',
231
- provider: 'ollama',
232
- chatModel: 'qwen2.5:7b',
233
- embeddingModel: 'nomic-embed-text',
234
- });
235
- });
236
-
237
- it('POST /api/settings/models/test 测试连接 / test model connection', async () => {
238
- const res = await fetch('settings/models/test', 'POST', {
239
- provider: 'ollama',
240
- chatModel: 'qwen2.5:7b',
241
- });
242
- expect(res.status).toBe(200);
243
- const data = res.json();
244
- // Should return success or error, not crash
245
- expect(data).toHaveProperty('success');
246
- });
247
- });
248
-
249
- // --------------- 场景 5:渠道配置 / Channel Configuration ---------------
250
-
251
- describe('场景5: 渠道配置 / Channel Configuration', () => {
252
- it('GET /api/settings/channels 返回渠道列表 / returns channel list', async () => {
253
- const res = await fetch('settings/channels');
254
- expect(res.status).toBe(200);
255
- const data = res.json();
256
- // Channels endpoint returns data (may be empty if none configured)
257
- expect(data).toBeTruthy();
258
- });
259
-
260
- it('每个渠道有状态标识 / each channel has status', async () => {
261
- const res = await fetch('settings/channels');
262
- const data = res.json();
263
- const list = Array.isArray(data) ? data : (data.channels || []);
264
- if (list.length > 0) {
265
- const ch = list[0];
266
- expect(ch.name || ch.id || ch.type).toBeTruthy();
267
- }
268
- });
269
-
270
- it('PUT /api/settings/channels/:name 保存渠道配置 / save channel config', async () => {
271
- const res = await fetch('settings/channels/telegram', 'PUT', {
272
- token: 'test-token-12345',
273
- enabled: true,
274
- });
275
- expect(res.status).toBe(200);
276
- });
277
- });
278
-
279
- // --------------- 场景 6:对话 / Chat ---------------
280
-
281
- describe('场景6: 对话 / Chat', () => {
282
- let agentId: string;
283
-
284
- beforeAll(async () => {
285
- const res = await fetch('agents', 'POST', { template_id: 'tech-support', name: 'Chat Test Agent' });
286
- agentId = res.json().id;
287
- });
288
-
289
- afterAll(async () => {
290
- if (agentId) await fetch(`agents/${agentId}`, 'DELETE');
291
- });
292
-
293
- it('POST /api/agents/:id/chat 返回响应 / returns chat response', async () => {
294
- const res = await fetch(`agents/${agentId}/chat`, 'POST', { message: 'Hello!' });
295
- // SSE or JSON response — should not be 404/500
296
- expect([200, 201]).toContain(res.status);
297
- });
298
-
299
- it('空消息应该报错 / empty message should error', async () => {
300
- const res = await fetch(`agents/${agentId}/chat`, 'POST', { message: '' });
301
- // Server may reject or accept — at minimum should not crash (5xx)
302
- expect(res.status).toBeLessThan(500);
303
- });
304
-
305
- it('Agent 不存在应该 404 / non-existent agent returns 404', async () => {
306
- const res = await fetch('agents/nonexistent-id-12345/chat', 'POST', { message: 'hi' });
307
- expect(res.status).toBe(404);
308
- });
309
- });
310
-
311
- // --------------- 场景 7:运行状态 / Runtime Status ---------------
312
-
313
- describe('场景7: 运行状态 / Runtime Status', () => {
314
- it('GET /api/settings/status 返回运行信息 / returns status info', async () => {
315
- const res = await fetch('settings/status');
316
- expect(res.status).toBe(200);
317
- const data = res.json();
318
- expect(data).toBeTruthy();
319
- // Should contain uptime or memory info
320
- expect(data.uptime !== undefined || data.memory !== undefined || data.modules !== undefined).toBe(true);
321
- });
322
- });
323
-
324
- // --------------- 场景 8:用量统计 / Usage Stats ---------------
325
-
326
- describe('场景8: 用量统计 / Usage Stats', () => {
327
- it('GET /api/settings/usage 返回用量数据 / returns usage data', async () => {
328
- const res = await fetch('settings/usage');
329
- expect(res.status).toBe(200);
330
- const data = res.json();
331
- expect(data).toBeTruthy();
332
- });
333
- });
334
-
335
- // --------------- 场景 9:记忆管理入口 / Memory Management ---------------
336
-
337
- describe('场景9: 记忆管理入口 / Memory Management Entry', () => {
338
- it('settings/status 包含 DeepBrain 模块信息 / status includes DeepBrain module', async () => {
339
- const res = await fetch('settings/status');
340
- expect(res.status).toBe(200);
341
- const data = res.json();
342
- // DeepBrain should be listed in modules
343
- if (data.modules) {
344
- const brain = data.modules.find((m: any) => m.name === 'DeepBrain' || m.path === 'brain');
345
- expect(brain).toBeTruthy();
346
- }
347
- });
348
- });
349
-
350
- // --------------- 场景 10:角色编辑入口 / Role Editor Entry ---------------
351
-
352
- describe('场景10: 角色编辑入口 / Role Editor Entry', () => {
353
- it('settings/status 包含 Workstation 模块信息 / status includes Workstation module', async () => {
354
- const res = await fetch('settings/status');
355
- expect(res.status).toBe(200);
356
- const data = res.json();
357
- if (data.modules) {
358
- const ws = data.modules.find((m: any) => m.name === 'Workstation' || m.path === 'workstation');
359
- expect(ws).toBeTruthy();
360
- }
361
- });
362
- });
363
-
364
- // --------------- 场景 11:完整流程 / Full E2E Flow ---------------
365
-
366
- describe('场景11: 完整端到端流程 / Full E2E Flow', () => {
367
- let agentId: string;
368
-
369
- it('完整操作链 / full operation chain', async () => {
370
- // 1. 浏览模板
371
- const templates = await fetch('templates');
372
- expect(templates.status).toBe(200);
373
-
374
- // 2. 创建 Agent
375
- const create = await fetch('agents', 'POST', { template_id: 'tech-support', name: 'E2E Flow Agent' });
376
- expect([200, 201]).toContain(create.status);
377
- agentId = create.json().id;
378
-
379
- // 3. 查看 Dashboard(agent list)
380
- const list = await fetch('agents');
381
- expect(list.status).toBe(200);
382
- const agents = list.json();
383
- const agentList = Array.isArray(agents) ? agents : (agents.agents || []);
384
- expect(agentList.some((a: any) => a.id === agentId)).toBe(true);
385
-
386
- // 4. 查看模型配置
387
- const models = await fetch('settings/models');
388
- expect(models.status).toBe(200);
389
-
390
- // 5. 查看渠道
391
- const channels = await fetch('settings/channels');
392
- expect(channels.status).toBe(200);
393
-
394
- // 6. 查看状态
395
- const status = await fetch('settings/status');
396
- expect(status.status).toBe(200);
397
-
398
- // 7. 查看用量
399
- const usage = await fetch('settings/usage');
400
- expect(usage.status).toBe(200);
401
-
402
- // Cleanup
403
- await fetch(`agents/${agentId}`, 'DELETE');
404
- });
405
- });
406
-
407
- // --------------- 场景 12:错误处理 / Error Handling ---------------
408
-
409
- describe('场景12: 错误处理(小白友好)/ User-Friendly Error Handling', () => {
410
- it('无效 Agent ID 返回友好错误 / invalid agent ID returns friendly error', async () => {
411
- const res = await fetch('agents/this-does-not-exist');
412
- expect(res.status).toBe(404);
413
- const data = res.json();
414
- // Should have a message, not a raw stack trace
415
- expect(data.error || data.message).toBeTruthy();
416
- expect(res.body).not.toContain('Error:');
417
- expect(res.body).not.toContain('at Object.');
418
- });
419
-
420
- it('缺少必填字段仍能处理 / missing fields handled gracefully', async () => {
421
- const res = await fetch('agents', 'POST', {});
422
- // Server should handle it (may create with defaults or reject)
423
- expect(res.status).toBeLessThan(500);
424
- });
425
-
426
- it('模型测试失败返回友好提示 / model test failure gives friendly message', async () => {
427
- const res = await fetch('settings/models/test', 'POST', {
428
- provider: 'ollama',
429
- chatModel: 'nonexistent-model',
430
- baseUrl: 'http://localhost:99999',
431
- });
432
- // Server may return 200 with success:false or 500 — either way should have info
433
- const data = res.json();
434
- if (res.status === 200 && !data.success) {
435
- expect(data.error || data.message).toBeTruthy();
436
- }
437
- // If 500, the error should still be parseable JSON (not raw stack)
438
- if (res.status >= 500) {
439
- expect(data.error || data.message).toBeTruthy();
440
- }
441
- });
442
- });
1
+ /**
2
+ * e2e-nocode.test.ts
3
+ * 小白视角端到端测试 / End-to-end tests from a non-coder's perspective
4
+ *
5
+ * 模拟一个完全不懂代码的人使用 OPC Studio 的每一步操作。
6
+ * Simulates every step a non-technical user would take in OPC Studio.
7
+ *
8
+ * All network calls (Ollama, external APIs) are mocked — no real infra needed.
9
+ */
10
+
11
+ import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
12
+ import { StudioServer } from '../src/studio/server';
13
+ import * as http from 'http';
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import * as os from 'os';
17
+
18
+ // --------------- helpers ---------------
19
+
20
+ const PORT = 19876;
21
+ const FIXTURE_DIR = path.join(__dirname, '__e2e_nocode_fixture__');
22
+ const STATIC_DIR = path.join(FIXTURE_DIR, 'studio-ui');
23
+
24
+ /** Minimal HTTP fetch that works without node-fetch */
25
+ function fetch(
26
+ urlPath: string,
27
+ method = 'GET',
28
+ body?: string | object,
29
+ ): Promise<{ status: number; headers: any; body: string; json: () => any }> {
30
+ const bodyStr = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined;
31
+ return new Promise((resolve, reject) => {
32
+ const req = http.request(
33
+ { hostname: '127.0.0.1', port: PORT, path: `/api/${urlPath}`, method, headers: body ? { 'Content-Type': 'application/json' } : {} },
34
+ (res) => {
35
+ let data = '';
36
+ res.on('data', (c) => (data += c));
37
+ res.on('end', () =>
38
+ resolve({
39
+ status: res.statusCode!,
40
+ headers: res.headers,
41
+ body: data,
42
+ json: () => JSON.parse(data),
43
+ }),
44
+ );
45
+ },
46
+ );
47
+ req.on('error', reject);
48
+ if (bodyStr) req.write(bodyStr);
49
+ req.end();
50
+ });
51
+ }
52
+
53
+ /** Fetch raw path (not prefixed with /api/) */
54
+ function fetchRaw(urlPath: string): Promise<{ status: number; body: string }> {
55
+ return new Promise((resolve, reject) => {
56
+ http.get({ hostname: '127.0.0.1', port: PORT, path: urlPath }, (res) => {
57
+ let data = '';
58
+ res.on('data', (c) => (data += c));
59
+ res.on('end', () => resolve({ status: res.statusCode!, body: data }));
60
+ }).on('error', reject);
61
+ });
62
+ }
63
+
64
+ // --------------- setup / teardown ---------------
65
+
66
+ let server: StudioServer;
67
+
68
+ beforeAll(async () => {
69
+ fs.mkdirSync(STATIC_DIR, { recursive: true });
70
+ fs.writeFileSync(path.join(STATIC_DIR, 'index.html'), '<html><body>OPC Studio Dashboard</body></html>');
71
+ fs.writeFileSync(
72
+ path.join(FIXTURE_DIR, 'package.json'),
73
+ JSON.stringify({ name: 'test-nocode', version: '1.0.0', description: 'E2E fixture' }),
74
+ );
75
+
76
+ server = new StudioServer({ port: PORT, agentDir: FIXTURE_DIR, staticDir: STATIC_DIR });
77
+ await server.start();
78
+ await new Promise((r) => setTimeout(r, 300));
79
+ });
80
+
81
+ afterAll(async () => {
82
+ await server.stop();
83
+ fs.rmSync(FIXTURE_DIR, { recursive: true, force: true });
84
+ });
85
+
86
+ // --------------- 场景 1:首次打开 / First Open ---------------
87
+
88
+ describe('场景1: 首次打开 / First Open', () => {
89
+ it('GET / 应该返回 Dashboard 页面 / should serve the dashboard page', async () => {
90
+ const res = await fetchRaw('/');
91
+ expect(res.status).toBe(200);
92
+ expect(res.body).toContain('OPC Studio');
93
+ });
94
+
95
+ it('没有 Agent 时可以获取列表 / can fetch agent list when none created', async () => {
96
+ const res = await fetch('agents');
97
+ expect(res.status).toBe(200);
98
+ const data = res.json();
99
+ expect(Array.isArray(data) || typeof data === 'object').toBe(true);
100
+ });
101
+ });
102
+
103
+ // --------------- 场景 2:浏览模板市场 / Browse Template Market ---------------
104
+
105
+ describe('场景2: 浏览模板市场 / Template Market', () => {
106
+ it('GET /api/templates 返回 100+ 模板 / returns 100+ templates', async () => {
107
+ const res = await fetch('templates');
108
+ expect(res.status).toBe(200);
109
+ const data = res.json();
110
+ expect(Array.isArray(data.templates || data)).toBe(true);
111
+ const list = data.templates || data;
112
+ expect(list.length).toBeGreaterThanOrEqual(100);
113
+ });
114
+
115
+ it('按行业筛选(technology)/ filter by industry', async () => {
116
+ const res = await fetch('templates?industry=technology');
117
+ expect(res.status).toBe(200);
118
+ const data = res.json();
119
+ const list = data.templates || data;
120
+ expect(list.length).toBeGreaterThan(0);
121
+ list.forEach((t: any) => expect(t.industry).toBe('technology'));
122
+ });
123
+
124
+ it('搜索关键词 / search by keyword', async () => {
125
+ const res = await fetch('templates?search=support');
126
+ expect(res.status).toBe(200);
127
+ const data = res.json();
128
+ const list = data.templates || data;
129
+ expect(list.length).toBeGreaterThan(0);
130
+ });
131
+
132
+ it('每个模板有必要字段 / each template has required fields', async () => {
133
+ const res = await fetch('templates');
134
+ const data = res.json();
135
+ const list = data.templates || data;
136
+ const sample = list[0];
137
+ expect(sample).toHaveProperty('name');
138
+ expect(sample).toHaveProperty('description');
139
+ expect(sample).toHaveProperty('industry');
140
+ expect(sample).toHaveProperty('icon');
141
+ });
142
+ });
143
+
144
+ // --------------- 场景 3:3 步创建 Agent / 3-Step Agent Creation ---------------
145
+
146
+ describe('场景3: 3步创建Agent / 3-Step Agent Creation', () => {
147
+ let createdId: string;
148
+
149
+ it('Step1: 选模板创建 Agent / create agent with template', async () => {
150
+ const res = await fetch('agents', 'POST', { template_id: 'tech-support', name: 'My First Agent' });
151
+ expect([200, 201]).toContain(res.status);
152
+ const data = res.json();
153
+ expect(data.id).toBeTruthy();
154
+ createdId = data.id;
155
+ });
156
+
157
+ it('Step2: 验证有默认值 / agent has defaults', async () => {
158
+ expect(createdId).toBeTruthy();
159
+ const res = await fetch(`agents/${createdId}`);
160
+ expect([200, 201]).toContain(res.status);
161
+ const data = res.json();
162
+ expect(data.id || data.name).toBeTruthy();
163
+ });
164
+
165
+ it('Step3: Agent 数据已持久化 / agent data persisted', async () => {
166
+ expect(createdId).toBeTruthy();
167
+ const res = await fetch(`agents/${createdId}`);
168
+ expect([200, 201]).toContain(res.status);
169
+ });
170
+
171
+ it('创建后在列表中可见 / appears in agent list after creation', async () => {
172
+ const res = await fetch('agents');
173
+ expect(res.status).toBe(200);
174
+ const data = res.json();
175
+ const list = Array.isArray(data) ? data : (data.agents || []);
176
+ expect(list.some((a: any) => a.id === createdId)).toBe(true);
177
+ });
178
+
179
+ // cleanup
180
+ afterAll(async () => {
181
+ if (createdId) {
182
+ await fetch(`agents/${createdId}`, 'DELETE');
183
+ }
184
+ });
185
+ });
186
+
187
+ // --------------- 场景 4:模型配置 / Model Configuration ---------------
188
+
189
+ describe('场景4: 模型配置 / Model Configuration', () => {
190
+ it('GET /api/settings/models 返回当前配置 / returns current model config', async () => {
191
+ const res = await fetch('settings/models');
192
+ expect(res.status).toBe(200);
193
+ const data = res.json();
194
+ // Should have default values
195
+ expect(data.chatModel || data.mode).toBeTruthy();
196
+ });
197
+
198
+ it('GET /api/settings/models/local 检测本地 Ollama / detect local Ollama', async () => {
199
+ const res = await fetch('settings/models/local');
200
+ expect(res.status).toBe(200);
201
+ const data = res.json();
202
+ // Response should indicate Ollama running state and model list
203
+ expect(data).toHaveProperty('running');
204
+ expect(data).toHaveProperty('models');
205
+ });
206
+
207
+ it('默认值正确 / correct defaults: qwen2.5:7b + nomic-embed-text', async () => {
208
+ const res = await fetch('settings/models');
209
+ const data = res.json();
210
+ expect(data.chatModel).toBe('qwen2.5:7b');
211
+ expect(data.embeddingModel).toBe('nomic-embed-text');
212
+ });
213
+
214
+ it('PUT /api/settings/models 保存配置 / saves model config', async () => {
215
+ const res = await fetch('settings/models', 'PUT', {
216
+ mode: 'local',
217
+ provider: 'ollama',
218
+ chatModel: 'llama3:8b',
219
+ embeddingModel: 'nomic-embed-text',
220
+ });
221
+ expect(res.status).toBe(200);
222
+
223
+ // Read back
224
+ const res2 = await fetch('settings/models');
225
+ const data = res2.json();
226
+ expect(data.chatModel).toBe('llama3:8b');
227
+
228
+ // Restore default
229
+ await fetch('settings/models', 'PUT', {
230
+ mode: 'local',
231
+ provider: 'ollama',
232
+ chatModel: 'qwen2.5:7b',
233
+ embeddingModel: 'nomic-embed-text',
234
+ });
235
+ });
236
+
237
+ it('POST /api/settings/models/test 测试连接 / test model connection', async () => {
238
+ const res = await fetch('settings/models/test', 'POST', {
239
+ provider: 'ollama',
240
+ chatModel: 'qwen2.5:7b',
241
+ });
242
+ expect(res.status).toBe(200);
243
+ const data = res.json();
244
+ // Should return success or error, not crash
245
+ expect(data).toHaveProperty('success');
246
+ });
247
+ });
248
+
249
+ // --------------- 场景 5:渠道配置 / Channel Configuration ---------------
250
+
251
+ describe('场景5: 渠道配置 / Channel Configuration', () => {
252
+ it('GET /api/settings/channels 返回渠道列表 / returns channel list', async () => {
253
+ const res = await fetch('settings/channels');
254
+ expect(res.status).toBe(200);
255
+ const data = res.json();
256
+ // Channels endpoint returns data (may be empty if none configured)
257
+ expect(data).toBeTruthy();
258
+ });
259
+
260
+ it('每个渠道有状态标识 / each channel has status', async () => {
261
+ const res = await fetch('settings/channels');
262
+ const data = res.json();
263
+ const list = Array.isArray(data) ? data : (data.channels || []);
264
+ if (list.length > 0) {
265
+ const ch = list[0];
266
+ expect(ch.name || ch.id || ch.type).toBeTruthy();
267
+ }
268
+ });
269
+
270
+ it('PUT /api/settings/channels/:name 保存渠道配置 / save channel config', async () => {
271
+ const res = await fetch('settings/channels/telegram', 'PUT', {
272
+ token: 'test-token-12345',
273
+ enabled: true,
274
+ });
275
+ expect(res.status).toBe(200);
276
+ });
277
+ });
278
+
279
+ // --------------- 场景 6:对话 / Chat ---------------
280
+
281
+ describe('场景6: 对话 / Chat', () => {
282
+ let agentId: string;
283
+
284
+ beforeAll(async () => {
285
+ const res = await fetch('agents', 'POST', { template_id: 'tech-support', name: 'Chat Test Agent' });
286
+ agentId = res.json().id;
287
+ });
288
+
289
+ afterAll(async () => {
290
+ if (agentId) await fetch(`agents/${agentId}`, 'DELETE');
291
+ });
292
+
293
+ it('POST /api/agents/:id/chat 返回响应 / returns chat response', async () => {
294
+ const res = await fetch(`agents/${agentId}/chat`, 'POST', { message: 'Hello!' });
295
+ // SSE or JSON response — should not be 404/500
296
+ expect([200, 201]).toContain(res.status);
297
+ });
298
+
299
+ it('空消息应该报错 / empty message should error', async () => {
300
+ const res = await fetch(`agents/${agentId}/chat`, 'POST', { message: '' });
301
+ // Server may reject or accept — at minimum should not crash (5xx)
302
+ expect(res.status).toBeLessThan(500);
303
+ });
304
+
305
+ it('Agent 不存在应该 404 / non-existent agent returns 404', async () => {
306
+ const res = await fetch('agents/nonexistent-id-12345/chat', 'POST', { message: 'hi' });
307
+ expect(res.status).toBe(404);
308
+ });
309
+ });
310
+
311
+ // --------------- 场景 7:运行状态 / Runtime Status ---------------
312
+
313
+ describe('场景7: 运行状态 / Runtime Status', () => {
314
+ it('GET /api/settings/status 返回运行信息 / returns status info', async () => {
315
+ const res = await fetch('settings/status');
316
+ expect(res.status).toBe(200);
317
+ const data = res.json();
318
+ expect(data).toBeTruthy();
319
+ // Should contain uptime or memory info
320
+ expect(data.uptime !== undefined || data.memory !== undefined || data.modules !== undefined).toBe(true);
321
+ });
322
+ });
323
+
324
+ // --------------- 场景 8:用量统计 / Usage Stats ---------------
325
+
326
+ describe('场景8: 用量统计 / Usage Stats', () => {
327
+ it('GET /api/settings/usage 返回用量数据 / returns usage data', async () => {
328
+ const res = await fetch('settings/usage');
329
+ expect(res.status).toBe(200);
330
+ const data = res.json();
331
+ expect(data).toBeTruthy();
332
+ });
333
+ });
334
+
335
+ // --------------- 场景 9:记忆管理入口 / Memory Management ---------------
336
+
337
+ describe('场景9: 记忆管理入口 / Memory Management Entry', () => {
338
+ it('settings/status 包含 DeepBrain 模块信息 / status includes DeepBrain module', async () => {
339
+ const res = await fetch('settings/status');
340
+ expect(res.status).toBe(200);
341
+ const data = res.json();
342
+ // DeepBrain should be listed in modules
343
+ if (data.modules) {
344
+ const brain = data.modules.find((m: any) => m.name === 'DeepBrain' || m.path === 'brain');
345
+ expect(brain).toBeTruthy();
346
+ }
347
+ });
348
+ });
349
+
350
+ // --------------- 场景 10:角色编辑入口 / Role Editor Entry ---------------
351
+
352
+ describe('场景10: 角色编辑入口 / Role Editor Entry', () => {
353
+ it('settings/status 包含 Workstation 模块信息 / status includes Workstation module', async () => {
354
+ const res = await fetch('settings/status');
355
+ expect(res.status).toBe(200);
356
+ const data = res.json();
357
+ if (data.modules) {
358
+ const ws = data.modules.find((m: any) => m.name === 'Workstation' || m.path === 'workstation');
359
+ expect(ws).toBeTruthy();
360
+ }
361
+ });
362
+ });
363
+
364
+ // --------------- 场景 11:完整流程 / Full E2E Flow ---------------
365
+
366
+ describe('场景11: 完整端到端流程 / Full E2E Flow', () => {
367
+ let agentId: string;
368
+
369
+ it('完整操作链 / full operation chain', async () => {
370
+ // 1. 浏览模板
371
+ const templates = await fetch('templates');
372
+ expect(templates.status).toBe(200);
373
+
374
+ // 2. 创建 Agent
375
+ const create = await fetch('agents', 'POST', { template_id: 'tech-support', name: 'E2E Flow Agent' });
376
+ expect([200, 201]).toContain(create.status);
377
+ agentId = create.json().id;
378
+
379
+ // 3. 查看 Dashboard(agent list)
380
+ const list = await fetch('agents');
381
+ expect(list.status).toBe(200);
382
+ const agents = list.json();
383
+ const agentList = Array.isArray(agents) ? agents : (agents.agents || []);
384
+ expect(agentList.some((a: any) => a.id === agentId)).toBe(true);
385
+
386
+ // 4. 查看模型配置
387
+ const models = await fetch('settings/models');
388
+ expect(models.status).toBe(200);
389
+
390
+ // 5. 查看渠道
391
+ const channels = await fetch('settings/channels');
392
+ expect(channels.status).toBe(200);
393
+
394
+ // 6. 查看状态
395
+ const status = await fetch('settings/status');
396
+ expect(status.status).toBe(200);
397
+
398
+ // 7. 查看用量
399
+ const usage = await fetch('settings/usage');
400
+ expect(usage.status).toBe(200);
401
+
402
+ // Cleanup
403
+ await fetch(`agents/${agentId}`, 'DELETE');
404
+ });
405
+ });
406
+
407
+ // --------------- 场景 12:错误处理 / Error Handling ---------------
408
+
409
+ describe('场景12: 错误处理(小白友好)/ User-Friendly Error Handling', () => {
410
+ it('无效 Agent ID 返回友好错误 / invalid agent ID returns friendly error', async () => {
411
+ const res = await fetch('agents/this-does-not-exist');
412
+ expect(res.status).toBe(404);
413
+ const data = res.json();
414
+ // Should have a message, not a raw stack trace
415
+ expect(data.error || data.message).toBeTruthy();
416
+ expect(res.body).not.toContain('Error:');
417
+ expect(res.body).not.toContain('at Object.');
418
+ });
419
+
420
+ it('缺少必填字段仍能处理 / missing fields handled gracefully', async () => {
421
+ const res = await fetch('agents', 'POST', {});
422
+ // Server should handle it (may create with defaults or reject)
423
+ expect(res.status).toBeLessThan(500);
424
+ });
425
+
426
+ it('模型测试失败返回友好提示 / model test failure gives friendly message', async () => {
427
+ const res = await fetch('settings/models/test', 'POST', {
428
+ provider: 'ollama',
429
+ chatModel: 'nonexistent-model',
430
+ baseUrl: 'http://localhost:99999',
431
+ });
432
+ // Server may return 200 with success:false or 500 — either way should have info
433
+ const data = res.json();
434
+ if (res.status === 200 && !data.success) {
435
+ expect(data.error || data.message).toBeTruthy();
436
+ }
437
+ // If 500, the error should still be parseable JSON (not raw stack)
438
+ if (res.status >= 500) {
439
+ expect(data.error || data.message).toBeTruthy();
440
+ }
441
+ });
442
+ });