opc-agent 4.1.0 → 4.1.2

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