bluera-knowledge 0.9.30 → 0.9.31

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bluera-knowledge",
3
- "version": "0.9.30",
3
+ "version": "0.9.31",
4
4
  "description": "CLI tool for managing knowledge stores with semantic search",
5
5
  "type": "module",
6
6
  "bin": {
@@ -935,7 +935,7 @@ describe('store command execution', () => {
935
935
  });
936
936
  });
937
937
 
938
- it('routes source to path for repo stores', async () => {
938
+ it('routes source to path for repo stores with local path', async () => {
939
939
  const mockStore: RepoStore = {
940
940
  id: createStoreId('store-2'),
941
941
  name: 'repo-store',
@@ -967,6 +967,73 @@ describe('store command execution', () => {
967
967
  });
968
968
  });
969
969
 
970
+ it('routes URL source to url for repo stores (Bug #1 fix)', async () => {
971
+ const mockStore: RepoStore = {
972
+ id: createStoreId('store-2'),
973
+ name: 'repo-url-store',
974
+ type: 'repo',
975
+ path: '/cloned/repo/path',
976
+ url: 'https://github.com/user/repo',
977
+ createdAt: new Date(),
978
+ updatedAt: new Date(),
979
+ };
980
+
981
+ mockServices.store.create.mockResolvedValue({
982
+ success: true,
983
+ data: mockStore,
984
+ });
985
+
986
+ const command = createStoreCommand(getOptions);
987
+ const createCommand = command.commands.find(c => c.name() === 'create');
988
+ const actionHandler = createCommand?._actionHandler;
989
+
990
+ createCommand.parseOptions(['--type', 'repo', '--source', 'https://github.com/user/repo']);
991
+ await actionHandler!(['repo-url-store']);
992
+
993
+ // URL should be routed to 'url' parameter, not 'path'
994
+ expect(mockServices.store.create).toHaveBeenCalledWith({
995
+ name: 'repo-url-store',
996
+ type: 'repo',
997
+ path: undefined,
998
+ url: 'https://github.com/user/repo',
999
+ description: undefined,
1000
+ tags: undefined,
1001
+ });
1002
+ });
1003
+
1004
+ it('routes http:// URL source to url for repo stores', async () => {
1005
+ const mockStore: RepoStore = {
1006
+ id: createStoreId('store-2'),
1007
+ name: 'repo-http-store',
1008
+ type: 'repo',
1009
+ path: '/cloned/repo/path',
1010
+ url: 'http://internal-git.example.com/repo',
1011
+ createdAt: new Date(),
1012
+ updatedAt: new Date(),
1013
+ };
1014
+
1015
+ mockServices.store.create.mockResolvedValue({
1016
+ success: true,
1017
+ data: mockStore,
1018
+ });
1019
+
1020
+ const command = createStoreCommand(getOptions);
1021
+ const createCommand = command.commands.find(c => c.name() === 'create');
1022
+ const actionHandler = createCommand?._actionHandler;
1023
+
1024
+ createCommand.parseOptions(['--type', 'repo', '--source', 'http://internal-git.example.com/repo']);
1025
+ await actionHandler!(['repo-http-store']);
1026
+
1027
+ expect(mockServices.store.create).toHaveBeenCalledWith({
1028
+ name: 'repo-http-store',
1029
+ type: 'repo',
1030
+ path: undefined,
1031
+ url: 'http://internal-git.example.com/repo',
1032
+ description: undefined,
1033
+ tags: undefined,
1034
+ });
1035
+ });
1036
+
970
1037
  it('routes source to url for web stores', async () => {
971
1038
  const mockStore: WebStore = {
972
1039
  id: createStoreId('store-3'),
@@ -54,12 +54,15 @@ export function createStoreCommand(getOptions: () => GlobalOptions): Command {
54
54
  }) => {
55
55
  const globalOpts = getOptions();
56
56
  const services = await createServices(globalOpts.config, globalOpts.dataDir);
57
+ let exitCode = 0;
57
58
  try {
59
+ // Detect if source is a URL (for repo stores that should clone from remote)
60
+ const isUrl = options.source.startsWith('http://') || options.source.startsWith('https://');
58
61
  const result = await services.store.create({
59
62
  name,
60
63
  type: options.type,
61
- path: options.type !== 'web' ? options.source : undefined,
62
- url: options.type === 'web' ? options.source : undefined,
64
+ path: options.type === 'file' || (options.type === 'repo' && !isUrl) ? options.source : undefined,
65
+ url: options.type === 'web' || (options.type === 'repo' && isUrl) ? options.source : undefined,
63
66
  description: options.description,
64
67
  tags: options.tags?.split(',').map((t) => t.trim()),
65
68
  });
@@ -72,11 +75,14 @@ export function createStoreCommand(getOptions: () => GlobalOptions): Command {
72
75
  }
73
76
  } else {
74
77
  console.error(`Error: ${result.error.message}`);
75
- process.exit(1);
78
+ exitCode = 1;
76
79
  }
77
80
  } finally {
78
81
  await destroyServices(services);
79
82
  }
83
+ if (exitCode !== 0) {
84
+ process.exit(exitCode);
85
+ }
80
86
  });
81
87
 
82
88
  store
@@ -292,6 +292,37 @@ describe('LanceStore', () => {
292
292
  });
293
293
  });
294
294
 
295
+ describe('close', () => {
296
+ it('clears tables and connection', async () => {
297
+ const closeStoreId = createStoreId('close-test-store');
298
+ const closeStore = new LanceStore(tempDir);
299
+ await closeStore.initialize(closeStoreId);
300
+
301
+ const doc = {
302
+ id: createDocumentId('close-doc'),
303
+ content: 'test',
304
+ vector: new Array(384).fill(0.1),
305
+ metadata: {
306
+ type: 'file' as const,
307
+ storeId: closeStoreId,
308
+ indexedAt: new Date(),
309
+ },
310
+ };
311
+
312
+ await closeStore.addDocuments(closeStoreId, [doc]);
313
+
314
+ // Close should not throw
315
+ expect(() => closeStore.close()).not.toThrow();
316
+ });
317
+
318
+ it('handles close when never initialized', () => {
319
+ const uninitializedStore = new LanceStore(tempDir);
320
+
321
+ // Should not throw even when never initialized
322
+ expect(() => uninitializedStore.close()).not.toThrow();
323
+ });
324
+ });
325
+
295
326
  describe('multiple documents operations', () => {
296
327
  it('adds multiple documents at once', async () => {
297
328
  const multiStoreId = createStoreId('multi-doc-store');
package/src/db/lance.ts CHANGED
@@ -145,6 +145,14 @@ export class LanceStore {
145
145
  }
146
146
  }
147
147
 
148
+ close(): void {
149
+ this.tables.clear();
150
+ if (this.connection !== null) {
151
+ this.connection.close();
152
+ this.connection = null;
153
+ }
154
+ }
155
+
148
156
  private getTableName(storeId: StoreId): string {
149
157
  return `documents_${storeId}`;
150
158
  }
@@ -79,6 +79,15 @@ export async function createServices(
79
79
  */
80
80
  export async function destroyServices(services: ServiceContainer): Promise<void> {
81
81
  logger.info('Shutting down services');
82
- await services.pythonBridge.stop();
82
+ try {
83
+ services.lance.close();
84
+ } catch (e) {
85
+ logger.error({ error: e }, 'Error closing LanceStore');
86
+ }
87
+ try {
88
+ await services.pythonBridge.stop();
89
+ } catch (e) {
90
+ logger.error({ error: e }, 'Error stopping Python bridge');
91
+ }
83
92
  await shutdownLogger();
84
93
  }
@@ -25,8 +25,8 @@ describe('destroyServices', () => {
25
25
  it('handles stop errors gracefully', async () => {
26
26
  mockPythonBridge.stop.mockRejectedValue(new Error('stop failed'));
27
27
 
28
- // destroyServices should propagate errors
29
- await expect(destroyServices(mockServices)).rejects.toThrow('stop failed');
28
+ // destroyServices catches and logs errors instead of propagating (Bug #2 fix)
29
+ await expect(destroyServices(mockServices)).resolves.not.toThrow();
30
30
  });
31
31
 
32
32
  it('is idempotent - multiple calls work correctly', async () => {
@@ -237,6 +237,34 @@ describe('StoreService', () => {
237
237
  });
238
238
  });
239
239
 
240
+ describe('name validation', () => {
241
+ it('returns error when name is empty string', async () => {
242
+ const result = await storeService.create({
243
+ name: '',
244
+ type: 'file',
245
+ path: tempDir
246
+ });
247
+
248
+ expect(result.success).toBe(false);
249
+ if (!result.success) {
250
+ expect(result.error.message).toContain('Store name cannot be empty');
251
+ }
252
+ });
253
+
254
+ it('returns error when name is whitespace only', async () => {
255
+ const result = await storeService.create({
256
+ name: ' ',
257
+ type: 'file',
258
+ path: tempDir
259
+ });
260
+
261
+ expect(result.success).toBe(false);
262
+ if (!result.success) {
263
+ expect(result.error.message).toContain('Store name cannot be empty');
264
+ }
265
+ });
266
+ });
267
+
240
268
  describe('duplicate name handling', () => {
241
269
  it('returns error when creating store with duplicate name', async () => {
242
270
  const dir1 = await mkdtemp(join(tmpdir(), 'dup1-'));
@@ -37,6 +37,10 @@ export class StoreService {
37
37
  }
38
38
 
39
39
  async create(input: CreateStoreInput): Promise<Result<Store>> {
40
+ if (!input.name || input.name.trim() === '') {
41
+ return err(new Error('Store name cannot be empty'));
42
+ }
43
+
40
44
  const existing = await this.getByName(input.name);
41
45
  if (existing !== undefined) {
42
46
  return err(new Error(`Store with name "${input.name}" already exists`));