@lobehub/lobehub 2.0.0-next.352 → 2.0.0-next.354
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/.agents/skills/add-provider-doc/SKILL.md +90 -0
- package/.agents/skills/add-setting-env/SKILL.md +106 -0
- package/.agents/skills/debug/SKILL.md +66 -0
- package/.agents/skills/desktop/SKILL.md +78 -0
- package/.agents/skills/desktop/references/feature-implementation.md +99 -0
- package/.agents/skills/desktop/references/local-tools.md +133 -0
- package/.agents/skills/desktop/references/menu-config.md +103 -0
- package/.agents/skills/desktop/references/window-management.md +143 -0
- package/.agents/skills/drizzle/SKILL.md +129 -0
- package/.agents/skills/drizzle/references/db-migrations.md +50 -0
- package/.agents/skills/hotkey/SKILL.md +90 -0
- package/{.cursor/rules/i18n.mdc → .agents/skills/i18n/SKILL.md} +14 -23
- package/.agents/skills/linear/SKILL.md +51 -0
- package/.agents/skills/microcopy/SKILL.md +83 -0
- package/.agents/skills/modal/SKILL.md +102 -0
- package/{.cursor/rules/project-structure.mdc → .agents/skills/project-overview/SKILL.md} +65 -37
- package/.agents/skills/react/SKILL.md +73 -0
- package/.agents/skills/react/references/layout-kit.md +100 -0
- package/.agents/skills/recent-data/SKILL.md +108 -0
- package/.agents/skills/testing/SKILL.md +89 -0
- package/.agents/skills/testing/references/agent-runtime-e2e.md +126 -0
- package/.agents/skills/testing/references/db-model-test.md +124 -0
- package/.agents/skills/testing/references/desktop-controller-test.md +124 -0
- package/.agents/skills/testing/references/electron-ipc-test.md +63 -0
- package/.agents/skills/testing/references/zustand-store-action-test.md +150 -0
- package/.agents/skills/typescript/SKILL.md +52 -0
- package/.agents/skills/zustand/SKILL.md +78 -0
- package/.agents/skills/zustand/references/action-patterns.md +125 -0
- package/.agents/skills/zustand/references/slice-organization.md +125 -0
- package/AGENTS.md +42 -55
- package/CHANGELOG.md +58 -0
- package/CLAUDE.md +57 -46
- package/GEMINI.md +47 -39
- package/changelog/v1.json +14 -0
- package/docs/development/database-schema.dbml +5 -0
- package/package.json +1 -1
- package/packages/database/migrations/0071_add_async_task_extend.sql +5 -0
- package/packages/database/migrations/meta/0071_snapshot.json +10720 -0
- package/packages/database/migrations/meta/_journal.json +7 -0
- package/packages/database/src/schemas/asyncTask.ts +12 -2
- package/src/features/FileViewer/Renderer/PDF/index.tsx +2 -3
- package/src/features/ShareModal/SharePdf/PdfPreview.tsx +1 -2
- package/src/libs/pdfjs/index.tsx +25 -0
- package/src/store/test-coverage.md +5 -5
- package/.cursor/rules/add-provider-doc.mdc +0 -183
- package/.cursor/rules/add-setting-env.mdc +0 -175
- package/.cursor/rules/cursor-rules.mdc +0 -28
- package/.cursor/rules/db-migrations.mdc +0 -46
- package/.cursor/rules/debug-usage.mdc +0 -86
- package/.cursor/rules/desktop-controller-tests.mdc +0 -189
- package/.cursor/rules/desktop-feature-implementation.mdc +0 -155
- package/.cursor/rules/desktop-local-tools-implement.mdc +0 -81
- package/.cursor/rules/desktop-menu-configuration.mdc +0 -209
- package/.cursor/rules/desktop-window-management.mdc +0 -301
- package/.cursor/rules/drizzle-schema-style-guide.mdc +0 -218
- package/.cursor/rules/hotkey.mdc +0 -162
- package/.cursor/rules/linear.mdc +0 -53
- package/.cursor/rules/microcopy-cn.mdc +0 -158
- package/.cursor/rules/microcopy-en.mdc +0 -148
- package/.cursor/rules/modal-imperative.mdc +0 -162
- package/.cursor/rules/packages/react-layout-kit.mdc +0 -122
- package/.cursor/rules/project-introduce.mdc +0 -36
- package/.cursor/rules/react.mdc +0 -169
- package/.cursor/rules/recent-data-usage.mdc +0 -139
- package/.cursor/rules/rules-index.mdc +0 -44
- package/.cursor/rules/testing-guide/agent-runtime-e2e.mdc +0 -285
- package/.cursor/rules/testing-guide/db-model-test.mdc +0 -455
- package/.cursor/rules/testing-guide/electron-ipc-test.mdc +0 -80
- package/.cursor/rules/testing-guide/testing-guide.mdc +0 -534
- package/.cursor/rules/testing-guide/zustand-store-action-test.mdc +0 -574
- package/.cursor/rules/typescript.mdc +0 -55
- package/.cursor/rules/zustand-action-patterns.mdc +0 -328
- package/.cursor/rules/zustand-slice-organization.mdc +0 -308
- package/src/libs/pdfjs/pdf.worker.ts +0 -1
- package/src/libs/pdfjs/worker.ts +0 -12
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/AGENTS.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/SKILL.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/advanced-event-handler-refs.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/advanced-use-latest.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-api-routes.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-defer-await.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-dependencies.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-parallel.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/async-suspense-boundaries.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-barrel-imports.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-conditional.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-defer-third-party.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-dynamic-imports.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/bundle-preload.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-event-listeners.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-localstorage-schema.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-passive-event-listeners.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/client-swr-dedup.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-batch-dom-css.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-function-results.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-property-access.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-cache-storage.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-combine-iterations.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-early-exit.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-hoist-regexp.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-index-maps.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-length-check-first.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-min-max-loop.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-set-map-lookups.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/js-tosorted-immutable.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-activity.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-animate-svg-wrapper.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-conditional-render.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-content-visibility.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-hoist-jsx.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-hydration-no-flicker.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rendering-svg-precision.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-defer-reads.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-dependencies.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-derived-state.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-functional-setstate.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-lazy-state-init.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-memo.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/rerender-transitions.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-after-nonblocking.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-cache-lru.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-cache-react.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-parallel-fetching.md +0 -0
- /package/.agents/{vercel-react-best-practices → skills/vercel-react-best-practices}/rules/server-serialization.md +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Agent Runtime E2E Testing Guide
|
|
2
|
+
|
|
3
|
+
## Core Principles
|
|
4
|
+
|
|
5
|
+
### Minimal Mock Principle
|
|
6
|
+
|
|
7
|
+
Only mock **three external dependencies**:
|
|
8
|
+
|
|
9
|
+
| Dependency | Mock | Description |
|
|
10
|
+
|------------|------|-------------|
|
|
11
|
+
| Database | PGLite | In-memory database from `@lobechat/database/test-utils` |
|
|
12
|
+
| Redis | InMemoryAgentStateManager | Memory implementation |
|
|
13
|
+
| Redis | InMemoryStreamEventManager | Memory implementation |
|
|
14
|
+
|
|
15
|
+
**NOT mocked:**
|
|
16
|
+
- `model-bank` - Uses real model config
|
|
17
|
+
- `Mecha` (AgentToolsEngine, ContextEngineering)
|
|
18
|
+
- `AgentRuntimeService`
|
|
19
|
+
- `AgentRuntimeCoordinator`
|
|
20
|
+
|
|
21
|
+
### Use vi.spyOn, not vi.mock
|
|
22
|
+
|
|
23
|
+
Different tests need different LLM responses. `vi.spyOn` provides:
|
|
24
|
+
- Flexible return values per test
|
|
25
|
+
- Easy testing of different scenarios
|
|
26
|
+
- Better test isolation
|
|
27
|
+
|
|
28
|
+
### Default Model: gpt-5
|
|
29
|
+
|
|
30
|
+
- Always available in `model-bank`
|
|
31
|
+
- Stable across model updates
|
|
32
|
+
|
|
33
|
+
## Technical Implementation
|
|
34
|
+
|
|
35
|
+
### Database Setup
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { LobeChatDatabase } from '@lobechat/database';
|
|
39
|
+
import { getTestDB } from '@lobechat/database/test-utils';
|
|
40
|
+
|
|
41
|
+
let testDB: LobeChatDatabase;
|
|
42
|
+
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
testDB = await getTestDB();
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### OpenAI Stream Response Helper
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
export const createOpenAIStreamResponse = (options: {
|
|
52
|
+
content?: string;
|
|
53
|
+
toolCalls?: Array<{ id: string; name: string; arguments: string }>;
|
|
54
|
+
finishReason?: 'stop' | 'tool_calls';
|
|
55
|
+
}) => {
|
|
56
|
+
const { content, toolCalls, finishReason = 'stop' } = options;
|
|
57
|
+
|
|
58
|
+
return new Response(
|
|
59
|
+
new ReadableStream({
|
|
60
|
+
start(controller) {
|
|
61
|
+
const encoder = new TextEncoder();
|
|
62
|
+
|
|
63
|
+
if (content) {
|
|
64
|
+
const chunk = {
|
|
65
|
+
id: 'chatcmpl-mock',
|
|
66
|
+
object: 'chat.completion.chunk',
|
|
67
|
+
model: 'gpt-5',
|
|
68
|
+
choices: [{ index: 0, delta: { content }, finish_reason: null }],
|
|
69
|
+
};
|
|
70
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ... tool_calls handling
|
|
74
|
+
// ... finish chunk
|
|
75
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
76
|
+
controller.close();
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
{ headers: { 'content-type': 'text/event-stream' } }
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### State Management
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { InMemoryAgentStateManager, InMemoryStreamEventManager } from '@/server/modules/AgentRuntime';
|
|
88
|
+
|
|
89
|
+
const stateManager = new InMemoryAgentStateManager();
|
|
90
|
+
const streamEventManager = new InMemoryStreamEventManager();
|
|
91
|
+
|
|
92
|
+
const service = new AgentRuntimeService(serverDB, userId, {
|
|
93
|
+
coordinatorOptions: { stateManager, streamEventManager },
|
|
94
|
+
queueService: null,
|
|
95
|
+
streamEventManager,
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Mock OpenAI API
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
|
103
|
+
|
|
104
|
+
it('should handle text response', async () => {
|
|
105
|
+
fetchSpy.mockResolvedValueOnce(createOpenAIStreamResponse({ content: 'Response text' }));
|
|
106
|
+
// ... execute test
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should handle tool calls', async () => {
|
|
110
|
+
fetchSpy.mockResolvedValueOnce(createOpenAIStreamResponse({
|
|
111
|
+
toolCalls: [{
|
|
112
|
+
id: 'call_123',
|
|
113
|
+
name: 'lobe-web-browsing____search____builtin',
|
|
114
|
+
arguments: JSON.stringify({ query: 'weather' }),
|
|
115
|
+
}],
|
|
116
|
+
finishReason: 'tool_calls',
|
|
117
|
+
}));
|
|
118
|
+
// ... execute test
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Notes
|
|
123
|
+
|
|
124
|
+
1. **Test isolation**: Clean `InMemoryAgentStateManager` and `InMemoryStreamEventManager` after each test
|
|
125
|
+
2. **Timeout**: E2E tests may need longer timeouts
|
|
126
|
+
3. **Debug**: Use `DEBUG=lobe-server:*` for detailed logs
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Database Model Testing Guide
|
|
2
|
+
|
|
3
|
+
Test `packages/database` Model layer.
|
|
4
|
+
|
|
5
|
+
## Dual Environment Verification (Required)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# 1. Client environment (fast)
|
|
9
|
+
cd packages/database && TEST_SERVER_DB=0 bunx vitest run --silent='passed-only' '[file]'
|
|
10
|
+
|
|
11
|
+
# 2. Server environment (compatibility)
|
|
12
|
+
cd packages/database && TEST_SERVER_DB=1 bunx vitest run --silent='passed-only' '[file]'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## User Permission Check - Security First 🔒
|
|
16
|
+
|
|
17
|
+
**Critical security requirement**: All user data operations must include permission checks.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// ❌ DANGEROUS: Missing permission check
|
|
21
|
+
update = async (id: string, data: Partial<MyModel>) => {
|
|
22
|
+
return this.db.update(myTable).set(data)
|
|
23
|
+
.where(eq(myTable.id, id)) // Only checks ID
|
|
24
|
+
.returning();
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ✅ SECURE: Permission check included
|
|
28
|
+
update = async (id: string, data: Partial<MyModel>) => {
|
|
29
|
+
return this.db.update(myTable).set(data)
|
|
30
|
+
.where(and(
|
|
31
|
+
eq(myTable.id, id),
|
|
32
|
+
eq(myTable.userId, this.userId) // ✅ Permission check
|
|
33
|
+
))
|
|
34
|
+
.returning();
|
|
35
|
+
};
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Test File Structure
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// @vitest-environment node
|
|
42
|
+
describe('MyModel', () => {
|
|
43
|
+
describe('create', () => { /* ... */ });
|
|
44
|
+
describe('queryAll', () => { /* ... */ });
|
|
45
|
+
describe('update', () => {
|
|
46
|
+
it('should update own records');
|
|
47
|
+
it('should NOT update other users records'); // 🔒 Security
|
|
48
|
+
});
|
|
49
|
+
describe('delete', () => {
|
|
50
|
+
it('should delete own records');
|
|
51
|
+
it('should NOT delete other users records'); // 🔒 Security
|
|
52
|
+
});
|
|
53
|
+
describe('user isolation', () => {
|
|
54
|
+
it('should enforce user data isolation'); // 🔒 Core security
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Security Test Example
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
it('should not update records of other users', async () => {
|
|
63
|
+
const [otherUserRecord] = await serverDB
|
|
64
|
+
.insert(myTable)
|
|
65
|
+
.values({ userId: 'other-user', data: 'original' })
|
|
66
|
+
.returning();
|
|
67
|
+
|
|
68
|
+
const result = await myModel.update(otherUserRecord.id, { data: 'hacked' });
|
|
69
|
+
|
|
70
|
+
expect(result).toBeUndefined();
|
|
71
|
+
const unchanged = await serverDB.query.myTable.findFirst({
|
|
72
|
+
where: eq(myTable.id, otherUserRecord.id),
|
|
73
|
+
});
|
|
74
|
+
expect(unchanged?.data).toBe('original');
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Data Management
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
const userId = 'test-user';
|
|
82
|
+
const otherUserId = 'other-user';
|
|
83
|
+
|
|
84
|
+
beforeEach(async () => {
|
|
85
|
+
await serverDB.delete(users);
|
|
86
|
+
await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(async () => {
|
|
90
|
+
await serverDB.delete(users);
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Foreign Key Handling
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
// ❌ Wrong: Invalid foreign key
|
|
98
|
+
const testData = { asyncTaskId: 'invalid-uuid', fileId: 'non-existent' };
|
|
99
|
+
|
|
100
|
+
// ✅ Correct: Use null
|
|
101
|
+
const testData = { asyncTaskId: null, fileId: null };
|
|
102
|
+
|
|
103
|
+
// ✅ Or: Create referenced record first
|
|
104
|
+
beforeEach(async () => {
|
|
105
|
+
const [asyncTask] = await serverDB.insert(asyncTasks)
|
|
106
|
+
.values({ id: 'valid-id', status: 'pending' }).returning();
|
|
107
|
+
testData.asyncTaskId = asyncTask.id;
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Predictable Sorting
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// ✅ Use explicit timestamps
|
|
115
|
+
const oldDate = new Date('2024-01-01T10:00:00Z');
|
|
116
|
+
const newDate = new Date('2024-01-02T10:00:00Z');
|
|
117
|
+
await serverDB.insert(table).values([
|
|
118
|
+
{ ...data1, createdAt: oldDate },
|
|
119
|
+
{ ...data2, createdAt: newDate },
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
// ❌ Don't rely on insert order
|
|
123
|
+
await serverDB.insert(table).values([data1, data2]); // Unpredictable
|
|
124
|
+
```
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Desktop Controller Unit Testing Guide
|
|
2
|
+
|
|
3
|
+
## Testing Framework & Directory Structure
|
|
4
|
+
|
|
5
|
+
LobeChat Desktop uses Vitest as the test framework. Controller unit tests should be placed in the `__tests__` directory adjacent to the controller file, named with the original controller filename plus `.test.ts`.
|
|
6
|
+
|
|
7
|
+
```plaintext
|
|
8
|
+
apps/desktop/src/main/controllers/
|
|
9
|
+
├── __tests__/
|
|
10
|
+
│ ├── index.test.ts
|
|
11
|
+
│ ├── MenuCtr.test.ts
|
|
12
|
+
│ └── ...
|
|
13
|
+
├── McpCtr.ts
|
|
14
|
+
├── MenuCtr.ts
|
|
15
|
+
└── ...
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Basic Test File Structure
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
22
|
+
|
|
23
|
+
import type { App } from '@/core/App';
|
|
24
|
+
|
|
25
|
+
import YourController from '../YourControllerName';
|
|
26
|
+
|
|
27
|
+
// Mock dependencies
|
|
28
|
+
vi.mock('dependency-module', () => ({
|
|
29
|
+
dependencyFunction: vi.fn(),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
// Mock App instance
|
|
33
|
+
const mockApp = {
|
|
34
|
+
// Mock necessary App properties and methods as needed
|
|
35
|
+
} as unknown as App;
|
|
36
|
+
|
|
37
|
+
describe('YourController', () => {
|
|
38
|
+
let controller: YourController;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
controller = new YourController(mockApp);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('methodName', () => {
|
|
46
|
+
it('test scenario description', async () => {
|
|
47
|
+
// Prepare test data
|
|
48
|
+
|
|
49
|
+
// Execute method under test
|
|
50
|
+
const result = await controller.methodName(params);
|
|
51
|
+
|
|
52
|
+
// Verify results
|
|
53
|
+
expect(result).toMatchObject(expectedResult);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Mocking External Dependencies
|
|
60
|
+
|
|
61
|
+
### Module Functions
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
const mockFunction = vi.fn();
|
|
65
|
+
|
|
66
|
+
vi.mock('module-name', () => ({
|
|
67
|
+
functionName: mockFunction,
|
|
68
|
+
}));
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Node.js Core Modules
|
|
72
|
+
|
|
73
|
+
Example: mocking `child_process.exec` and `util.promisify`:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
const mockExecImpl = vi.fn();
|
|
77
|
+
|
|
78
|
+
vi.mock('child_process', () => ({
|
|
79
|
+
exec: vi.fn((cmd, callback) => {
|
|
80
|
+
return mockExecImpl(cmd, callback);
|
|
81
|
+
}),
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
vi.mock('util', () => ({
|
|
85
|
+
promisify: vi.fn((fn) => {
|
|
86
|
+
return async (cmd: string) => {
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
mockExecImpl(cmd, (error: Error | null, result: any) => {
|
|
89
|
+
if (error) reject(error);
|
|
90
|
+
else resolve(result);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
}),
|
|
95
|
+
}));
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Best Practices
|
|
99
|
+
|
|
100
|
+
1. **Isolate tests**: Use `beforeEach` to reset mocks and state
|
|
101
|
+
2. **Comprehensive coverage**: Test normal flows, edge cases, and error handling
|
|
102
|
+
3. **Clear naming**: Test names should describe content and expected results
|
|
103
|
+
4. **Avoid implementation details**: Test behavior, not implementation
|
|
104
|
+
5. **Mock external dependencies**: Use `vi.mock()` for all external dependencies
|
|
105
|
+
|
|
106
|
+
## Example: Testing IPC Event Handler
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
it('should handle IPC event correctly', async () => {
|
|
110
|
+
mockSomething.mockReturnValue({ result: 'success' });
|
|
111
|
+
|
|
112
|
+
const result = await controller.ipcMethodName({
|
|
113
|
+
param1: 'value1',
|
|
114
|
+
param2: 'value2',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(result).toEqual({
|
|
118
|
+
success: true,
|
|
119
|
+
data: { result: 'success' },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(mockSomething).toHaveBeenCalledWith('value1', 'value2');
|
|
123
|
+
});
|
|
124
|
+
```
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Electron IPC Testing Strategy
|
|
2
|
+
|
|
3
|
+
For Electron IPC tests, use **Mock return values** instead of real Electron environment.
|
|
4
|
+
|
|
5
|
+
## Basic Mock Setup
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { vi } from 'vitest';
|
|
9
|
+
import { electronIpcClient } from '@/server/modules/ElectronIPCClient';
|
|
10
|
+
|
|
11
|
+
vi.mock('@/server/modules/ElectronIPCClient', () => ({
|
|
12
|
+
electronIpcClient: {
|
|
13
|
+
getFilePathById: vi.fn(),
|
|
14
|
+
deleteFiles: vi.fn(),
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Setting Mock Behavior
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.resetAllMocks();
|
|
24
|
+
vi.mocked(electronIpcClient.getFilePathById).mockResolvedValue('/path/to/file.txt');
|
|
25
|
+
vi.mocked(electronIpcClient.deleteFiles).mockResolvedValue({ success: true });
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Testing Different Scenarios
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
it('should handle successful file deletion', async () => {
|
|
33
|
+
vi.mocked(electronIpcClient.deleteFiles).mockResolvedValue({ success: true });
|
|
34
|
+
|
|
35
|
+
const result = await service.deleteFiles(['desktop://file1.txt']);
|
|
36
|
+
|
|
37
|
+
expect(electronIpcClient.deleteFiles).toHaveBeenCalledWith(['desktop://file1.txt']);
|
|
38
|
+
expect(result.success).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should handle file deletion failure', async () => {
|
|
42
|
+
vi.mocked(electronIpcClient.deleteFiles).mockRejectedValue(new Error('Delete failed'));
|
|
43
|
+
|
|
44
|
+
const result = await service.deleteFiles(['desktop://file1.txt']);
|
|
45
|
+
|
|
46
|
+
expect(result.success).toBe(false);
|
|
47
|
+
expect(result.errors).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Advantages
|
|
52
|
+
|
|
53
|
+
1. **Environment simplification**: No complex Electron setup
|
|
54
|
+
2. **Controlled testing**: Precise control over IPC return values
|
|
55
|
+
3. **Scenario coverage**: Easy to test success/failure cases
|
|
56
|
+
4. **Speed**: Mock calls are faster than real IPC
|
|
57
|
+
|
|
58
|
+
## Notes
|
|
59
|
+
|
|
60
|
+
- Ensure mock behavior matches real IPC interface
|
|
61
|
+
- Use `vi.mocked()` for type safety
|
|
62
|
+
- Reset mocks in `beforeEach` to avoid test interference
|
|
63
|
+
- Verify both return values and that IPC methods were called correctly
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Zustand Store Action Testing Guide
|
|
2
|
+
|
|
3
|
+
## Basic Structure
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { act, renderHook } from '@testing-library/react';
|
|
7
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
import { useChatStore } from '../../store';
|
|
9
|
+
|
|
10
|
+
vi.mock('zustand/traditional');
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
useChatStore.setState({
|
|
15
|
+
activeId: 'test-session-id',
|
|
16
|
+
messagesMap: {},
|
|
17
|
+
loadingIds: [],
|
|
18
|
+
}, false);
|
|
19
|
+
|
|
20
|
+
vi.spyOn(messageService, 'createMessage').mockResolvedValue('new-message-id');
|
|
21
|
+
|
|
22
|
+
act(() => {
|
|
23
|
+
useChatStore.setState({
|
|
24
|
+
refreshMessages: vi.fn(),
|
|
25
|
+
internal_coreProcessMessage: vi.fn(),
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
vi.restoreAllMocks();
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Key Principles
|
|
36
|
+
|
|
37
|
+
### 1. Spy Direct Dependencies Only
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// ✅ Good: Spy on direct dependency
|
|
41
|
+
const fetchAIChatSpy = vi.spyOn(result.current, 'internal_fetchAIChatMessage')
|
|
42
|
+
.mockResolvedValue({ isFunctionCall: false, content: 'AI response' });
|
|
43
|
+
|
|
44
|
+
// ❌ Bad: Spy on lower-level implementation
|
|
45
|
+
const streamSpy = vi.spyOn(chatService, 'createAssistantMessageStream')
|
|
46
|
+
.mockImplementation(...);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Minimize Global Spies
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// ✅ Spy only when needed
|
|
53
|
+
it('should process message', async () => {
|
|
54
|
+
const streamSpy = vi.spyOn(chatService, 'createAssistantMessageStream')
|
|
55
|
+
.mockImplementation(...);
|
|
56
|
+
// test logic
|
|
57
|
+
streamSpy.mockRestore();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ❌ Don't setup all spies globally
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
vi.spyOn(chatService, 'createAssistantMessageStream').mockResolvedValue({});
|
|
63
|
+
vi.spyOn(fileService, 'uploadFile').mockResolvedValue({});
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 3. Use act() for Async Operations
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
it('should send message', async () => {
|
|
71
|
+
const { result } = renderHook(() => useChatStore());
|
|
72
|
+
|
|
73
|
+
await act(async () => {
|
|
74
|
+
await result.current.sendMessage({ message: 'Hello' });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(messageService.createMessage).toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 4. Test Organization
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
describe('sendMessage', () => {
|
|
85
|
+
describe('validation', () => {
|
|
86
|
+
it('should not send when session is inactive');
|
|
87
|
+
it('should not send when message is empty');
|
|
88
|
+
});
|
|
89
|
+
describe('message creation', () => {
|
|
90
|
+
it('should create user message and trigger AI processing');
|
|
91
|
+
});
|
|
92
|
+
describe('error handling', () => {
|
|
93
|
+
it('should handle message creation errors gracefully');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Streaming Response Mock
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
it('should handle streaming chunks', async () => {
|
|
102
|
+
const { result } = renderHook(() => useChatStore());
|
|
103
|
+
|
|
104
|
+
const streamSpy = vi.spyOn(chatService, 'createAssistantMessageStream')
|
|
105
|
+
.mockImplementation(async ({ onMessageHandle, onFinish }) => {
|
|
106
|
+
await onMessageHandle?.({ type: 'text', text: 'Hello' } as any);
|
|
107
|
+
await onMessageHandle?.({ type: 'text', text: ' World' } as any);
|
|
108
|
+
await onFinish?.('Hello World', {});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await act(async () => {
|
|
112
|
+
await result.current.internal_fetchAIChatMessage({...});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
streamSpy.mockRestore();
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## SWR Hook Testing
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
it('should fetch data', async () => {
|
|
123
|
+
const mockData = [{ id: '1', name: 'Item 1' }];
|
|
124
|
+
vi.spyOn(discoverService, 'getPluginCategories').mockResolvedValue(mockData);
|
|
125
|
+
|
|
126
|
+
const { result } = renderHook(() => useStore.getState().usePluginCategories(params));
|
|
127
|
+
|
|
128
|
+
await waitFor(() => {
|
|
129
|
+
expect(result.current.data).toEqual(mockData);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Key points for SWR:**
|
|
135
|
+
- DO NOT mock useSWR - let it use real implementation
|
|
136
|
+
- Only mock service methods (fetchers)
|
|
137
|
+
- Use `waitFor` for async operations
|
|
138
|
+
|
|
139
|
+
## Anti-Patterns
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// ❌ Don't mock entire store
|
|
143
|
+
vi.mock('../../store', () => ({ useChatStore: vi.fn(() => ({...})) }));
|
|
144
|
+
|
|
145
|
+
// ❌ Don't test internal state structure
|
|
146
|
+
expect(result.current.messagesMap).toHaveProperty('test-session');
|
|
147
|
+
|
|
148
|
+
// ✅ Test behavior instead
|
|
149
|
+
expect(result.current.refreshMessages).toHaveBeenCalled();
|
|
150
|
+
```
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: typescript
|
|
3
|
+
description: TypeScript code style and optimization guidelines. Use when writing TypeScript code (.ts, .tsx, .mts files), reviewing code quality, or implementing type-safe patterns. Triggers on TypeScript development, type safety questions, or code style discussions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TypeScript Code Style Guide
|
|
7
|
+
|
|
8
|
+
## Types and Type Safety
|
|
9
|
+
|
|
10
|
+
- Avoid explicit type annotations when TypeScript can infer
|
|
11
|
+
- Avoid implicitly `any`; explicitly type when necessary
|
|
12
|
+
- Use accurate types: prefer `Record<PropertyKey, unknown>` over `object` or `any`
|
|
13
|
+
- Prefer `interface` for object shapes (e.g., React props); use `type` for unions/intersections
|
|
14
|
+
- Prefer `as const satisfies XyzInterface` over plain `as const`
|
|
15
|
+
- Prefer `@ts-expect-error` over `@ts-ignore` over `as any`
|
|
16
|
+
- Avoid meaningless null/undefined parameters; design strict function contracts
|
|
17
|
+
|
|
18
|
+
## Async Patterns
|
|
19
|
+
|
|
20
|
+
- Prefer `async`/`await` over callbacks or `.then()` chains
|
|
21
|
+
- Prefer async APIs over sync ones (avoid `*Sync`)
|
|
22
|
+
- Use promise-based variants: `import { readFile } from 'fs/promises'`
|
|
23
|
+
- Use `Promise.all`, `Promise.race` for concurrent operations where safe
|
|
24
|
+
|
|
25
|
+
## Code Structure
|
|
26
|
+
|
|
27
|
+
- Prefer object destructuring
|
|
28
|
+
- Use consistent, descriptive naming; avoid obscure abbreviations
|
|
29
|
+
- Replace magic numbers/strings with well-named constants
|
|
30
|
+
- Defer formatting to tooling
|
|
31
|
+
|
|
32
|
+
## UI and Theming
|
|
33
|
+
|
|
34
|
+
- Use `@lobehub/ui`, Ant Design components instead of raw HTML tags
|
|
35
|
+
- Design for dark mode and mobile responsiveness
|
|
36
|
+
- Use `antd-style` token system instead of hard-coded colors
|
|
37
|
+
|
|
38
|
+
## Performance
|
|
39
|
+
|
|
40
|
+
- Prefer `for…of` loops over index-based `for` loops
|
|
41
|
+
- Reuse existing utils in `packages/utils` or installed npm packages
|
|
42
|
+
- Query only required columns from database
|
|
43
|
+
|
|
44
|
+
## Time Consistency
|
|
45
|
+
|
|
46
|
+
- Assign `Date.now()` to a constant once and reuse for consistency
|
|
47
|
+
|
|
48
|
+
## Logging
|
|
49
|
+
|
|
50
|
+
- Never log user private information (API keys, etc.)
|
|
51
|
+
- Don't use `import { log } from 'debug'` directly (logs to console)
|
|
52
|
+
- Use `console.error` in catch blocks instead of debug package
|