opc-agent 4.0.44 → 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 (250) 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/cli.js +2 -2
  9. package/dist/core/runtime.js +18 -0
  10. package/dist/deploy/index.js +56 -56
  11. package/dist/providers/index.js +39 -13
  12. package/dist/studio/server.js +211 -20
  13. package/dist/studio-ui/index.html +279 -24
  14. package/dist/ui/components.js +105 -105
  15. package/examples/README.md +22 -22
  16. package/examples/basic-agent.ts +90 -90
  17. package/examples/brain-integration.ts +71 -71
  18. package/examples/multi-channel.ts +74 -74
  19. package/fix-sidebar.mjs +188 -188
  20. package/install.ps1 +154 -154
  21. package/install.sh +164 -164
  22. package/package.json +1 -1
  23. package/scripts/install.ps1 +31 -31
  24. package/scripts/install.sh +40 -40
  25. package/serve-studio.js +13 -13
  26. package/serve-test.js +25 -25
  27. package/src/channels/dingtalk.ts +46 -46
  28. package/src/channels/email.ts +351 -351
  29. package/src/channels/feishu.ts +349 -349
  30. package/src/channels/googlechat.ts +42 -42
  31. package/src/channels/imessage.ts +31 -31
  32. package/src/channels/irc.ts +82 -82
  33. package/src/channels/line.ts +32 -32
  34. package/src/channels/matrix.ts +33 -33
  35. package/src/channels/mattermost.ts +57 -57
  36. package/src/channels/msteams.ts +32 -32
  37. package/src/channels/nostr.ts +32 -32
  38. package/src/channels/qq.ts +33 -33
  39. package/src/channels/signal.ts +32 -32
  40. package/src/channels/sms.ts +33 -33
  41. package/src/channels/telegram.ts +616 -616
  42. package/src/channels/twitch.ts +65 -65
  43. package/src/channels/voice-call.ts +100 -100
  44. package/src/channels/websocket.ts +399 -399
  45. package/src/channels/wechat.ts +329 -329
  46. package/src/channels/whatsapp.ts +32 -32
  47. package/src/cli/chat.ts +99 -99
  48. package/src/cli/setup.ts +314 -314
  49. package/src/cli.ts +2 -2
  50. package/src/core/agent.ts +476 -476
  51. package/src/core/api-server.ts +277 -277
  52. package/src/core/audio.ts +98 -98
  53. package/src/core/collaboration.ts +275 -275
  54. package/src/core/context-discovery.ts +85 -85
  55. package/src/core/context-refs.ts +140 -140
  56. package/src/core/gateway.ts +106 -106
  57. package/src/core/heartbeat.ts +51 -51
  58. package/src/core/hooks.ts +105 -105
  59. package/src/core/ide-bridge.ts +133 -133
  60. package/src/core/node-network.ts +86 -86
  61. package/src/core/profiles.ts +122 -122
  62. package/src/core/runtime.ts +18 -0
  63. package/src/core/scheduler.ts +187 -187
  64. package/src/core/session-manager.ts +137 -137
  65. package/src/core/subagent.ts +98 -98
  66. package/src/core/vision.ts +180 -180
  67. package/src/core/workflow-graph.ts +365 -365
  68. package/src/daemon.ts +96 -96
  69. package/src/deploy/index.ts +255 -255
  70. package/src/doctor.ts +156 -156
  71. package/src/eval/index.ts +211 -211
  72. package/src/eval/suites/basic.json +16 -16
  73. package/src/eval/suites/memory.json +12 -12
  74. package/src/eval/suites/safety.json +14 -14
  75. package/src/hub/brain-seed.ts +54 -54
  76. package/src/hub/client.ts +60 -60
  77. package/src/mcp/servers/calculator-mcp.ts +65 -65
  78. package/src/mcp/servers/crypto-mcp.ts +73 -73
  79. package/src/mcp/servers/database-mcp.ts +72 -72
  80. package/src/mcp/servers/datetime-mcp.ts +69 -69
  81. package/src/mcp/servers/filesystem.ts +66 -66
  82. package/src/mcp/servers/github-mcp.ts +58 -58
  83. package/src/mcp/servers/index.ts +63 -63
  84. package/src/mcp/servers/json-mcp.ts +102 -102
  85. package/src/mcp/servers/memory-mcp.ts +56 -56
  86. package/src/mcp/servers/regex-mcp.ts +53 -53
  87. package/src/mcp/servers/web-mcp.ts +49 -49
  88. package/src/memory/context-compressor.ts +189 -189
  89. package/src/memory/seed-loader.ts +212 -212
  90. package/src/memory/user-profiler.ts +215 -215
  91. package/src/plugins/content-filter.ts +23 -23
  92. package/src/plugins/logger.ts +18 -18
  93. package/src/plugins/rate-limiter.ts +38 -38
  94. package/src/protocols/a2a/client.ts +132 -132
  95. package/src/protocols/a2a/index.ts +8 -8
  96. package/src/protocols/a2a/server.ts +333 -333
  97. package/src/protocols/a2a/types.ts +88 -88
  98. package/src/protocols/a2a/utils.ts +50 -50
  99. package/src/protocols/agui/client.ts +83 -83
  100. package/src/protocols/agui/index.ts +4 -4
  101. package/src/protocols/agui/server.ts +218 -218
  102. package/src/protocols/agui/types.ts +153 -153
  103. package/src/protocols/index.ts +2 -2
  104. package/src/protocols/mcp/agent-tools.ts +134 -134
  105. package/src/protocols/mcp/index.ts +8 -8
  106. package/src/protocols/mcp/server.ts +262 -262
  107. package/src/protocols/mcp/types.ts +69 -69
  108. package/src/providers/index.ts +632 -608
  109. package/src/publish/index.ts +376 -376
  110. package/src/scheduler/cron-engine.ts +191 -191
  111. package/src/scheduler/index.ts +2 -2
  112. package/src/schema/oad.ts +217 -217
  113. package/src/security/approval.ts +131 -131
  114. package/src/security/approvals.ts +143 -143
  115. package/src/security/elevated.ts +105 -105
  116. package/src/security/guardrails.ts +248 -248
  117. package/src/security/index.ts +9 -9
  118. package/src/security/keys.ts +87 -87
  119. package/src/security/secrets.ts +129 -129
  120. package/src/skills/builtin/index.ts +408 -408
  121. package/src/skills/marketplace.ts +113 -113
  122. package/src/skills/types.ts +42 -42
  123. package/src/studio/server.ts +209 -22
  124. package/src/studio/templates-data.ts +178 -178
  125. package/src/studio-ui/index.html +279 -24
  126. package/src/telemetry/index.ts +324 -324
  127. package/src/tools/builtin/browser.ts +299 -299
  128. package/src/tools/builtin/datetime.ts +41 -41
  129. package/src/tools/builtin/file.ts +107 -107
  130. package/src/tools/builtin/home-assistant.ts +116 -116
  131. package/src/tools/builtin/rl-tools.ts +243 -243
  132. package/src/tools/builtin/shell.ts +43 -43
  133. package/src/tools/builtin/vision.ts +64 -64
  134. package/src/tools/builtin/web-search.ts +126 -126
  135. package/src/tools/builtin/web.ts +35 -35
  136. package/src/tools/document-processor.ts +213 -213
  137. package/src/tools/image-generator.ts +150 -150
  138. package/src/tools/integrations/calendar.ts +73 -73
  139. package/src/tools/integrations/code-exec.ts +39 -39
  140. package/src/tools/integrations/csv-analyzer.ts +92 -92
  141. package/src/tools/integrations/database.ts +44 -44
  142. package/src/tools/integrations/email-send.ts +76 -76
  143. package/src/tools/integrations/git-tool.ts +42 -42
  144. package/src/tools/integrations/github-tool.ts +76 -76
  145. package/src/tools/integrations/image-gen.ts +56 -56
  146. package/src/tools/integrations/index.ts +92 -92
  147. package/src/tools/integrations/jira.ts +83 -83
  148. package/src/tools/integrations/notion.ts +71 -71
  149. package/src/tools/integrations/npm-tool.ts +48 -48
  150. package/src/tools/integrations/pdf-reader.ts +58 -58
  151. package/src/tools/integrations/slack.ts +65 -65
  152. package/src/tools/integrations/summarizer.ts +49 -49
  153. package/src/tools/integrations/translator.ts +48 -48
  154. package/src/tools/integrations/trello.ts +60 -60
  155. package/src/tools/integrations/vector-search.ts +42 -42
  156. package/src/tools/integrations/web-scraper.ts +47 -47
  157. package/src/tools/integrations/web-search.ts +58 -58
  158. package/src/tools/integrations/webhook.ts +38 -38
  159. package/src/tools/mcp-client.ts +131 -131
  160. package/src/tools/web-scraper.ts +179 -179
  161. package/src/tools/web-search.ts +180 -180
  162. package/src/ui/components.ts +127 -127
  163. package/srv-out.txt +1 -1
  164. package/templates/ecommerce-assistant/README.md +45 -45
  165. package/templates/ecommerce-assistant/oad.yaml +47 -47
  166. package/templates/tech-support/README.md +43 -43
  167. package/templates/tech-support/oad.yaml +45 -45
  168. package/test-agent/Dockerfile +9 -9
  169. package/test-agent/README.md +50 -50
  170. package/test-agent/agent.yaml +23 -23
  171. package/test-agent/docker-compose.yml +11 -11
  172. package/test-agent/oad.yaml +31 -31
  173. package/test-agent/package-lock.json +1492 -1492
  174. package/test-agent/package.json +17 -17
  175. package/test-agent/src/index.ts +24 -24
  176. package/test-agent/src/skills/echo.ts +15 -15
  177. package/test-agent/tsconfig.json +24 -24
  178. package/test-full.js +43 -43
  179. package/test-sidebar.js +22 -22
  180. package/test-studio3.js +75 -75
  181. package/test-studio4.js +41 -41
  182. package/tests/a2a-protocol.test.ts +285 -285
  183. package/tests/agui-protocol.test.ts +246 -246
  184. package/tests/api-server.test.ts +148 -148
  185. package/tests/approvals.test.ts +89 -89
  186. package/tests/audio.test.ts +40 -40
  187. package/tests/brain-seed-extended.test.ts +490 -490
  188. package/tests/brain-seed.test.ts +239 -239
  189. package/tests/browser.test.ts +179 -179
  190. package/tests/channels/discord.test.ts +79 -79
  191. package/tests/channels/email.test.ts +148 -148
  192. package/tests/channels/feishu.test.ts +123 -123
  193. package/tests/channels/telegram.test.ts +129 -129
  194. package/tests/channels/websocket.test.ts +53 -53
  195. package/tests/channels/wechat.test.ts +170 -170
  196. package/tests/channels-extra.test.ts +45 -45
  197. package/tests/chat-cli.test.ts +160 -160
  198. package/tests/cli.test.ts +46 -46
  199. package/tests/context-compressor.test.ts +172 -172
  200. package/tests/context-refs.test.ts +121 -121
  201. package/tests/cron-engine.test.ts +101 -101
  202. package/tests/daemon.test.ts +135 -135
  203. package/tests/deepbrain-wire.test.ts +234 -234
  204. package/tests/deploy-and-dag.test.ts +196 -196
  205. package/tests/doctor.test.ts +38 -38
  206. package/tests/document-processor.test.ts +69 -69
  207. package/tests/e2e-nocode.test.ts +442 -442
  208. package/tests/elevated.test.ts +69 -69
  209. package/tests/eval.test.ts +173 -173
  210. package/tests/gateway.test.ts +63 -63
  211. package/tests/guardrails.test.ts +177 -177
  212. package/tests/home-assistant.test.ts +40 -40
  213. package/tests/hooks.test.ts +79 -79
  214. package/tests/ide-bridge.test.ts +38 -38
  215. package/tests/image-generator.test.ts +84 -84
  216. package/tests/init-role.test.ts +124 -124
  217. package/tests/integrations.test.ts +249 -249
  218. package/tests/mcp-client.test.ts +92 -92
  219. package/tests/mcp-server.test.ts +178 -178
  220. package/tests/mcp-servers.test.ts +260 -260
  221. package/tests/node-network.test.ts +74 -74
  222. package/tests/plugin-a2a-enhanced.test.ts +230 -230
  223. package/tests/profiles.test.ts +61 -61
  224. package/tests/publish.test.ts +231 -231
  225. package/tests/rl-tools.test.ts +93 -93
  226. package/tests/sandbox-manager.test.ts +46 -46
  227. package/tests/scheduler.test.ts +200 -200
  228. package/tests/secrets.test.ts +107 -107
  229. package/tests/security-enhanced.test.ts +233 -233
  230. package/tests/settings-api.test.ts +148 -148
  231. package/tests/setup.test.ts +73 -73
  232. package/tests/subagent.test.ts +193 -193
  233. package/tests/telegram-discord.test.ts +60 -60
  234. package/tests/telemetry.test.ts +186 -186
  235. package/tests/user-profiler.test.ts +169 -169
  236. package/tests/v090-features.test.ts +254 -254
  237. package/tests/vision.test.ts +61 -61
  238. package/tests/voice-call.test.ts +47 -47
  239. package/tests/voice-enhanced.test.ts +169 -169
  240. package/tests/voice-interaction.test.ts +38 -38
  241. package/tests/web-search.test.ts +155 -155
  242. package/tests/workflow-graph.test.ts +279 -279
  243. package/tutorial/customer-service-agent/README.md +612 -612
  244. package/tutorial/customer-service-agent/SOUL.md +26 -26
  245. package/tutorial/customer-service-agent/agent.yaml +63 -63
  246. package/tutorial/customer-service-agent/package.json +19 -19
  247. package/tutorial/customer-service-agent/src/index.ts +69 -69
  248. package/tutorial/customer-service-agent/src/skills/faq.ts +27 -27
  249. package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -22
  250. 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
+ });