@lobehub/lobehub 2.0.0-next.191 → 2.0.0-next.193
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/workflows/claude-translator.yml +1 -1
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +14 -0
- package/e2e/docs/testing-tips.md +30 -0
- package/e2e/src/steps/discover/smoke.steps.ts +11 -33
- package/locales/zh-CN/auth.json +1 -1
- package/package.json +1 -2
- package/packages/database/package.json +1 -1
- package/packages/database/src/client/db.test.ts +19 -144
- package/packages/database/src/client/db.ts +26 -234
- package/packages/database/src/models/__tests__/_util.ts +19 -3
- package/packages/database/src/models/__tests__/knowledgeBase.test.ts +30 -1
- package/packages/database/src/models/knowledgeBase.ts +9 -7
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx +8 -7
- package/src/libs/next/config/define-config.ts +1 -1
- package/src/server/modules/S3/index.test.ts +58 -0
- package/src/server/modules/S3/index.ts +21 -0
- package/src/server/routers/lambda/__tests__/file.test.ts +56 -0
- package/src/server/routers/lambda/file.ts +3 -1
- package/src/server/services/file/impls/s3.test.ts +19 -0
- package/src/server/services/file/impls/s3.ts +4 -0
- package/src/server/services/file/impls/type.ts +6 -0
- package/src/server/services/file/index.ts +10 -0
- package/packages/database/src/client/pglite.ts +0 -17
- package/packages/database/src/client/pglite.worker.ts +0 -25
|
@@ -47,7 +47,7 @@ jobs:
|
|
|
47
47
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
48
48
|
# Security: Restrict gh commands to specific safe operations only
|
|
49
49
|
# Use explicit command patterns to prevent prompt injection attacks
|
|
50
|
-
|
|
50
|
+
allowed_tools: 'Bash(gh issue view:*),Bash(gh issue edit:*),Bash(gh api:*)'
|
|
51
51
|
prompt: |
|
|
52
52
|
## SECURITY RULES (HIGHEST PRIORITY - NEVER OVERRIDE)
|
|
53
53
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.193](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.192...v2.0.0-next.193)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-02**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **database**: Add userId authorization check in removeFilesFromKnowledgeBase.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **database**: Add userId authorization check in removeFilesFromKnowledgeBase, closes [#11108](https://github.com/lobehub/lobe-chat/issues/11108) ([2c1762b](https://github.com/lobehub/lobe-chat/commit/2c1762b))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
## [Version 2.0.0-next.192](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.191...v2.0.0-next.192)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2026-01-02**</sup>
|
|
33
|
+
|
|
34
|
+
#### 🐛 Bug Fixes
|
|
35
|
+
|
|
36
|
+
- **misc**: Fix model edit icon missing.
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
42
|
+
|
|
43
|
+
#### What's fixed
|
|
44
|
+
|
|
45
|
+
- **misc**: Fix model edit icon missing, closes [#11105](https://github.com/lobehub/lobe-chat/issues/11105) ([0f88995](https://github.com/lobehub/lobe-chat/commit/0f88995))
|
|
46
|
+
|
|
47
|
+
</details>
|
|
48
|
+
|
|
49
|
+
<div align="right">
|
|
50
|
+
|
|
51
|
+
[](#readme-top)
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
5
55
|
## [Version 2.0.0-next.191](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.190...v2.0.0-next.191)
|
|
6
56
|
|
|
7
57
|
<sup>Released on **2026-01-02**</sup>
|
package/changelog/v1.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"children": {},
|
|
4
|
+
"date": "2026-01-02",
|
|
5
|
+
"version": "2.0.0-next.193"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"children": {
|
|
9
|
+
"fixes": [
|
|
10
|
+
"Fix model edit icon missing."
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"date": "2026-01-02",
|
|
14
|
+
"version": "2.0.0-next.192"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"children": {
|
|
4
18
|
"improvements": [
|
package/e2e/docs/testing-tips.md
CHANGED
|
@@ -64,6 +64,36 @@ HEADLESS=false pnpm exec cucumber-js --config cucumber.config.js --tags "@smoke"
|
|
|
64
64
|
|
|
65
65
|
## 常见问题
|
|
66
66
|
|
|
67
|
+
### waitForLoadState ('networkidle') 超时
|
|
68
|
+
|
|
69
|
+
**原因**: `networkidle` 表示 500ms 内没有网络请求。在 CI 环境中,由于分析脚本、外部资源加载、轮询等持续网络活动,这个状态可能永远无法达到。
|
|
70
|
+
|
|
71
|
+
**错误示例**:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
page.waitForLoadState: Timeout 10000ms exceeded.
|
|
75
|
+
=========================== logs ===========================
|
|
76
|
+
"load" event fired
|
|
77
|
+
============================================================
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**解决**:
|
|
81
|
+
|
|
82
|
+
- **避免使用 `networkidle`** - 这是不可靠的等待策略
|
|
83
|
+
- **直接等待目标元素** - 使用 `expect(element).toBeVisible({ timeout: 30_000 })` 替代
|
|
84
|
+
- 如果必须等待页面加载完成,使用 `domcontentloaded` 或 `load` 事件
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// ❌ 不推荐 - networkidle 在 CI 中容易超时
|
|
88
|
+
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
|
89
|
+
const element = this.page.locator('[data-testid="my-element"]');
|
|
90
|
+
await expect(element).toBeVisible();
|
|
91
|
+
|
|
92
|
+
// ✅ 推荐 - 直接等待目标元素
|
|
93
|
+
const element = this.page.locator('[data-testid="my-element"]');
|
|
94
|
+
await expect(element).toBeVisible({ timeout: 30_000 });
|
|
95
|
+
```
|
|
96
|
+
|
|
67
97
|
### 测试超时 (function timed out)
|
|
68
98
|
|
|
69
99
|
**原因**: 元素定位失败或等待时间不足
|
|
@@ -9,50 +9,40 @@ import { CustomWorld } from '../../support/world';
|
|
|
9
9
|
|
|
10
10
|
// Home Page Steps
|
|
11
11
|
Then('I should see the featured assistants section', async function (this: CustomWorld) {
|
|
12
|
-
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
|
13
|
-
|
|
14
12
|
// Look for "Featured Agents" heading text (i18n key: home.featuredAssistants)
|
|
15
13
|
// Supports: en-US "Featured Agents", zh-CN "推荐助理"
|
|
16
14
|
const featuredSection = this.page
|
|
17
15
|
.getByRole('heading', { name: /featured agents|推荐助理/i })
|
|
18
16
|
.first();
|
|
19
|
-
await expect(featuredSection).toBeVisible({ timeout:
|
|
17
|
+
await expect(featuredSection).toBeVisible({ timeout: 30_000 });
|
|
20
18
|
});
|
|
21
19
|
|
|
22
20
|
Then('I should see the featured MCP tools section', async function (this: CustomWorld) {
|
|
23
|
-
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
|
24
|
-
|
|
25
21
|
// Look for "Featured Skills" heading text (i18n key: home.featuredTools)
|
|
26
22
|
// Supports: en-US "Featured Skills", zh-CN "推荐技能"
|
|
27
23
|
const mcpSection = this.page.getByRole('heading', { name: /featured skills|推荐技能/i }).first();
|
|
28
|
-
await expect(mcpSection).toBeVisible({ timeout:
|
|
24
|
+
await expect(mcpSection).toBeVisible({ timeout: 30_000 });
|
|
29
25
|
});
|
|
30
26
|
|
|
31
27
|
// Assistant List Page Steps
|
|
32
28
|
Then('I should see the search bar', async function (this: CustomWorld) {
|
|
33
|
-
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
|
34
|
-
|
|
35
29
|
// SearchBar component has data-testid="search-bar"
|
|
36
30
|
const searchBar = this.page.locator('[data-testid="search-bar"]').first();
|
|
37
|
-
await expect(searchBar).toBeVisible({ timeout:
|
|
31
|
+
await expect(searchBar).toBeVisible({ timeout: 30_000 });
|
|
38
32
|
});
|
|
39
33
|
|
|
40
34
|
Then('I should see the category menu', async function (this: CustomWorld) {
|
|
41
|
-
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
|
42
|
-
|
|
43
35
|
// CategoryMenu component has data-testid="category-menu"
|
|
44
36
|
const categoryMenu = this.page.locator('[data-testid="category-menu"]').first();
|
|
45
|
-
await expect(categoryMenu).toBeVisible({ timeout:
|
|
37
|
+
await expect(categoryMenu).toBeVisible({ timeout: 30_000 });
|
|
46
38
|
});
|
|
47
39
|
|
|
48
40
|
Then('I should see assistant cards', async function (this: CustomWorld) {
|
|
49
|
-
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
|
50
|
-
|
|
51
41
|
// Look for assistant items by data-testid
|
|
52
42
|
const assistantItems = this.page.locator('[data-testid="assistant-item"]');
|
|
53
43
|
|
|
54
44
|
// Wait for at least one item to be visible
|
|
55
|
-
await expect(assistantItems.first()).toBeVisible({ timeout:
|
|
45
|
+
await expect(assistantItems.first()).toBeVisible({ timeout: 30_000 });
|
|
56
46
|
|
|
57
47
|
// Check we have multiple items
|
|
58
48
|
const count = await assistantItems.count();
|
|
@@ -60,22 +50,18 @@ Then('I should see assistant cards', async function (this: CustomWorld) {
|
|
|
60
50
|
});
|
|
61
51
|
|
|
62
52
|
Then('I should see pagination controls', async function (this: CustomWorld) {
|
|
63
|
-
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
|
64
|
-
|
|
65
53
|
// Pagination component has data-testid="pagination"
|
|
66
54
|
const pagination = this.page.locator('[data-testid="pagination"]').first();
|
|
67
|
-
await expect(pagination).toBeVisible({ timeout:
|
|
55
|
+
await expect(pagination).toBeVisible({ timeout: 30_000 });
|
|
68
56
|
});
|
|
69
57
|
|
|
70
58
|
// Model List Page Steps
|
|
71
59
|
Then('I should see model cards', async function (this: CustomWorld) {
|
|
72
|
-
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
|
73
|
-
|
|
74
60
|
// Model items have data-testid="model-item"
|
|
75
61
|
const modelItems = this.page.locator('[data-testid="model-item"]');
|
|
76
62
|
|
|
77
63
|
// Wait for at least one item to be visible
|
|
78
|
-
await expect(modelItems.first()).toBeVisible({ timeout:
|
|
64
|
+
await expect(modelItems.first()).toBeVisible({ timeout: 30_000 });
|
|
79
65
|
|
|
80
66
|
// Check we have multiple items
|
|
81
67
|
const count = await modelItems.count();
|
|
@@ -83,22 +69,18 @@ Then('I should see model cards', async function (this: CustomWorld) {
|
|
|
83
69
|
});
|
|
84
70
|
|
|
85
71
|
Then('I should see the sort dropdown', async function (this: CustomWorld) {
|
|
86
|
-
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
|
87
|
-
|
|
88
72
|
// SortButton has data-testid="sort-dropdown"
|
|
89
73
|
const sortDropdown = this.page.locator('[data-testid="sort-dropdown"]').first();
|
|
90
|
-
await expect(sortDropdown).toBeVisible({ timeout:
|
|
74
|
+
await expect(sortDropdown).toBeVisible({ timeout: 30_000 });
|
|
91
75
|
});
|
|
92
76
|
|
|
93
77
|
// Provider List Page Steps
|
|
94
78
|
Then('I should see provider cards', async function (this: CustomWorld) {
|
|
95
|
-
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
|
96
|
-
|
|
97
79
|
// Look for provider items by data-testid
|
|
98
80
|
const providerItems = this.page.locator('[data-testid="provider-item"]');
|
|
99
81
|
|
|
100
82
|
// Wait for at least one item to be visible
|
|
101
|
-
await expect(providerItems.first()).toBeVisible({ timeout:
|
|
83
|
+
await expect(providerItems.first()).toBeVisible({ timeout: 30_000 });
|
|
102
84
|
|
|
103
85
|
// Check we have multiple items
|
|
104
86
|
const count = await providerItems.count();
|
|
@@ -107,13 +89,11 @@ Then('I should see provider cards', async function (this: CustomWorld) {
|
|
|
107
89
|
|
|
108
90
|
// MCP List Page Steps
|
|
109
91
|
Then('I should see MCP cards', async function (this: CustomWorld) {
|
|
110
|
-
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
|
111
|
-
|
|
112
92
|
// Look for MCP items by data-testid
|
|
113
93
|
const mcpItems = this.page.locator('[data-testid="mcp-item"]');
|
|
114
94
|
|
|
115
95
|
// Wait for at least one item to be visible
|
|
116
|
-
await expect(mcpItems.first()).toBeVisible({ timeout:
|
|
96
|
+
await expect(mcpItems.first()).toBeVisible({ timeout: 30_000 });
|
|
117
97
|
|
|
118
98
|
// Check we have multiple items
|
|
119
99
|
const count = await mcpItems.count();
|
|
@@ -121,9 +101,7 @@ Then('I should see MCP cards', async function (this: CustomWorld) {
|
|
|
121
101
|
});
|
|
122
102
|
|
|
123
103
|
Then('I should see the category filter', async function (this: CustomWorld) {
|
|
124
|
-
await this.page.waitForLoadState('networkidle', { timeout: 10_000 });
|
|
125
|
-
|
|
126
104
|
// CategoryMenu component has data-testid="category-menu" (shared across list pages)
|
|
127
105
|
const categoryFilter = this.page.locator('[data-testid="category-menu"]').first();
|
|
128
|
-
await expect(categoryFilter).toBeVisible({ timeout:
|
|
106
|
+
await expect(categoryFilter).toBeVisible({ timeout: 30_000 });
|
|
129
107
|
});
|
package/locales/zh-CN/auth.json
CHANGED
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
"stats.topicsRank.right": "消息数",
|
|
215
215
|
"stats.topicsRank.title": "话题内容量",
|
|
216
216
|
"stats.updatedAt": "更新至",
|
|
217
|
-
"stats.welcome": "{{
|
|
217
|
+
"stats.welcome": "{{username}},这是你与 {{appName}} 一起记录协作的第 <span>{{days}}</span> 天",
|
|
218
218
|
"stats.words": "累计字数",
|
|
219
219
|
"tab.apikey": "API Key",
|
|
220
220
|
"tab.profile": "账号",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.193",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -154,7 +154,6 @@
|
|
|
154
154
|
"@codesandbox/sandpack-react": "^2.20.0",
|
|
155
155
|
"@dnd-kit/core": "^6.3.1",
|
|
156
156
|
"@dnd-kit/utilities": "^3.2.2",
|
|
157
|
-
"@electric-sql/pglite": "0.2.17",
|
|
158
157
|
"@emotion/react": "^11.14.0",
|
|
159
158
|
"@fal-ai/client": "^1.8.0",
|
|
160
159
|
"@formkit/auto-animate": "^0.9.0",
|
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
"ws": "^8.18.3"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
+
"@electric-sql/pglite": "^0.3.14",
|
|
26
27
|
"dotenv": "^17.2.3",
|
|
27
28
|
"fake-indexeddb": "^6.2.5"
|
|
28
29
|
},
|
|
29
30
|
"peerDependencies": {
|
|
30
|
-
"@electric-sql/pglite": "^0.2.17",
|
|
31
31
|
"dayjs": ">=1.11.19",
|
|
32
32
|
"drizzle-orm": ">=0.44.7",
|
|
33
33
|
"nanoid": ">=5.1.6",
|
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PGlite } from '@electric-sql/pglite';
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
3
|
|
|
4
|
-
import { DatabaseManager } from './db';
|
|
5
|
-
|
|
6
|
-
// Mock 所有外部依赖
|
|
7
4
|
vi.mock('@electric-sql/pglite', () => ({
|
|
8
|
-
|
|
9
|
-
IdbFs: vi.fn(),
|
|
10
|
-
PGlite: vi.fn(),
|
|
11
|
-
MemoryFS: vi.fn(),
|
|
5
|
+
PGlite: vi.fn(() => ({})),
|
|
12
6
|
}));
|
|
13
7
|
|
|
14
8
|
vi.mock('@electric-sql/pglite/vector', () => ({
|
|
15
|
-
default: vi.fn(),
|
|
16
9
|
vector: vi.fn(),
|
|
17
10
|
}));
|
|
18
11
|
|
|
@@ -24,154 +17,36 @@ vi.mock('drizzle-orm/pglite', () => ({
|
|
|
24
17
|
})),
|
|
25
18
|
}));
|
|
26
19
|
|
|
27
|
-
let manager: DatabaseManager;
|
|
28
|
-
let progressEvents: ClientDBLoadingProgress[] = [];
|
|
29
|
-
let stateChanges: DatabaseLoadingState[] = [];
|
|
30
|
-
|
|
31
|
-
let callbacks = {
|
|
32
|
-
onProgress: vi.fn((progress: ClientDBLoadingProgress) => {
|
|
33
|
-
progressEvents.push(progress);
|
|
34
|
-
}),
|
|
35
|
-
onStateChange: vi.fn((state: DatabaseLoadingState) => {
|
|
36
|
-
stateChanges.push(state);
|
|
37
|
-
}),
|
|
38
|
-
};
|
|
39
|
-
|
|
40
20
|
beforeEach(() => {
|
|
41
21
|
vi.clearAllMocks();
|
|
42
|
-
|
|
43
|
-
stateChanges = [];
|
|
44
|
-
|
|
45
|
-
callbacks = {
|
|
46
|
-
onProgress: vi.fn((progress: ClientDBLoadingProgress) => {
|
|
47
|
-
progressEvents.push(progress);
|
|
48
|
-
}),
|
|
49
|
-
onStateChange: vi.fn((state: DatabaseLoadingState) => {
|
|
50
|
-
stateChanges.push(state);
|
|
51
|
-
}),
|
|
52
|
-
};
|
|
53
|
-
// @ts-expect-error
|
|
54
|
-
DatabaseManager['instance'] = undefined;
|
|
55
|
-
manager = DatabaseManager.getInstance();
|
|
22
|
+
vi.resetModules();
|
|
56
23
|
});
|
|
57
24
|
|
|
58
25
|
describe('DatabaseManager', () => {
|
|
59
|
-
describe('
|
|
60
|
-
it(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
DatabaseLoadingState.Initializing,
|
|
68
|
-
DatabaseLoadingState.LoadingDependencies,
|
|
69
|
-
DatabaseLoadingState.LoadingWasm,
|
|
70
|
-
DatabaseLoadingState.Migrating,
|
|
71
|
-
DatabaseLoadingState.Finished,
|
|
72
|
-
DatabaseLoadingState.Ready,
|
|
73
|
-
]);
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
timeout: 15000,
|
|
77
|
-
},
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
it('should report dependencies loading progress', async () => {
|
|
81
|
-
await manager.initialize(callbacks);
|
|
82
|
-
|
|
83
|
-
// 验证依赖加载进度回调
|
|
84
|
-
const dependencyProgress = progressEvents.filter((e) => e.phase === 'dependencies');
|
|
85
|
-
expect(dependencyProgress.length).toBeGreaterThan(0);
|
|
86
|
-
expect(dependencyProgress[dependencyProgress.length - 1]).toEqual(
|
|
87
|
-
expect.objectContaining({
|
|
88
|
-
phase: 'dependencies',
|
|
89
|
-
progress: 100,
|
|
90
|
-
costTime: expect.any(Number),
|
|
91
|
-
}),
|
|
92
|
-
);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should report WASM loading progress', async () => {
|
|
96
|
-
await manager.initialize(callbacks);
|
|
97
|
-
|
|
98
|
-
// 验证 WASM 加载进度回调
|
|
99
|
-
const wasmProgress = progressEvents.filter((e) => e.phase === 'wasm');
|
|
100
|
-
// expect(wasmProgress.length).toBeGreaterThan(0);
|
|
101
|
-
expect(wasmProgress[wasmProgress.length - 1]).toEqual(
|
|
102
|
-
expect.objectContaining({
|
|
103
|
-
phase: 'wasm',
|
|
104
|
-
progress: 100,
|
|
105
|
-
costTime: expect.any(Number),
|
|
106
|
-
}),
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should handle initialization errors', async () => {
|
|
111
|
-
// 模拟加载失败
|
|
112
|
-
vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'));
|
|
113
|
-
|
|
114
|
-
await expect(manager.initialize(callbacks)).rejects.toThrow();
|
|
115
|
-
expect(stateChanges).toContain(DatabaseLoadingState.Error);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should only initialize once when called multiple times', async () => {
|
|
119
|
-
const firstInit = manager.initialize(callbacks);
|
|
120
|
-
const secondInit = manager.initialize(callbacks);
|
|
121
|
-
|
|
122
|
-
await Promise.all([firstInit, secondInit]);
|
|
123
|
-
|
|
124
|
-
// 验证回调只被调用一次
|
|
125
|
-
const readyStateCount = stateChanges.filter(
|
|
126
|
-
(state) => state === DatabaseLoadingState.Ready,
|
|
127
|
-
).length;
|
|
128
|
-
expect(readyStateCount).toBe(1);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe('Progress Calculation', () => {
|
|
133
|
-
it('should report progress between 0 and 100', async () => {
|
|
134
|
-
await manager.initialize(callbacks);
|
|
135
|
-
|
|
136
|
-
// 验证所有进度值都在有效范围内
|
|
137
|
-
progressEvents.forEach((event) => {
|
|
138
|
-
expect(event.progress).toBeGreaterThanOrEqual(0);
|
|
139
|
-
expect(event.progress).toBeLessThanOrEqual(100);
|
|
26
|
+
describe('initializeDB', () => {
|
|
27
|
+
it('should initialize database with PGlite', async () => {
|
|
28
|
+
const { initializeDB } = await import('./db');
|
|
29
|
+
await initializeDB();
|
|
30
|
+
|
|
31
|
+
expect(PGlite).toHaveBeenCalledWith('idb://lobechat', {
|
|
32
|
+
extensions: { vector: expect.any(Function) },
|
|
33
|
+
relaxedDurability: true,
|
|
140
34
|
});
|
|
141
35
|
});
|
|
142
36
|
|
|
143
|
-
it('should
|
|
144
|
-
await
|
|
145
|
-
|
|
146
|
-
// 验证最终进度回调包含耗时信息
|
|
147
|
-
const finalProgress = progressEvents[progressEvents.length - 1];
|
|
148
|
-
expect(finalProgress.costTime).toBeGreaterThan(0);
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
describe('Error Handling', () => {
|
|
153
|
-
it('should handle missing callbacks gracefully', async () => {
|
|
154
|
-
// 测试没有提供回调的情况
|
|
155
|
-
await expect(manager.initialize()).resolves.toBeDefined();
|
|
156
|
-
});
|
|
37
|
+
it('should only initialize once when called multiple times', async () => {
|
|
38
|
+
const { initializeDB } = await import('./db');
|
|
39
|
+
await Promise.all([initializeDB(), initializeDB()]);
|
|
157
40
|
|
|
158
|
-
|
|
159
|
-
// 只提供部分回调
|
|
160
|
-
await expect(manager.initialize({ onProgress: callbacks.onProgress })).resolves.toBeDefined();
|
|
161
|
-
await expect(
|
|
162
|
-
manager.initialize({ onStateChange: callbacks.onStateChange }),
|
|
163
|
-
).resolves.toBeDefined();
|
|
41
|
+
expect(PGlite).toHaveBeenCalledTimes(1);
|
|
164
42
|
});
|
|
165
43
|
});
|
|
166
44
|
|
|
167
|
-
describe('
|
|
168
|
-
it('should throw error when accessing database before initialization', () => {
|
|
169
|
-
expect(() => manager.db).toThrow('Database not initialized');
|
|
170
|
-
});
|
|
171
|
-
|
|
45
|
+
describe('clientDB proxy', () => {
|
|
172
46
|
it('should provide access to database after initialization', async () => {
|
|
173
|
-
await
|
|
174
|
-
|
|
47
|
+
const { clientDB, initializeDB } = await import('./db');
|
|
48
|
+
await initializeDB();
|
|
49
|
+
expect(clientDB).toBeDefined();
|
|
175
50
|
});
|
|
176
51
|
});
|
|
177
52
|
});
|