@lobehub/lobehub 2.0.0-next.51 → 2.0.0-next.52
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/apps/desktop/src/main/controllers/LocalFileCtr.ts +25 -5
- package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +4 -1
- package/apps/desktop/src/main/modules/fileSearch/__tests__/macOS.integration.test.ts +357 -0
- package/apps/desktop/src/main/modules/fileSearch/impl/macOS.ts +30 -22
- package/changelog/v1.json +9 -0
- package/locales/ar/models.json +119 -126
- package/locales/ar/plugin.json +1 -1
- package/locales/bg-BG/models.json +104 -132
- package/locales/bg-BG/plugin.json +1 -1
- package/locales/de-DE/models.json +119 -126
- package/locales/de-DE/plugin.json +1 -1
- package/locales/en-US/models.json +167 -126
- package/locales/en-US/plugin.json +1 -1
- package/locales/es-ES/models.json +119 -126
- package/locales/es-ES/plugin.json +1 -1
- package/locales/fa-IR/models.json +119 -126
- package/locales/fa-IR/plugin.json +1 -1
- package/locales/fr-FR/models.json +119 -126
- package/locales/fr-FR/plugin.json +1 -1
- package/locales/it-IT/models.json +119 -126
- package/locales/it-IT/plugin.json +1 -1
- package/locales/ja-JP/models.json +119 -126
- package/locales/ja-JP/plugin.json +1 -1
- package/locales/ko-KR/models.json +119 -126
- package/locales/ko-KR/plugin.json +1 -1
- package/locales/nl-NL/models.json +119 -126
- package/locales/nl-NL/plugin.json +1 -1
- package/locales/pl-PL/models.json +119 -126
- package/locales/pl-PL/plugin.json +1 -1
- package/locales/pt-BR/models.json +119 -126
- package/locales/pt-BR/plugin.json +1 -1
- package/locales/ru-RU/models.json +119 -126
- package/locales/ru-RU/plugin.json +1 -1
- package/locales/tr-TR/models.json +119 -126
- package/locales/tr-TR/plugin.json +1 -1
- package/locales/vi-VN/models.json +119 -126
- package/locales/vi-VN/plugin.json +1 -1
- package/locales/zh-CN/models.json +173 -80
- package/locales/zh-CN/plugin.json +1 -1
- package/locales/zh-TW/models.json +119 -126
- package/locales/zh-TW/plugin.json +1 -1
- package/package.json +1 -1
- package/packages/electron-client-ipc/src/types/localSystem.ts +26 -2
- package/packages/model-runtime/src/core/contextBuilders/openai.test.ts +58 -0
- package/packages/model-runtime/src/core/contextBuilders/openai.ts +24 -10
- package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +3 -2
- package/packages/model-runtime/src/providers/openai/index.test.ts +44 -0
- package/packages/types/src/tool/builtin.ts +6 -4
- package/src/features/Conversation/Messages/Assistant/Tool/Render/LoadingPlaceholder/index.tsx +3 -3
- package/src/features/Conversation/Messages/Group/Tool/Render/Intervention/index.tsx +2 -2
- package/src/features/Conversation/Messages/Group/Tool/Render/LoadingPlaceholder/index.tsx +3 -3
- package/src/features/PluginsUI/Render/BuiltinType/index.test.tsx +10 -4
- package/src/features/PluginsUI/Render/BuiltinType/index.tsx +2 -2
- package/src/locales/default/plugin.ts +1 -1
- package/src/services/chat/chat.test.ts +1 -0
- package/src/store/aiInfra/slices/aiProvider/__tests__/selectors.test.ts +62 -0
- package/src/store/aiInfra/slices/aiProvider/selectors.ts +1 -1
- package/src/tools/code-interpreter/Render/index.tsx +1 -1
- package/src/tools/interventions.ts +28 -4
- package/src/tools/local-system/Placeholder/ListFiles.tsx +3 -5
- package/src/tools/local-system/Placeholder/SearchFiles.tsx +2 -5
- package/src/tools/local-system/Render/ListFiles/index.tsx +16 -21
- package/src/tools/local-system/Render/RenameLocalFile/index.tsx +15 -20
- package/src/tools/local-system/Render/RunCommand/index.tsx +67 -70
- package/src/tools/local-system/Render/SearchFiles/SearchQuery/index.tsx +0 -1
- package/src/tools/local-system/Render/SearchFiles/index.tsx +15 -20
- package/src/tools/local-system/Render/WriteFile/index.tsx +2 -8
- package/src/tools/local-system/index.ts +4 -4
- package/src/tools/local-system/systemRole.ts +1 -1
- package/src/tools/placeholders.ts +39 -8
- package/src/tools/renders.ts +56 -9
- package/src/tools/web-browsing/Placeholder/{PageContent.tsx → CrawlMultiPages.tsx} +4 -1
- package/src/tools/web-browsing/Placeholder/CrawlSinglePage.tsx +12 -0
- package/src/tools/web-browsing/Placeholder/Search.tsx +4 -4
- package/src/tools/web-browsing/Render/CrawlMultiPages.tsx +15 -0
- package/src/tools/web-browsing/Render/CrawlSinglePage.tsx +15 -0
- package/src/tools/web-browsing/Render/Search/index.tsx +39 -44
- package/packages/database/migrations/0044_add_tool_intervention.sql +0 -1
- package/src/tools/local-system/Intervention/index.tsx +0 -17
- package/src/tools/local-system/Placeholder/index.tsx +0 -25
- package/src/tools/local-system/Render/index.tsx +0 -42
- package/src/tools/web-browsing/Placeholder/index.tsx +0 -40
- package/src/tools/web-browsing/Render/index.tsx +0 -57
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.52](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.51...v2.0.0-next.52)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2025-11-13**</sup>
|
|
8
|
+
|
|
9
|
+
#### 🐛 Bug Fixes
|
|
10
|
+
|
|
11
|
+
- **misc**: Filter out reasoning fields from messages in ChatCompletion API.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### What's fixed
|
|
19
|
+
|
|
20
|
+
- **misc**: Filter out reasoning fields from messages in ChatCompletion API, closes [#10203](https://github.com/lobehub/lobe-chat/issues/10203) [#10193](https://github.com/lobehub/lobe-chat/issues/10193) ([5f28b2c](https://github.com/lobehub/lobe-chat/commit/5f28b2c))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
## [Version 2.0.0-next.51](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.50...v2.0.0-next.51)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2025-11-13**</sup>
|
|
@@ -467,15 +467,35 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
467
467
|
*/
|
|
468
468
|
@ipcClientEvent('searchLocalFiles')
|
|
469
469
|
async handleLocalFilesSearch(params: LocalSearchFilesParams): Promise<FileResult[]> {
|
|
470
|
-
logger.debug('Received file search request:', {
|
|
470
|
+
logger.debug('Received file search request:', {
|
|
471
|
+
directory: params.directory,
|
|
472
|
+
keywords: params.keywords,
|
|
473
|
+
});
|
|
471
474
|
|
|
472
|
-
|
|
473
|
-
|
|
475
|
+
// Build search options from params, mapping directory to onlyIn
|
|
476
|
+
const options: SearchOptions = {
|
|
477
|
+
contentContains: params.contentContains,
|
|
478
|
+
createdAfter: params.createdAfter ? new Date(params.createdAfter) : undefined,
|
|
479
|
+
createdBefore: params.createdBefore ? new Date(params.createdBefore) : undefined,
|
|
480
|
+
detailed: params.detailed,
|
|
481
|
+
exclude: params.exclude,
|
|
482
|
+
fileTypes: params.fileTypes,
|
|
483
|
+
keywords: params.keywords,
|
|
484
|
+
limit: params.limit || 30,
|
|
485
|
+
liveUpdate: params.liveUpdate,
|
|
486
|
+
modifiedAfter: params.modifiedAfter ? new Date(params.modifiedAfter) : undefined,
|
|
487
|
+
modifiedBefore: params.modifiedBefore ? new Date(params.modifiedBefore) : undefined,
|
|
488
|
+
onlyIn: params.directory, // Map directory param to onlyIn option
|
|
489
|
+
sortBy: params.sortBy,
|
|
490
|
+
sortDirection: params.sortDirection,
|
|
474
491
|
};
|
|
475
492
|
|
|
476
493
|
try {
|
|
477
|
-
const results = await this.searchService.search(
|
|
478
|
-
logger.debug('File search completed', {
|
|
494
|
+
const results = await this.searchService.search(options.keywords, options);
|
|
495
|
+
logger.debug('File search completed', {
|
|
496
|
+
count: results.length,
|
|
497
|
+
directory: params.directory,
|
|
498
|
+
});
|
|
479
499
|
return results;
|
|
480
500
|
} catch (error) {
|
|
481
501
|
logger.error('File search failed:', error);
|
|
@@ -345,7 +345,10 @@ describe('LocalFileCtr', () => {
|
|
|
345
345
|
const result = await localFileCtr.handleLocalFilesSearch({ keywords: 'test' });
|
|
346
346
|
|
|
347
347
|
expect(result).toEqual(mockResults);
|
|
348
|
-
expect(mockSearchService.search).toHaveBeenCalledWith('test', {
|
|
348
|
+
expect(mockSearchService.search).toHaveBeenCalledWith('test', {
|
|
349
|
+
keywords: 'test',
|
|
350
|
+
limit: 30,
|
|
351
|
+
});
|
|
349
352
|
});
|
|
350
353
|
|
|
351
354
|
it('should return empty array on search error', async () => {
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { MacOSSearchServiceImpl } from '../impl/macOS';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* macOS File Search Integration Tests
|
|
8
|
+
*
|
|
9
|
+
* These tests run against the real macOS Spotlight service
|
|
10
|
+
* using files in the current repository.
|
|
11
|
+
*
|
|
12
|
+
* Run with: bunx vitest run 'macOS.integration.test'
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Get repository root path (assumes test runs from apps/desktop)
|
|
16
|
+
const repoRoot = path.resolve(__dirname, '../../../../..');
|
|
17
|
+
|
|
18
|
+
describe.skipIf(process.platform !== 'darwin')('MacOSSearchServiceImpl Integration', () => {
|
|
19
|
+
const searchService = new MacOSSearchServiceImpl();
|
|
20
|
+
|
|
21
|
+
describe('checkSearchServiceStatus', () => {
|
|
22
|
+
it('should verify Spotlight is available on macOS', async () => {
|
|
23
|
+
const isAvailable = await searchService.checkSearchServiceStatus();
|
|
24
|
+
|
|
25
|
+
expect(isAvailable).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('search for known repository files', () => {
|
|
30
|
+
it('should find package.json in repo root', async () => {
|
|
31
|
+
const results = await searchService.search({
|
|
32
|
+
keywords: 'package.json',
|
|
33
|
+
limit: 10,
|
|
34
|
+
onlyIn: repoRoot,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(results.length).toBeGreaterThan(0);
|
|
38
|
+
|
|
39
|
+
// Should find at least one package.json
|
|
40
|
+
const packageJson = results.find((r) => r.name === 'package.json');
|
|
41
|
+
expect(packageJson).toBeDefined();
|
|
42
|
+
expect(packageJson!.type).toBe('json');
|
|
43
|
+
expect(packageJson!.path).toContain(repoRoot);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should find README files', async () => {
|
|
47
|
+
const results = await searchService.search({
|
|
48
|
+
keywords: 'README',
|
|
49
|
+
limit: 10,
|
|
50
|
+
onlyIn: repoRoot,
|
|
51
|
+
});
|
|
52
|
+
expect(results.length).toBeGreaterThan(0);
|
|
53
|
+
|
|
54
|
+
// Should contain markdown files
|
|
55
|
+
const mdFile = results.find((r) => r.type === 'md');
|
|
56
|
+
expect(mdFile).toBeDefined();
|
|
57
|
+
expect(mdFile!.name).toMatch(/README/i);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should find TypeScript files', async () => {
|
|
61
|
+
const results = await searchService.search({
|
|
62
|
+
keywords: 'macOS',
|
|
63
|
+
limit: 10,
|
|
64
|
+
onlyIn: repoRoot,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(results.length).toBeGreaterThan(0);
|
|
68
|
+
|
|
69
|
+
// Should find the macOS.ts implementation file
|
|
70
|
+
const macOSFile = results.find((r) => r.name.includes('macOS') && r.type === 'ts');
|
|
71
|
+
expect(macOSFile).toBeDefined();
|
|
72
|
+
expect(macOSFile!.contentType).toBe('code');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should find files in apps/desktop directory', async () => {
|
|
76
|
+
const desktopPath = path.join(repoRoot, 'apps/desktop');
|
|
77
|
+
|
|
78
|
+
const results = await searchService.search({
|
|
79
|
+
keywords: 'src',
|
|
80
|
+
limit: 20,
|
|
81
|
+
onlyIn: desktopPath,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Spotlight indexing may not be complete for this directory
|
|
85
|
+
// so we make the test lenient
|
|
86
|
+
if (results.length > 0) {
|
|
87
|
+
// All results should be within apps/desktop
|
|
88
|
+
results.forEach((result) => {
|
|
89
|
+
expect(result.path).toContain('apps/desktop');
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
// eslint-disable-next-line no-console
|
|
93
|
+
console.warn(
|
|
94
|
+
'⚠️ No results found in apps/desktop - Spotlight indexing may not be complete',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// At minimum, verify the search completed without error
|
|
99
|
+
expect(Array.isArray(results)).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should find test files', async () => {
|
|
103
|
+
const results = await searchService.search({
|
|
104
|
+
keywords: 'test.ts',
|
|
105
|
+
limit: 10,
|
|
106
|
+
onlyIn: repoRoot,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
expect(results.length).toBeGreaterThan(0);
|
|
110
|
+
|
|
111
|
+
// Should find test files
|
|
112
|
+
const testFile = results.find((r) => r.name.endsWith('.test.ts'));
|
|
113
|
+
expect(testFile).toBeDefined();
|
|
114
|
+
expect(testFile!.path).toContain('__tests__');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('search with filters', () => {
|
|
119
|
+
it('should respect limit parameter', async () => {
|
|
120
|
+
const limit = 3;
|
|
121
|
+
const results = await searchService.search({
|
|
122
|
+
keywords: 'src',
|
|
123
|
+
limit,
|
|
124
|
+
onlyIn: repoRoot,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(results.length).toBeLessThanOrEqual(limit);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should search in specific subdirectory only', async () => {
|
|
131
|
+
const srcPath = path.join(repoRoot, 'apps/desktop/src');
|
|
132
|
+
|
|
133
|
+
const results = await searchService.search({
|
|
134
|
+
keywords: 'index',
|
|
135
|
+
limit: 10,
|
|
136
|
+
onlyIn: srcPath,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// All results should be within the specified directory
|
|
140
|
+
results.forEach((result) => {
|
|
141
|
+
expect(result.path).toContain('apps/desktop/src');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should return empty array for non-existent keywords', async () => {
|
|
146
|
+
const results = await searchService.search({
|
|
147
|
+
keywords: 'xyzabc123unlikely-keyword-that-does-not-exist-12345',
|
|
148
|
+
limit: 5,
|
|
149
|
+
onlyIn: repoRoot,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(results).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('file type detection', () => {
|
|
157
|
+
it('should correctly identify TypeScript files', async () => {
|
|
158
|
+
const results = await searchService.search({
|
|
159
|
+
keywords: 'LocalFileCtr',
|
|
160
|
+
limit: 5,
|
|
161
|
+
onlyIn: repoRoot,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const tsFile = results.find((r) => r.name === 'LocalFileCtr.ts');
|
|
165
|
+
if (tsFile) {
|
|
166
|
+
expect(tsFile.type).toBe('ts');
|
|
167
|
+
expect(tsFile.contentType).toBe('code');
|
|
168
|
+
expect(tsFile.isDirectory).toBe(false);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should correctly identify JSON files', async () => {
|
|
173
|
+
const results = await searchService.search({
|
|
174
|
+
keywords: 'tsconfig',
|
|
175
|
+
limit: 5,
|
|
176
|
+
onlyIn: repoRoot,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const jsonFile = results.find((r) => r.name.includes('tsconfig') && r.type === 'json');
|
|
180
|
+
if (jsonFile) {
|
|
181
|
+
expect(jsonFile.type).toBe('json');
|
|
182
|
+
expect(jsonFile.contentType).toBe('code');
|
|
183
|
+
expect(jsonFile.size).toBeGreaterThan(0);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should correctly identify directories', async () => {
|
|
188
|
+
const results = await searchService.search({
|
|
189
|
+
keywords: '__tests__',
|
|
190
|
+
limit: 10,
|
|
191
|
+
onlyIn: repoRoot,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const testDir = results.find((r) => r.name === '__tests__' && r.isDirectory);
|
|
195
|
+
if (testDir) {
|
|
196
|
+
expect(testDir.isDirectory).toBe(true);
|
|
197
|
+
expect(testDir.type).toBe('');
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should correctly identify markdown files', async () => {
|
|
202
|
+
const results = await searchService.search({
|
|
203
|
+
keywords: 'CLAUDE.md',
|
|
204
|
+
limit: 5,
|
|
205
|
+
onlyIn: repoRoot,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const mdFile = results.find((r) => r.name === 'CLAUDE.md');
|
|
209
|
+
if (mdFile) {
|
|
210
|
+
expect(mdFile.type).toBe('md');
|
|
211
|
+
expect(mdFile.contentType).toBe('text');
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('file metadata', () => {
|
|
217
|
+
it('should return valid file metadata', async () => {
|
|
218
|
+
const results = await searchService.search({
|
|
219
|
+
keywords: 'package.json',
|
|
220
|
+
limit: 1,
|
|
221
|
+
onlyIn: repoRoot,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
expect(results.length).toBeGreaterThan(0);
|
|
225
|
+
|
|
226
|
+
const file = results[0];
|
|
227
|
+
|
|
228
|
+
// Verify all metadata fields are present
|
|
229
|
+
expect(file.path).toBeTruthy();
|
|
230
|
+
expect(file.name).toBeTruthy();
|
|
231
|
+
expect(typeof file.isDirectory).toBe('boolean');
|
|
232
|
+
expect(typeof file.size).toBe('number');
|
|
233
|
+
expect(file.size).toBeGreaterThanOrEqual(0);
|
|
234
|
+
expect(file.type).toBeDefined();
|
|
235
|
+
expect(file.contentType).toBeDefined();
|
|
236
|
+
expect(file.modifiedTime).toBeInstanceOf(Date);
|
|
237
|
+
expect(file.createdTime).toBeInstanceOf(Date);
|
|
238
|
+
expect(file.lastAccessTime).toBeInstanceOf(Date);
|
|
239
|
+
|
|
240
|
+
// Dates should be valid
|
|
241
|
+
expect(file.modifiedTime.getTime()).toBeGreaterThan(0);
|
|
242
|
+
expect(file.createdTime.getTime()).toBeGreaterThan(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should handle files with different extensions', async () => {
|
|
246
|
+
const testCases = [
|
|
247
|
+
{ keyword: '.ts', expectedType: 'ts', expectedContentType: 'code' },
|
|
248
|
+
{ keyword: '.json', expectedType: 'json', expectedContentType: 'code' },
|
|
249
|
+
{ keyword: '.txt', expectedType: 'txt', expectedContentType: 'text' },
|
|
250
|
+
];
|
|
251
|
+
|
|
252
|
+
for (const { keyword, expectedType, expectedContentType } of testCases) {
|
|
253
|
+
const results = await searchService.search({
|
|
254
|
+
keywords: keyword,
|
|
255
|
+
limit: 5,
|
|
256
|
+
onlyIn: repoRoot,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (results.length > 0) {
|
|
260
|
+
const file = results.find((r) => r.type === expectedType);
|
|
261
|
+
if (file) {
|
|
262
|
+
expect(file.type).toBe(expectedType);
|
|
263
|
+
expect(file.contentType).toBe(expectedContentType);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('search accuracy after fix', () => {
|
|
271
|
+
it('should use fuzzy matching instead of exact phrase', async () => {
|
|
272
|
+
// Test the fix: keywords should do fuzzy matching, not exact phrase
|
|
273
|
+
// Before fix: "local file" would only match exact phrase "local file"
|
|
274
|
+
// After fix: "local file" should match "LocalFileCtr" (contains "local" and "file")
|
|
275
|
+
|
|
276
|
+
const results = await searchService.search({
|
|
277
|
+
keywords: 'LocalFile',
|
|
278
|
+
limit: 10,
|
|
279
|
+
onlyIn: repoRoot,
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
expect(results.length).toBeGreaterThan(0);
|
|
283
|
+
|
|
284
|
+
// Should find LocalFileCtr.ts or similar files
|
|
285
|
+
const found = results.some(
|
|
286
|
+
(r) => r.name.includes('LocalFile') || r.name.includes('localFile'),
|
|
287
|
+
);
|
|
288
|
+
expect(found).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should handle paths with spaces correctly', async () => {
|
|
292
|
+
// Test the fix: command args should be properly split
|
|
293
|
+
// This test verifies spawn receives correct arguments array
|
|
294
|
+
|
|
295
|
+
const pathWithSpaces = repoRoot; // May contain spaces in CI or certain setups
|
|
296
|
+
const results = await searchService.search({
|
|
297
|
+
keywords: 'test',
|
|
298
|
+
limit: 5,
|
|
299
|
+
onlyIn: pathWithSpaces,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Should not throw error even if path contains spaces
|
|
303
|
+
expect(Array.isArray(results)).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should search case-insensitively', async () => {
|
|
307
|
+
// The "cd" flag in kMDItemFSName makes it case-insensitive
|
|
308
|
+
|
|
309
|
+
const lowerResults = await searchService.search({
|
|
310
|
+
keywords: 'readme',
|
|
311
|
+
limit: 5,
|
|
312
|
+
onlyIn: repoRoot,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const upperResults = await searchService.search({
|
|
316
|
+
keywords: 'README',
|
|
317
|
+
limit: 5,
|
|
318
|
+
onlyIn: repoRoot,
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Both searches should find similar files
|
|
322
|
+
expect(lowerResults.length).toBeGreaterThan(0);
|
|
323
|
+
expect(upperResults.length).toBeGreaterThan(0);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe('error handling', () => {
|
|
328
|
+
it('should handle non-existent directory gracefully', async () => {
|
|
329
|
+
const nonExistentPath = path.join(repoRoot, 'this-directory-does-not-exist-12345');
|
|
330
|
+
|
|
331
|
+
const results = await searchService.search({
|
|
332
|
+
keywords: 'test',
|
|
333
|
+
limit: 5,
|
|
334
|
+
onlyIn: nonExistentPath,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Should return empty array instead of throwing
|
|
338
|
+
expect(results).toEqual([]);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe('updateSearchIndex', () => {
|
|
343
|
+
it.skip('should handle index update request', async () => {
|
|
344
|
+
// Index update requires elevated permissions, may fail in restricted environments
|
|
345
|
+
const result = await searchService.updateSearchIndex(repoRoot);
|
|
346
|
+
|
|
347
|
+
// Should return boolean (true if succeeded, false if failed)
|
|
348
|
+
expect(typeof result).toBe('boolean');
|
|
349
|
+
}, 15000); // Index update can take time
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Skip message for non-macOS platforms
|
|
354
|
+
if (process.platform !== 'darwin') {
|
|
355
|
+
// eslint-disable-next-line no-console
|
|
356
|
+
console.log('⏭️ Skipping macOS integration tests on', process.platform, '(only runs on darwin)');
|
|
357
|
+
}
|
|
@@ -23,12 +23,11 @@ export class MacOSSearchServiceImpl extends FileSearchImpl {
|
|
|
23
23
|
*/
|
|
24
24
|
async search(options: SearchOptions): Promise<FileResult[]> {
|
|
25
25
|
// Build the command first, regardless of execution method
|
|
26
|
-
const
|
|
27
|
-
logger.debug(`Executing command: ${
|
|
26
|
+
const { cmd, args, commandString } = this.buildSearchCommand(options);
|
|
27
|
+
logger.debug(`Executing command: ${commandString}`);
|
|
28
28
|
|
|
29
29
|
// Use spawn for both live and non-live updates to handle large outputs
|
|
30
30
|
return new Promise((resolve, reject) => {
|
|
31
|
-
const [cmd, ...args] = command.split(' ');
|
|
32
31
|
const childProcess = spawn(cmd, args);
|
|
33
32
|
|
|
34
33
|
let results: string[] = []; // Store raw file paths
|
|
@@ -137,31 +136,39 @@ export class MacOSSearchServiceImpl extends FileSearchImpl {
|
|
|
137
136
|
/**
|
|
138
137
|
* Build mdfind command string
|
|
139
138
|
* @param options Search options
|
|
140
|
-
* @returns
|
|
139
|
+
* @returns Command components (cmd, args array, and command string for logging)
|
|
141
140
|
*/
|
|
142
|
-
private buildSearchCommand(options: SearchOptions):
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
private buildSearchCommand(options: SearchOptions): {
|
|
142
|
+
args: string[];
|
|
143
|
+
cmd: string;
|
|
144
|
+
commandString: string;
|
|
145
|
+
} {
|
|
146
|
+
// Command and arguments array
|
|
147
|
+
const cmd = 'mdfind';
|
|
148
|
+
const args: string[] = [];
|
|
148
149
|
|
|
149
150
|
// macOS mdfind doesn't support -limit parameter, we'll limit results in post-processing
|
|
150
151
|
|
|
151
152
|
// Search in specific directory
|
|
152
153
|
if (options.onlyIn) {
|
|
153
|
-
|
|
154
|
+
args.push('-onlyin', options.onlyIn);
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
// Live update
|
|
157
158
|
if (options.liveUpdate) {
|
|
158
|
-
|
|
159
|
+
args.push('-live');
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
// Detailed metadata
|
|
162
163
|
if (options.detailed) {
|
|
163
|
-
|
|
164
|
-
'-attr
|
|
164
|
+
args.push(
|
|
165
|
+
'-attr',
|
|
166
|
+
'kMDItemDisplayName',
|
|
167
|
+
'kMDItemContentType',
|
|
168
|
+
'kMDItemKind',
|
|
169
|
+
'kMDItemFSSize',
|
|
170
|
+
'kMDItemFSCreationDate',
|
|
171
|
+
'kMDItemFSContentChangeDate',
|
|
165
172
|
);
|
|
166
173
|
}
|
|
167
174
|
|
|
@@ -171,9 +178,10 @@ export class MacOSSearchServiceImpl extends FileSearchImpl {
|
|
|
171
178
|
// Basic query
|
|
172
179
|
if (options.keywords) {
|
|
173
180
|
// If the query string doesn't use Spotlight query syntax (doesn't contain kMDItem properties),
|
|
174
|
-
// treat it as
|
|
181
|
+
// treat it as a flexible name search rather than exact phrase match
|
|
175
182
|
if (!options.keywords.includes('kMDItem')) {
|
|
176
|
-
|
|
183
|
+
// Use kMDItemFSName for filename matching with wildcards for better flexibility
|
|
184
|
+
queryExpression = `kMDItemFSName == "*${options.keywords.replaceAll('"', '\\"')}*"cd`;
|
|
177
185
|
} else {
|
|
178
186
|
queryExpression = options.keywords;
|
|
179
187
|
}
|
|
@@ -244,15 +252,15 @@ export class MacOSSearchServiceImpl extends FileSearchImpl {
|
|
|
244
252
|
}
|
|
245
253
|
}
|
|
246
254
|
|
|
247
|
-
//
|
|
248
|
-
if (
|
|
249
|
-
|
|
255
|
+
// Add query expression to args
|
|
256
|
+
if (queryExpression) {
|
|
257
|
+
args.push(queryExpression);
|
|
250
258
|
}
|
|
251
259
|
|
|
252
|
-
//
|
|
253
|
-
|
|
260
|
+
// Build command string for logging
|
|
261
|
+
const commandString = `${cmd} ${args.map((arg) => (arg.includes(' ') || arg.includes('*') ? `"${arg}"` : arg)).join(' ')}`;
|
|
254
262
|
|
|
255
|
-
return
|
|
263
|
+
return { args, cmd, commandString };
|
|
256
264
|
}
|
|
257
265
|
|
|
258
266
|
/**
|