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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +20 -20
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -14
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -13
- package/CHANGELOG.md +48 -48
- package/CONTRIBUTING.md +36 -36
- package/README.zh-CN.md +497 -497
- package/USABILITY-ISSUES.md +73 -0
- package/dist/channels/web.js +8 -2
- package/dist/channels/wechat.js +6 -6
- package/dist/cli.js +200 -85
- package/dist/core/runtime.js +37 -15
- package/dist/deploy/index.js +56 -56
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +105 -10
- package/dist/memory/deepbrain.d.ts +1 -1
- package/dist/memory/deepbrain.js +95 -4
- package/dist/scheduler/cron-engine.js +3 -36
- package/dist/studio/server.js +30 -1
- package/dist/studio-ui/index.html +230 -10
- package/dist/ui/components.js +105 -105
- package/examples/README.md +22 -22
- package/examples/basic-agent.ts +90 -90
- package/examples/brain-integration.ts +71 -71
- package/examples/multi-channel.ts +74 -74
- package/fix-sidebar.mjs +188 -188
- package/install.ps1 +154 -154
- package/install.sh +164 -164
- package/package.json +1 -1
- package/scripts/install.ps1 +31 -31
- package/scripts/install.sh +40 -40
- package/serve-studio.js +13 -13
- package/serve-test.js +25 -25
- package/src/channels/dingtalk.ts +46 -46
- package/src/channels/email.ts +351 -351
- package/src/channels/feishu.ts +349 -349
- package/src/channels/googlechat.ts +42 -42
- package/src/channels/imessage.ts +31 -31
- package/src/channels/irc.ts +82 -82
- package/src/channels/line.ts +32 -32
- package/src/channels/matrix.ts +33 -33
- package/src/channels/mattermost.ts +57 -57
- package/src/channels/msteams.ts +32 -32
- package/src/channels/nostr.ts +32 -32
- package/src/channels/qq.ts +33 -33
- package/src/channels/signal.ts +32 -32
- package/src/channels/sms.ts +33 -33
- package/src/channels/telegram.ts +616 -616
- package/src/channels/twitch.ts +65 -65
- package/src/channels/voice-call.ts +100 -100
- package/src/channels/web.ts +8 -2
- package/src/channels/websocket.ts +399 -399
- package/src/channels/wechat.ts +329 -329
- package/src/channels/whatsapp.ts +32 -32
- package/src/cli/chat.ts +99 -99
- package/src/cli/setup.ts +314 -314
- package/src/cli.ts +195 -92
- package/src/core/agent.ts +476 -476
- package/src/core/api-server.ts +277 -277
- package/src/core/audio.ts +98 -98
- package/src/core/collaboration.ts +275 -275
- package/src/core/context-discovery.ts +85 -85
- package/src/core/context-refs.ts +140 -140
- package/src/core/gateway.ts +106 -106
- package/src/core/heartbeat.ts +51 -51
- package/src/core/hooks.ts +105 -105
- package/src/core/ide-bridge.ts +133 -133
- package/src/core/node-network.ts +86 -86
- package/src/core/profiles.ts +122 -122
- package/src/core/runtime.ts +25 -0
- package/src/core/scheduler.ts +187 -187
- package/src/core/session-manager.ts +137 -137
- package/src/core/subagent.ts +98 -98
- package/src/core/vision.ts +180 -180
- package/src/core/workflow-graph.ts +365 -365
- package/src/daemon.ts +96 -96
- package/src/deploy/index.ts +255 -255
- package/src/doctor.ts +98 -11
- package/src/eval/index.ts +211 -211
- package/src/eval/suites/basic.json +16 -16
- package/src/eval/suites/memory.json +12 -12
- package/src/eval/suites/safety.json +14 -14
- package/src/hub/brain-seed.ts +54 -54
- package/src/hub/client.ts +60 -60
- package/src/mcp/servers/calculator-mcp.ts +65 -65
- package/src/mcp/servers/crypto-mcp.ts +73 -73
- package/src/mcp/servers/database-mcp.ts +72 -72
- package/src/mcp/servers/datetime-mcp.ts +69 -69
- package/src/mcp/servers/filesystem.ts +66 -66
- package/src/mcp/servers/github-mcp.ts +58 -58
- package/src/mcp/servers/index.ts +63 -63
- package/src/mcp/servers/json-mcp.ts +102 -102
- package/src/mcp/servers/memory-mcp.ts +56 -56
- package/src/mcp/servers/regex-mcp.ts +53 -53
- package/src/mcp/servers/web-mcp.ts +49 -49
- package/src/memory/context-compressor.ts +189 -189
- package/src/memory/deepbrain.ts +99 -5
- package/src/memory/seed-loader.ts +212 -212
- package/src/memory/user-profiler.ts +215 -215
- package/src/plugins/content-filter.ts +23 -23
- package/src/plugins/logger.ts +18 -18
- package/src/plugins/rate-limiter.ts +38 -38
- package/src/protocols/a2a/client.ts +132 -132
- package/src/protocols/a2a/index.ts +8 -8
- package/src/protocols/a2a/server.ts +333 -333
- package/src/protocols/a2a/types.ts +88 -88
- package/src/protocols/a2a/utils.ts +50 -50
- package/src/protocols/agui/client.ts +83 -83
- package/src/protocols/agui/index.ts +4 -4
- package/src/protocols/agui/server.ts +218 -218
- package/src/protocols/agui/types.ts +153 -153
- package/src/protocols/index.ts +2 -2
- package/src/protocols/mcp/agent-tools.ts +134 -134
- package/src/protocols/mcp/index.ts +8 -8
- package/src/protocols/mcp/server.ts +262 -262
- package/src/protocols/mcp/types.ts +69 -69
- package/src/providers/index.ts +632 -632
- package/src/publish/index.ts +376 -376
- package/src/scheduler/cron-engine.ts +191 -191
- package/src/scheduler/index.ts +2 -2
- package/src/schema/oad.ts +217 -217
- package/src/security/approval.ts +131 -131
- package/src/security/approvals.ts +143 -143
- package/src/security/elevated.ts +105 -105
- package/src/security/guardrails.ts +248 -248
- package/src/security/index.ts +9 -9
- package/src/security/keys.ts +87 -87
- package/src/security/secrets.ts +129 -129
- package/src/skills/builtin/index.ts +408 -408
- package/src/skills/marketplace.ts +113 -113
- package/src/skills/types.ts +42 -42
- package/src/studio/server.ts +31 -1
- package/src/studio/templates-data.ts +178 -178
- package/src/studio-ui/index.html +230 -10
- package/src/telemetry/index.ts +324 -324
- package/src/tools/builtin/browser.ts +299 -299
- package/src/tools/builtin/datetime.ts +41 -41
- package/src/tools/builtin/file.ts +107 -107
- package/src/tools/builtin/home-assistant.ts +116 -116
- package/src/tools/builtin/rl-tools.ts +243 -243
- package/src/tools/builtin/shell.ts +43 -43
- package/src/tools/builtin/vision.ts +64 -64
- package/src/tools/builtin/web-search.ts +126 -126
- package/src/tools/builtin/web.ts +35 -35
- package/src/tools/document-processor.ts +213 -213
- package/src/tools/image-generator.ts +150 -150
- package/src/tools/integrations/calendar.ts +73 -73
- package/src/tools/integrations/code-exec.ts +39 -39
- package/src/tools/integrations/csv-analyzer.ts +92 -92
- package/src/tools/integrations/database.ts +44 -44
- package/src/tools/integrations/email-send.ts +76 -76
- package/src/tools/integrations/git-tool.ts +42 -42
- package/src/tools/integrations/github-tool.ts +76 -76
- package/src/tools/integrations/image-gen.ts +56 -56
- package/src/tools/integrations/index.ts +92 -92
- package/src/tools/integrations/jira.ts +83 -83
- package/src/tools/integrations/notion.ts +71 -71
- package/src/tools/integrations/npm-tool.ts +48 -48
- package/src/tools/integrations/pdf-reader.ts +58 -58
- package/src/tools/integrations/slack.ts +65 -65
- package/src/tools/integrations/summarizer.ts +49 -49
- package/src/tools/integrations/translator.ts +48 -48
- package/src/tools/integrations/trello.ts +60 -60
- package/src/tools/integrations/vector-search.ts +42 -42
- package/src/tools/integrations/web-scraper.ts +47 -47
- package/src/tools/integrations/web-search.ts +58 -58
- package/src/tools/integrations/webhook.ts +38 -38
- package/src/tools/mcp-client.ts +131 -131
- package/src/tools/web-scraper.ts +179 -179
- package/src/tools/web-search.ts +180 -180
- package/src/ui/components.ts +127 -127
- package/srv-out.txt +1 -1
- package/templates/ecommerce-assistant/README.md +45 -45
- package/templates/ecommerce-assistant/oad.yaml +47 -47
- package/templates/tech-support/README.md +43 -43
- package/templates/tech-support/oad.yaml +45 -45
- package/test-agent/Dockerfile +9 -9
- package/test-agent/README.md +50 -50
- package/test-agent/agent.yaml +23 -23
- package/test-agent/docker-compose.yml +11 -11
- package/test-agent/oad.yaml +31 -31
- package/test-agent/package-lock.json +1492 -1492
- package/test-agent/package.json +17 -17
- package/test-agent/src/index.ts +24 -24
- package/test-agent/src/skills/echo.ts +15 -15
- package/test-agent/tsconfig.json +24 -24
- package/test-full.js +43 -43
- package/test-sidebar.js +22 -22
- package/test-studio3.js +75 -75
- package/test-studio4.js +41 -41
- package/tests/a2a-protocol.test.ts +285 -285
- package/tests/agui-protocol.test.ts +246 -246
- package/tests/api-server.test.ts +148 -148
- package/tests/approvals.test.ts +89 -89
- package/tests/audio.test.ts +40 -40
- package/tests/brain-seed-extended.test.ts +490 -490
- package/tests/brain-seed.test.ts +239 -239
- package/tests/browser.test.ts +179 -179
- package/tests/channels/discord.test.ts +79 -79
- package/tests/channels/email.test.ts +148 -148
- package/tests/channels/feishu.test.ts +123 -123
- package/tests/channels/telegram.test.ts +129 -129
- package/tests/channels/websocket.test.ts +53 -53
- package/tests/channels/wechat.test.ts +170 -170
- package/tests/channels-extra.test.ts +45 -45
- package/tests/chat-cli.test.ts +160 -160
- package/tests/cli.test.ts +46 -46
- package/tests/context-compressor.test.ts +172 -172
- package/tests/context-refs.test.ts +121 -121
- package/tests/cron-engine.test.ts +101 -101
- package/tests/daemon.test.ts +135 -135
- package/tests/deepbrain-wire.test.ts +234 -234
- package/tests/deploy-and-dag.test.ts +196 -196
- package/tests/doctor.test.ts +38 -38
- package/tests/document-processor.test.ts +69 -69
- package/tests/e2e-nocode.test.ts +442 -442
- package/tests/elevated.test.ts +69 -69
- package/tests/eval.test.ts +173 -173
- package/tests/gateway.test.ts +63 -63
- package/tests/guardrails.test.ts +177 -177
- package/tests/home-assistant.test.ts +40 -40
- package/tests/hooks.test.ts +79 -79
- package/tests/ide-bridge.test.ts +38 -38
- package/tests/image-generator.test.ts +84 -84
- package/tests/init-role.test.ts +124 -124
- package/tests/integrations.test.ts +249 -249
- package/tests/mcp-client.test.ts +92 -92
- package/tests/mcp-server.test.ts +178 -178
- package/tests/mcp-servers.test.ts +260 -260
- package/tests/node-network.test.ts +74 -74
- package/tests/plugin-a2a-enhanced.test.ts +230 -230
- package/tests/profiles.test.ts +61 -61
- package/tests/publish.test.ts +231 -231
- package/tests/rl-tools.test.ts +93 -93
- package/tests/sandbox-manager.test.ts +46 -46
- package/tests/scheduler.test.ts +200 -200
- package/tests/secrets.test.ts +107 -107
- package/tests/security-enhanced.test.ts +233 -233
- package/tests/settings-api.test.ts +148 -148
- package/tests/setup.test.ts +73 -73
- package/tests/subagent.test.ts +193 -193
- package/tests/telegram-discord.test.ts +60 -60
- package/tests/telemetry.test.ts +186 -186
- package/tests/user-profiler.test.ts +169 -169
- package/tests/v090-features.test.ts +254 -254
- package/tests/vision.test.ts +61 -61
- package/tests/voice-call.test.ts +47 -47
- package/tests/voice-enhanced.test.ts +169 -169
- package/tests/voice-interaction.test.ts +38 -38
- package/tests/web-search.test.ts +155 -155
- package/tests/workflow-graph.test.ts +279 -279
- package/tutorial/customer-service-agent/README.md +612 -612
- package/tutorial/customer-service-agent/SOUL.md +26 -26
- package/tutorial/customer-service-agent/agent.yaml +63 -63
- package/tutorial/customer-service-agent/package.json +19 -19
- package/tutorial/customer-service-agent/src/index.ts +69 -69
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -27
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -22
- package/tutorial/customer-service-agent/tsconfig.json +14 -14
package/tests/e2e-nocode.test.ts
CHANGED
|
@@ -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
|
+
});
|