@lobehub/lobehub 2.0.0-next.266 → 2.0.0-next.267
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/CHANGELOG.md +25 -0
- package/changelog/v1.json +5 -0
- package/e2e/CLAUDE.md +34 -73
- package/e2e/docs/local-setup.md +67 -219
- package/e2e/scripts/setup.ts +529 -0
- package/e2e/src/features/home/sidebarAgent.feature +62 -0
- package/e2e/src/features/home/sidebarGroup.feature +62 -0
- package/e2e/src/steps/home/sidebarAgent.steps.ts +373 -0
- package/e2e/src/steps/home/sidebarGroup.steps.ts +168 -0
- package/e2e/src/steps/hooks.ts +2 -0
- package/package.json +3 -3
- package/packages/utils/src/multimodalContent.test.ts +302 -0
- package/packages/utils/src/server/__tests__/sse.test.ts +353 -0
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/Editing.tsx +4 -11
- package/src/app/[variants]/(main)/home/_layout/Body/Agent/List/AgentGroupItem/index.tsx +3 -3
- package/src/features/ChatInput/ActionBar/Params/Controls.tsx +42 -7
- package/src/store/home/slices/sidebarUI/action.ts +9 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* E2E Test Environment Setup Script
|
|
4
|
+
*
|
|
5
|
+
* One-click setup for E2E testing environment.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* bun e2e/scripts/setup.ts [options]
|
|
9
|
+
*
|
|
10
|
+
* Options:
|
|
11
|
+
* --clean Clean up existing containers and processes
|
|
12
|
+
* --skip-db Skip database setup (use existing)
|
|
13
|
+
* --skip-migrate Skip database migration
|
|
14
|
+
* --build Build the application before starting
|
|
15
|
+
* --start Start the server after setup
|
|
16
|
+
* --port <port> Server port (default: 3006)
|
|
17
|
+
* --help Show help message
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { type ChildProcess, spawn, spawnSync } from 'node:child_process';
|
|
21
|
+
import { existsSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { resolve } from 'node:path';
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Configuration
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
const CONFIG = {
|
|
29
|
+
containerName: 'postgres-e2e',
|
|
30
|
+
databaseDriver: 'node',
|
|
31
|
+
databaseUrl: 'postgresql://postgres:postgres@localhost:5433/postgres',
|
|
32
|
+
dbPort: 5433,
|
|
33
|
+
defaultPort: 3006,
|
|
34
|
+
dockerImage: 'paradedb/paradedb:latest',
|
|
35
|
+
projectRoot: resolve(__dirname, '../..'),
|
|
36
|
+
serverTimeout: 120_000, // 2 minutes
|
|
37
|
+
|
|
38
|
+
// Secrets (for e2e testing only)
|
|
39
|
+
secrets: {
|
|
40
|
+
betterAuthSecret: 'e2e-test-secret-key-for-better-auth-32chars!',
|
|
41
|
+
keyVaultsSecret: 'LA7n9k3JdEcbSgml2sxfw+4TV1AzaaFU5+R176aQz4s=',
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// S3 Mock (required even if not testing file uploads)
|
|
45
|
+
s3Mock: {
|
|
46
|
+
accessKeyId: 'e2e-mock-access-key',
|
|
47
|
+
bucket: 'e2e-mock-bucket',
|
|
48
|
+
endpoint: 'https://e2e-mock-s3.localhost',
|
|
49
|
+
secretAccessKey: 'e2e-mock-secret-key',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Utilities
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
const colors = {
|
|
58
|
+
cyan: (s: string) => `\x1b[36m${s}\x1b[0m`,
|
|
59
|
+
dim: (s: string) => `\x1b[2m${s}\x1b[0m`,
|
|
60
|
+
green: (s: string) => `\x1b[32m${s}\x1b[0m`,
|
|
61
|
+
red: (s: string) => `\x1b[31m${s}\x1b[0m`,
|
|
62
|
+
yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function log(emoji: string, message: string) {
|
|
66
|
+
console.log(`${emoji} ${message}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function logStep(step: number, total: number, message: string) {
|
|
70
|
+
console.log(`\n${colors.cyan(`[${step}/${total}]`)} ${message}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function exec(
|
|
74
|
+
command: string,
|
|
75
|
+
args: string[] = [],
|
|
76
|
+
options: { cwd?: string; silent?: boolean } = {}
|
|
77
|
+
) {
|
|
78
|
+
const { cwd = CONFIG.projectRoot, silent = false } = options;
|
|
79
|
+
const result = spawnSync(command, args, {
|
|
80
|
+
cwd,
|
|
81
|
+
encoding: 'utf-8',
|
|
82
|
+
shell: true,
|
|
83
|
+
stdio: silent ? 'pipe' : 'inherit',
|
|
84
|
+
});
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function execAsync(
|
|
89
|
+
command: string,
|
|
90
|
+
args: string[] = [],
|
|
91
|
+
env: Record<string, string> = {}
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const child = spawn(command, args, {
|
|
95
|
+
cwd: CONFIG.projectRoot,
|
|
96
|
+
env: { ...process.env, ...env },
|
|
97
|
+
shell: true,
|
|
98
|
+
stdio: 'inherit',
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
child.on('close', (code) => {
|
|
102
|
+
if (code === 0) {
|
|
103
|
+
resolve();
|
|
104
|
+
} else {
|
|
105
|
+
reject(new Error(`Command failed with code ${code}`));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
child.on('error', reject);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function sleep(ms: number): Promise<void> {
|
|
114
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function waitForCondition(
|
|
118
|
+
check: () => Promise<boolean>,
|
|
119
|
+
timeout: number,
|
|
120
|
+
interval: number = 1000,
|
|
121
|
+
onWait?: () => void
|
|
122
|
+
): Promise<boolean> {
|
|
123
|
+
const startTime = Date.now();
|
|
124
|
+
while (Date.now() - startTime < timeout) {
|
|
125
|
+
if (await check()) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
onWait?.();
|
|
129
|
+
await sleep(interval);
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ============================================================================
|
|
135
|
+
// Docker Operations
|
|
136
|
+
// ============================================================================
|
|
137
|
+
|
|
138
|
+
function isDockerRunning(): boolean {
|
|
139
|
+
const result = exec('docker', ['info'], { silent: true });
|
|
140
|
+
return result.status === 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isContainerRunning(name: string): boolean {
|
|
144
|
+
const result = exec('docker', ['ps', '-q', '-f', `name=${name}`], { silent: true });
|
|
145
|
+
return !!result.stdout?.trim();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function containerExists(name: string): boolean {
|
|
149
|
+
const result = exec('docker', ['ps', '-aq', '-f', `name=${name}`], { silent: true });
|
|
150
|
+
return !!result.stdout?.trim();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function stopContainer(name: string): void {
|
|
154
|
+
if (isContainerRunning(name)) {
|
|
155
|
+
log('🛑', `Stopping container: ${name}`);
|
|
156
|
+
exec('docker', ['stop', name], { silent: true });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function removeContainer(name: string): void {
|
|
161
|
+
if (containerExists(name)) {
|
|
162
|
+
log('🗑️ ', `Removing container: ${name}`);
|
|
163
|
+
exec('docker', ['rm', name], { silent: true });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function startPostgres(): Promise<void> {
|
|
168
|
+
// Check Docker is running
|
|
169
|
+
if (!isDockerRunning()) {
|
|
170
|
+
throw new Error('Docker is not running. Please start Docker Desktop first.');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (isContainerRunning(CONFIG.containerName)) {
|
|
174
|
+
log('✅', 'PostgreSQL container is already running');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Remove existing container if exists
|
|
179
|
+
removeContainer(CONFIG.containerName);
|
|
180
|
+
|
|
181
|
+
log('🐘', 'Starting PostgreSQL container...');
|
|
182
|
+
const result = exec('docker', [
|
|
183
|
+
'run',
|
|
184
|
+
'-d',
|
|
185
|
+
'--name',
|
|
186
|
+
CONFIG.containerName,
|
|
187
|
+
'-e',
|
|
188
|
+
'POSTGRES_PASSWORD=postgres',
|
|
189
|
+
'-p',
|
|
190
|
+
`${CONFIG.dbPort}:5432`,
|
|
191
|
+
CONFIG.dockerImage,
|
|
192
|
+
]);
|
|
193
|
+
|
|
194
|
+
if (result.status !== 0) {
|
|
195
|
+
throw new Error('Failed to start PostgreSQL container');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Wait for database to be ready
|
|
199
|
+
process.stdout.write(' Waiting for PostgreSQL to be ready');
|
|
200
|
+
const isReady = await waitForCondition(
|
|
201
|
+
async () => {
|
|
202
|
+
const result = exec('docker', ['exec', CONFIG.containerName, 'pg_isready'], { silent: true });
|
|
203
|
+
return result.status === 0;
|
|
204
|
+
},
|
|
205
|
+
30_000,
|
|
206
|
+
2000,
|
|
207
|
+
() => process.stdout.write('.')
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
console.log();
|
|
211
|
+
|
|
212
|
+
if (!isReady) {
|
|
213
|
+
throw new Error('PostgreSQL failed to start within 30 seconds');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
log('✅', 'PostgreSQL is ready');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// Process Management
|
|
221
|
+
// ============================================================================
|
|
222
|
+
|
|
223
|
+
function killProcessOnPort(port: number): void {
|
|
224
|
+
const result = exec('lsof', ['-ti', `:${port}`], { silent: true });
|
|
225
|
+
const pids = result.stdout?.trim();
|
|
226
|
+
if (pids) {
|
|
227
|
+
log('🔪', `Killing processes on port ${port}`);
|
|
228
|
+
for (const pid of pids.split('\n')) {
|
|
229
|
+
if (pid) {
|
|
230
|
+
exec('kill', ['-9', pid], { silent: true });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// Database Operations
|
|
238
|
+
// ============================================================================
|
|
239
|
+
|
|
240
|
+
async function runMigration(): Promise<void> {
|
|
241
|
+
log('🔄', 'Running database migration...');
|
|
242
|
+
|
|
243
|
+
await execAsync('bun', ['run', 'db:migrate'], {
|
|
244
|
+
DATABASE_DRIVER: CONFIG.databaseDriver,
|
|
245
|
+
DATABASE_URL: CONFIG.databaseUrl,
|
|
246
|
+
KEY_VAULTS_SECRET: CONFIG.secrets.keyVaultsSecret,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
log('✅', 'Database migration completed');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============================================================================
|
|
253
|
+
// Build Operations
|
|
254
|
+
// ============================================================================
|
|
255
|
+
|
|
256
|
+
async function buildApp(): Promise<void> {
|
|
257
|
+
log('🔨', 'Building application (this may take a few minutes)...');
|
|
258
|
+
|
|
259
|
+
await execAsync('bun', ['run', 'build'], {
|
|
260
|
+
BETTER_AUTH_SECRET: CONFIG.secrets.betterAuthSecret,
|
|
261
|
+
DATABASE_DRIVER: CONFIG.databaseDriver,
|
|
262
|
+
DATABASE_URL: CONFIG.databaseUrl,
|
|
263
|
+
KEY_VAULTS_SECRET: CONFIG.secrets.keyVaultsSecret,
|
|
264
|
+
NEXT_PUBLIC_ENABLE_BETTER_AUTH: '1',
|
|
265
|
+
SKIP_LINT: '1',
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
log('✅', 'Application built successfully');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ============================================================================
|
|
272
|
+
// Server Operations
|
|
273
|
+
// ============================================================================
|
|
274
|
+
|
|
275
|
+
async function isServerRunning(port: number): Promise<boolean> {
|
|
276
|
+
try {
|
|
277
|
+
const response = await fetch(`http://localhost:${port}/chat`, { method: 'HEAD' });
|
|
278
|
+
return response.ok;
|
|
279
|
+
} catch {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function getServerEnv(port: number): Record<string, string> {
|
|
285
|
+
return {
|
|
286
|
+
BETTER_AUTH_SECRET: CONFIG.secrets.betterAuthSecret,
|
|
287
|
+
DATABASE_DRIVER: CONFIG.databaseDriver,
|
|
288
|
+
DATABASE_URL: CONFIG.databaseUrl,
|
|
289
|
+
KEY_VAULTS_SECRET: CONFIG.secrets.keyVaultsSecret,
|
|
290
|
+
NEXT_PUBLIC_AUTH_EMAIL_VERIFICATION: '0',
|
|
291
|
+
NEXT_PUBLIC_ENABLE_BETTER_AUTH: '1',
|
|
292
|
+
NODE_OPTIONS: '--max-old-space-size=6144',
|
|
293
|
+
PORT: String(port),
|
|
294
|
+
S3_ACCESS_KEY_ID: CONFIG.s3Mock.accessKeyId,
|
|
295
|
+
S3_BUCKET: CONFIG.s3Mock.bucket,
|
|
296
|
+
S3_ENDPOINT: CONFIG.s3Mock.endpoint,
|
|
297
|
+
S3_SECRET_ACCESS_KEY: CONFIG.s3Mock.secretAccessKey,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function startServer(port: number): Promise<void> {
|
|
302
|
+
if (await isServerRunning(port)) {
|
|
303
|
+
log('✅', `Server is already running on port ${port}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Kill any process on the port first
|
|
308
|
+
killProcessOnPort(port);
|
|
309
|
+
|
|
310
|
+
log('🚀', `Starting server on port ${port}...`);
|
|
311
|
+
|
|
312
|
+
const env = getServerEnv(port);
|
|
313
|
+
|
|
314
|
+
// Start server in background
|
|
315
|
+
const child = spawn('bunx', ['next', 'start', '-p', String(port)], {
|
|
316
|
+
cwd: CONFIG.projectRoot,
|
|
317
|
+
detached: true,
|
|
318
|
+
env: { ...process.env, ...env },
|
|
319
|
+
stdio: 'ignore',
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
child.unref();
|
|
323
|
+
|
|
324
|
+
// Wait for server to be ready
|
|
325
|
+
process.stdout.write(' Waiting for server to be ready');
|
|
326
|
+
const isReady = await waitForCondition(
|
|
327
|
+
() => isServerRunning(port),
|
|
328
|
+
CONFIG.serverTimeout,
|
|
329
|
+
2000,
|
|
330
|
+
() => process.stdout.write('.')
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
console.log();
|
|
334
|
+
|
|
335
|
+
if (!isReady) {
|
|
336
|
+
throw new Error(`Server failed to start within ${CONFIG.serverTimeout / 1000} seconds`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
log('✅', `Server is ready at http://localhost:${port}`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ============================================================================
|
|
343
|
+
// Cleanup
|
|
344
|
+
// ============================================================================
|
|
345
|
+
|
|
346
|
+
function cleanup(): void {
|
|
347
|
+
log('🧹', 'Cleaning up environment...');
|
|
348
|
+
|
|
349
|
+
stopContainer(CONFIG.containerName);
|
|
350
|
+
removeContainer(CONFIG.containerName);
|
|
351
|
+
killProcessOnPort(3006);
|
|
352
|
+
killProcessOnPort(3010);
|
|
353
|
+
killProcessOnPort(5433);
|
|
354
|
+
|
|
355
|
+
log('✅', 'Cleanup completed');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ============================================================================
|
|
359
|
+
// CLI
|
|
360
|
+
// ============================================================================
|
|
361
|
+
|
|
362
|
+
function showHelp(): void {
|
|
363
|
+
console.log(`
|
|
364
|
+
${colors.cyan('E2E Test Environment Setup Script')}
|
|
365
|
+
|
|
366
|
+
${colors.dim('Usage:')}
|
|
367
|
+
bun e2e/scripts/setup.ts [options]
|
|
368
|
+
|
|
369
|
+
${colors.dim('Options:')}
|
|
370
|
+
--clean Clean up existing containers and processes
|
|
371
|
+
--skip-db Skip database setup (use existing)
|
|
372
|
+
--skip-migrate Skip database migration
|
|
373
|
+
--build Build the application before starting
|
|
374
|
+
--start Start the server after setup
|
|
375
|
+
--port <port> Server port (default: ${CONFIG.defaultPort})
|
|
376
|
+
--help Show this help message
|
|
377
|
+
|
|
378
|
+
${colors.dim('Examples:')}
|
|
379
|
+
${colors.green('bun e2e/scripts/setup.ts')} # Setup DB only
|
|
380
|
+
${colors.green('bun e2e/scripts/setup.ts --start')} # Setup DB and start server
|
|
381
|
+
${colors.green('bun e2e/scripts/setup.ts --build --start')} # Full setup with build
|
|
382
|
+
${colors.green('bun e2e/scripts/setup.ts --clean')} # Clean up environment
|
|
383
|
+
|
|
384
|
+
${colors.dim('After setup, run tests with:')}
|
|
385
|
+
cd e2e
|
|
386
|
+
BASE_URL=http://localhost:3006 bun run test
|
|
387
|
+
`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
interface Options {
|
|
391
|
+
build: boolean;
|
|
392
|
+
clean: boolean;
|
|
393
|
+
help: boolean;
|
|
394
|
+
port: number;
|
|
395
|
+
skipDb: boolean;
|
|
396
|
+
skipMigrate: boolean;
|
|
397
|
+
start: boolean;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function parseArgs(): Options {
|
|
401
|
+
const args = process.argv.slice(2);
|
|
402
|
+
const options: Options = {
|
|
403
|
+
build: false,
|
|
404
|
+
clean: false,
|
|
405
|
+
help: false,
|
|
406
|
+
port: CONFIG.defaultPort,
|
|
407
|
+
skipDb: false,
|
|
408
|
+
skipMigrate: false,
|
|
409
|
+
start: false,
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
for (let i = 0; i < args.length; i++) {
|
|
413
|
+
switch (args[i]) {
|
|
414
|
+
case '--help':
|
|
415
|
+
case '-h':
|
|
416
|
+
options.help = true;
|
|
417
|
+
break;
|
|
418
|
+
case '--clean':
|
|
419
|
+
options.clean = true;
|
|
420
|
+
break;
|
|
421
|
+
case '--skip-db':
|
|
422
|
+
options.skipDb = true;
|
|
423
|
+
break;
|
|
424
|
+
case '--skip-migrate':
|
|
425
|
+
options.skipMigrate = true;
|
|
426
|
+
break;
|
|
427
|
+
case '--build':
|
|
428
|
+
options.build = true;
|
|
429
|
+
break;
|
|
430
|
+
case '--start':
|
|
431
|
+
options.start = true;
|
|
432
|
+
break;
|
|
433
|
+
case '--port':
|
|
434
|
+
options.port = parseInt(args[++i], 10) || CONFIG.defaultPort;
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return options;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ============================================================================
|
|
443
|
+
// Main
|
|
444
|
+
// ============================================================================
|
|
445
|
+
|
|
446
|
+
async function main(): Promise<void> {
|
|
447
|
+
const options = parseArgs();
|
|
448
|
+
|
|
449
|
+
if (options.help) {
|
|
450
|
+
showHelp();
|
|
451
|
+
process.exit(0);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
console.log(`
|
|
455
|
+
${colors.cyan('🤯 LobeHub E2E Environment Setup')}
|
|
456
|
+
${'─'.repeat(50)}
|
|
457
|
+
`);
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
if (options.clean) {
|
|
461
|
+
cleanup();
|
|
462
|
+
process.exit(0);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Calculate total steps
|
|
466
|
+
let totalSteps = 0;
|
|
467
|
+
if (!options.skipDb) totalSteps++;
|
|
468
|
+
if (!options.skipMigrate) totalSteps++;
|
|
469
|
+
if (options.build) totalSteps++;
|
|
470
|
+
if (options.start) totalSteps++;
|
|
471
|
+
|
|
472
|
+
let currentStep = 0;
|
|
473
|
+
|
|
474
|
+
// Step 1: Start database
|
|
475
|
+
if (!options.skipDb) {
|
|
476
|
+
logStep(++currentStep, totalSteps, 'Setting up PostgreSQL database');
|
|
477
|
+
await startPostgres();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Step 2: Run migration
|
|
481
|
+
if (!options.skipMigrate) {
|
|
482
|
+
logStep(++currentStep, totalSteps, 'Running database migrations');
|
|
483
|
+
await runMigration();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Step 3: Build (optional)
|
|
487
|
+
if (options.build) {
|
|
488
|
+
logStep(++currentStep, totalSteps, 'Building application');
|
|
489
|
+
await buildApp();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Step 4: Start server (optional)
|
|
493
|
+
if (options.start) {
|
|
494
|
+
logStep(++currentStep, totalSteps, 'Starting application server');
|
|
495
|
+
await startServer(options.port);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
console.log(`
|
|
499
|
+
${'─'.repeat(50)}
|
|
500
|
+
${colors.green('✅ E2E environment setup completed!')}
|
|
501
|
+
`);
|
|
502
|
+
|
|
503
|
+
// Print next steps
|
|
504
|
+
if (!options.start) {
|
|
505
|
+
console.log(`${colors.dim('Next steps:')}`);
|
|
506
|
+
console.log(`
|
|
507
|
+
1. Start the server (in project root):
|
|
508
|
+
${colors.cyan(`bun e2e/scripts/setup.ts --start`)}
|
|
509
|
+
|
|
510
|
+
2. Or start manually:
|
|
511
|
+
${colors.cyan(`bunx next start -p ${options.port}`)}
|
|
512
|
+
`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
console.log(`${colors.dim('Run tests:')}`);
|
|
516
|
+
console.log(`
|
|
517
|
+
cd e2e
|
|
518
|
+
${colors.cyan(`BASE_URL=http://localhost:${options.port} bun run test`)}
|
|
519
|
+
|
|
520
|
+
${colors.dim('# Debug mode (show browser)')}
|
|
521
|
+
${colors.cyan(`HEADLESS=false BASE_URL=http://localhost:${options.port} bun run test`)}
|
|
522
|
+
`);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
console.error(`\n${colors.red('❌ Setup failed:')}`, error);
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
main();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
@journey @home @sidebar @agent
|
|
2
|
+
Feature: Home 页面 Agent 管理
|
|
3
|
+
作为用户,我希望能够在 Home 页面管理 Agent
|
|
4
|
+
|
|
5
|
+
Background:
|
|
6
|
+
Given 用户已登录系统
|
|
7
|
+
And 用户在 Home 页面有一个 Agent
|
|
8
|
+
|
|
9
|
+
# ============================================
|
|
10
|
+
# 重命名
|
|
11
|
+
# ============================================
|
|
12
|
+
|
|
13
|
+
@HOME-AGENT-RENAME-001 @P0
|
|
14
|
+
Scenario: 通过右键菜单重命名 Agent
|
|
15
|
+
When 用户右键点击该 Agent
|
|
16
|
+
And 用户在菜单中选择重命名
|
|
17
|
+
And 用户输入新的名称 "My Renamed Agent"
|
|
18
|
+
Then 该项名称应该更新为 "My Renamed Agent"
|
|
19
|
+
|
|
20
|
+
@HOME-AGENT-RENAME-002 @P0
|
|
21
|
+
Scenario: 通过更多操作菜单重命名 Agent
|
|
22
|
+
When 用户悬停在该 Agent 上
|
|
23
|
+
And 用户点击更多操作按钮
|
|
24
|
+
And 用户在菜单中选择重命名
|
|
25
|
+
And 用户输入新的名称 "Agent From Menu"
|
|
26
|
+
Then 该项名称应该更新为 "Agent From Menu"
|
|
27
|
+
|
|
28
|
+
@HOME-AGENT-RENAME-003 @P1
|
|
29
|
+
Scenario: 重命名后按 Enter 确认
|
|
30
|
+
When 用户右键点击该 Agent
|
|
31
|
+
And 用户在菜单中选择重命名
|
|
32
|
+
And 用户输入新的名称 "Enter Confirmed" 并按 Enter
|
|
33
|
+
Then 该项名称应该更新为 "Enter Confirmed"
|
|
34
|
+
|
|
35
|
+
# ============================================
|
|
36
|
+
# 置顶
|
|
37
|
+
# ============================================
|
|
38
|
+
|
|
39
|
+
@HOME-AGENT-PIN-001 @P1
|
|
40
|
+
Scenario: 置顶 Agent
|
|
41
|
+
Given 该 Agent 未被置顶
|
|
42
|
+
When 用户右键点击该 Agent
|
|
43
|
+
And 用户在菜单中选择置顶
|
|
44
|
+
Then Agent 应该显示置顶图标
|
|
45
|
+
|
|
46
|
+
@HOME-AGENT-PIN-002 @P1
|
|
47
|
+
Scenario: 取消置顶 Agent
|
|
48
|
+
Given 该 Agent 已被置顶
|
|
49
|
+
When 用户右键点击该 Agent
|
|
50
|
+
And 用户在菜单中选择取消置顶
|
|
51
|
+
Then Agent 不应该显示置顶图标
|
|
52
|
+
|
|
53
|
+
# ============================================
|
|
54
|
+
# 删除
|
|
55
|
+
# ============================================
|
|
56
|
+
|
|
57
|
+
@HOME-AGENT-DELETE-001 @P0
|
|
58
|
+
Scenario: 删除 Agent
|
|
59
|
+
When 用户右键点击该 Agent
|
|
60
|
+
And 用户在菜单中选择删除
|
|
61
|
+
And 用户在弹窗中确认删除
|
|
62
|
+
Then Agent 应该从列表中移除
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
@journey @home @sidebar @group
|
|
2
|
+
Feature: Home 页面 Agent Group 管理
|
|
3
|
+
作为用户,我希望能够在 Home 页面管理 Agent Group
|
|
4
|
+
|
|
5
|
+
Background:
|
|
6
|
+
Given 用户已登录系统
|
|
7
|
+
And 用户在 Home 页面有一个 Agent Group
|
|
8
|
+
|
|
9
|
+
# ============================================
|
|
10
|
+
# 重命名
|
|
11
|
+
# ============================================
|
|
12
|
+
|
|
13
|
+
@HOME-GROUP-RENAME-001 @P0
|
|
14
|
+
Scenario: 通过右键菜单重命名 Agent Group
|
|
15
|
+
When 用户右键点击该 Agent Group
|
|
16
|
+
And 用户在菜单中选择重命名
|
|
17
|
+
And 用户输入新的名称 "My Renamed Group"
|
|
18
|
+
Then 该项名称应该更新为 "My Renamed Group"
|
|
19
|
+
|
|
20
|
+
@HOME-GROUP-RENAME-002 @P0
|
|
21
|
+
Scenario: 通过更多操作菜单重命名 Agent Group
|
|
22
|
+
When 用户悬停在该 Agent Group 上
|
|
23
|
+
And 用户点击更多操作按钮
|
|
24
|
+
And 用户在菜单中选择重命名
|
|
25
|
+
And 用户输入新的名称 "Group From Menu"
|
|
26
|
+
Then 该项名称应该更新为 "Group From Menu"
|
|
27
|
+
|
|
28
|
+
@HOME-GROUP-RENAME-003 @P1
|
|
29
|
+
Scenario: 重命名后按 Enter 确认
|
|
30
|
+
When 用户右键点击该 Agent Group
|
|
31
|
+
And 用户在菜单中选择重命名
|
|
32
|
+
And 用户输入新的名称 "Enter Confirmed" 并按 Enter
|
|
33
|
+
Then 该项名称应该更新为 "Enter Confirmed"
|
|
34
|
+
|
|
35
|
+
# ============================================
|
|
36
|
+
# 置顶
|
|
37
|
+
# ============================================
|
|
38
|
+
|
|
39
|
+
@HOME-GROUP-PIN-001 @P1
|
|
40
|
+
Scenario: 置顶 Agent Group
|
|
41
|
+
Given 该 Agent Group 未被置顶
|
|
42
|
+
When 用户右键点击该 Agent Group
|
|
43
|
+
And 用户在菜单中选择置顶
|
|
44
|
+
Then Agent Group 应该显示置顶图标
|
|
45
|
+
|
|
46
|
+
@HOME-GROUP-PIN-002 @P1
|
|
47
|
+
Scenario: 取消置顶 Agent Group
|
|
48
|
+
Given 该 Agent Group 已被置顶
|
|
49
|
+
When 用户右键点击该 Agent Group
|
|
50
|
+
And 用户在菜单中选择取消置顶
|
|
51
|
+
Then Agent Group 不应该显示置顶图标
|
|
52
|
+
|
|
53
|
+
# ============================================
|
|
54
|
+
# 删除
|
|
55
|
+
# ============================================
|
|
56
|
+
|
|
57
|
+
@HOME-GROUP-DELETE-001 @P0
|
|
58
|
+
Scenario: 删除 Agent Group
|
|
59
|
+
When 用户右键点击该 Agent Group
|
|
60
|
+
And 用户在菜单中选择删除
|
|
61
|
+
And 用户在弹窗中确认删除
|
|
62
|
+
Then Agent Group 应该从列表中移除
|