bluera-knowledge 0.11.17 → 0.11.19
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +48 -0
- package/dist/{chunk-ZDEO4WJT.js → chunk-GOAOBPOA.js} +22 -70
- package/dist/chunk-GOAOBPOA.js.map +1 -0
- package/dist/{chunk-6FHWC36B.js → chunk-HRQD3MPH.js} +8 -6
- package/dist/chunk-HRQD3MPH.js.map +1 -0
- package/dist/{chunk-ZZNABJMQ.js → chunk-QEHSDQTL.js} +79 -13
- package/dist/chunk-QEHSDQTL.js.map +1 -0
- package/dist/{chunk-5NUI6JL6.js → chunk-VP4VZULK.js} +2 -2
- package/dist/index.js +36 -18
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +3 -3
- package/dist/watch.service-OPLKIDFQ.js +7 -0
- package/dist/workers/background-worker-cli.js +3 -3
- package/package.json +1 -1
- package/src/cli/commands/crawl.ts +1 -1
- package/src/cli/commands/index-cmd.test.ts +14 -4
- package/src/cli/commands/index-cmd.ts +11 -4
- package/src/cli/commands/store.test.ts +211 -18
- package/src/cli/commands/store.ts +26 -8
- package/src/crawl/article-converter.test.ts +30 -61
- package/src/crawl/article-converter.ts +2 -8
- package/src/crawl/bridge.test.ts +14 -0
- package/src/crawl/bridge.ts +17 -5
- package/src/crawl/intelligent-crawler.test.ts +65 -76
- package/src/crawl/intelligent-crawler.ts +33 -69
- package/src/plugin/git-clone.test.ts +44 -0
- package/src/plugin/git-clone.ts +4 -0
- package/src/services/code-unit.service.test.ts +59 -6
- package/src/services/code-unit.service.ts +47 -2
- package/src/services/index.ts +19 -3
- package/src/services/job.service.test.ts +10 -7
- package/src/services/job.service.ts +12 -6
- package/src/services/services.test.ts +19 -6
- package/src/services/watch.service.test.ts +80 -56
- package/src/services/watch.service.ts +9 -6
- package/dist/chunk-6FHWC36B.js.map +0 -1
- package/dist/chunk-ZDEO4WJT.js.map +0 -1
- package/dist/chunk-ZZNABJMQ.js.map +0 -1
- package/dist/watch.service-BJV3TI3F.js +0 -7
- /package/dist/{chunk-5NUI6JL6.js.map → chunk-VP4VZULK.js.map} +0 -0
- /package/dist/{watch.service-BJV3TI3F.js.map → watch.service-OPLKIDFQ.js.map} +0 -0
|
@@ -141,17 +141,7 @@ export class IntelligentCrawler extends EventEmitter {
|
|
|
141
141
|
): AsyncIterable<CrawlResult> {
|
|
142
142
|
// Check if Claude CLI is available before attempting intelligent mode
|
|
143
143
|
if (!ClaudeClient.isAvailable()) {
|
|
144
|
-
|
|
145
|
-
type: 'error',
|
|
146
|
-
pagesVisited: 0,
|
|
147
|
-
totalPages: maxPages,
|
|
148
|
-
message:
|
|
149
|
-
'Claude CLI not found, using simple crawl mode (install Claude Code for intelligent crawling)',
|
|
150
|
-
error: new Error('Claude CLI not available'),
|
|
151
|
-
};
|
|
152
|
-
this.emit('progress', fallbackProgress);
|
|
153
|
-
yield* this.crawlSimple(seedUrl, extractInstruction, maxPages, useHeadless);
|
|
154
|
-
return;
|
|
144
|
+
throw new Error('Claude CLI not available: install Claude Code for intelligent crawling');
|
|
155
145
|
}
|
|
156
146
|
|
|
157
147
|
let strategy: CrawlStrategy;
|
|
@@ -180,18 +170,8 @@ export class IntelligentCrawler extends EventEmitter {
|
|
|
180
170
|
};
|
|
181
171
|
this.emit('progress', strategyCompleteProgress);
|
|
182
172
|
} catch (error) {
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
type: 'error',
|
|
186
|
-
pagesVisited: 0,
|
|
187
|
-
totalPages: maxPages,
|
|
188
|
-
message: 'Claude crawl strategy failed, falling back to simple mode',
|
|
189
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
190
|
-
};
|
|
191
|
-
this.emit('progress', errorProgress);
|
|
192
|
-
|
|
193
|
-
yield* this.crawlSimple(seedUrl, extractInstruction, maxPages);
|
|
194
|
-
return;
|
|
173
|
+
// Re-throw strategy errors - do not fall back silently
|
|
174
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
195
175
|
}
|
|
196
176
|
|
|
197
177
|
// Step 3: Crawl each URL from Claude's strategy
|
|
@@ -288,12 +268,25 @@ export class IntelligentCrawler extends EventEmitter {
|
|
|
288
268
|
}
|
|
289
269
|
}
|
|
290
270
|
} catch (error) {
|
|
271
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
272
|
+
|
|
273
|
+
// Re-throw non-recoverable errors (extraction failures, Claude CLI not available, headless failures)
|
|
274
|
+
// These represent failures in user-requested functionality that should not be silently skipped
|
|
275
|
+
if (
|
|
276
|
+
errorObj.message.includes('Extraction failed') ||
|
|
277
|
+
errorObj.message.includes('Claude CLI not available') ||
|
|
278
|
+
errorObj.message.includes('Headless fetch failed')
|
|
279
|
+
) {
|
|
280
|
+
throw errorObj;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// For recoverable errors (page fetch failures), emit progress and continue
|
|
291
284
|
const simpleErrorProgress: CrawlProgress = {
|
|
292
285
|
type: 'error',
|
|
293
286
|
pagesVisited,
|
|
294
287
|
totalPages: maxPages,
|
|
295
288
|
currentUrl: current.url,
|
|
296
|
-
error:
|
|
289
|
+
error: errorObj,
|
|
297
290
|
};
|
|
298
291
|
this.emit('progress', simpleErrorProgress);
|
|
299
292
|
}
|
|
@@ -324,13 +317,9 @@ export class IntelligentCrawler extends EventEmitter {
|
|
|
324
317
|
const html = await this.fetchHtml(url, useHeadless);
|
|
325
318
|
|
|
326
319
|
// Convert to clean markdown using slurp-ai techniques
|
|
320
|
+
// Note: convertHtmlToMarkdown throws on errors, no need to check success
|
|
327
321
|
const conversion = await convertHtmlToMarkdown(html, url);
|
|
328
322
|
|
|
329
|
-
if (!conversion.success) {
|
|
330
|
-
logger.error({ url, error: conversion.error }, 'HTML to markdown conversion failed');
|
|
331
|
-
throw new Error(`Failed to convert HTML: ${conversion.error ?? 'Unknown error'}`);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
323
|
logger.debug(
|
|
335
324
|
{
|
|
336
325
|
url,
|
|
@@ -344,44 +333,20 @@ export class IntelligentCrawler extends EventEmitter {
|
|
|
344
333
|
|
|
345
334
|
// Optional: Extract specific information using Claude
|
|
346
335
|
if (extractInstruction !== undefined && extractInstruction !== '') {
|
|
347
|
-
//
|
|
336
|
+
// Throw if extraction requested but Claude CLI isn't available
|
|
348
337
|
if (!ClaudeClient.isAvailable()) {
|
|
349
|
-
|
|
350
|
-
type: 'error',
|
|
351
|
-
pagesVisited,
|
|
352
|
-
totalPages: 0,
|
|
353
|
-
currentUrl: url,
|
|
354
|
-
message: 'Skipping extraction (Claude CLI not available), storing raw markdown',
|
|
355
|
-
error: new Error('Claude CLI not available'),
|
|
356
|
-
};
|
|
357
|
-
this.emit('progress', skipProgress);
|
|
358
|
-
} else {
|
|
359
|
-
try {
|
|
360
|
-
const extractionProgress: CrawlProgress = {
|
|
361
|
-
type: 'extraction',
|
|
362
|
-
pagesVisited,
|
|
363
|
-
totalPages: 0,
|
|
364
|
-
currentUrl: url,
|
|
365
|
-
};
|
|
366
|
-
this.emit('progress', extractionProgress);
|
|
367
|
-
|
|
368
|
-
extracted = await this.claudeClient.extractContent(
|
|
369
|
-
conversion.markdown,
|
|
370
|
-
extractInstruction
|
|
371
|
-
);
|
|
372
|
-
} catch (error) {
|
|
373
|
-
// If extraction fails, just store raw markdown
|
|
374
|
-
const extractionErrorProgress: CrawlProgress = {
|
|
375
|
-
type: 'error',
|
|
376
|
-
pagesVisited,
|
|
377
|
-
totalPages: 0,
|
|
378
|
-
currentUrl: url,
|
|
379
|
-
message: 'Extraction failed, storing raw markdown',
|
|
380
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
381
|
-
};
|
|
382
|
-
this.emit('progress', extractionErrorProgress);
|
|
383
|
-
}
|
|
338
|
+
throw new Error('Claude CLI not available: install Claude Code for extraction');
|
|
384
339
|
}
|
|
340
|
+
|
|
341
|
+
const extractionProgress: CrawlProgress = {
|
|
342
|
+
type: 'extraction',
|
|
343
|
+
pagesVisited,
|
|
344
|
+
totalPages: 0,
|
|
345
|
+
currentUrl: url,
|
|
346
|
+
};
|
|
347
|
+
this.emit('progress', extractionProgress);
|
|
348
|
+
|
|
349
|
+
extracted = await this.claudeClient.extractContent(conversion.markdown, extractInstruction);
|
|
385
350
|
}
|
|
386
351
|
|
|
387
352
|
return {
|
|
@@ -414,10 +379,9 @@ export class IntelligentCrawler extends EventEmitter {
|
|
|
414
379
|
);
|
|
415
380
|
return result.html;
|
|
416
381
|
} catch (error) {
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
'Headless fetch failed, falling back to axios'
|
|
382
|
+
// Wrap with distinctive message so crawlSimple knows not to recover
|
|
383
|
+
throw new Error(
|
|
384
|
+
`Headless fetch failed: ${error instanceof Error ? error.message : String(error)}`
|
|
421
385
|
);
|
|
422
386
|
}
|
|
423
387
|
}
|
|
@@ -266,6 +266,50 @@ describe('GitClone - cloneRepository', () => {
|
|
|
266
266
|
expect(result.error.message).toContain('not found');
|
|
267
267
|
}
|
|
268
268
|
});
|
|
269
|
+
|
|
270
|
+
it('handles spawn error when git is not installed', async () => {
|
|
271
|
+
const mockProcess = new EventEmitter() as ChildProcess;
|
|
272
|
+
mockProcess.stderr = new EventEmitter() as any;
|
|
273
|
+
|
|
274
|
+
mockSpawn.mockImplementation(() => {
|
|
275
|
+
setImmediate(() => {
|
|
276
|
+
mockProcess.emit('error', new Error('spawn git ENOENT'));
|
|
277
|
+
});
|
|
278
|
+
return mockProcess;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const result = await cloneRepository({
|
|
282
|
+
url: 'https://github.com/user/repo.git',
|
|
283
|
+
targetDir: join(tempDir, 'repo'),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(result.success).toBe(false);
|
|
287
|
+
if (!result.success) {
|
|
288
|
+
expect(result.error.message).toContain('spawn git ENOENT');
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('handles spawn error with permission denied', async () => {
|
|
293
|
+
const mockProcess = new EventEmitter() as ChildProcess;
|
|
294
|
+
mockProcess.stderr = new EventEmitter() as any;
|
|
295
|
+
|
|
296
|
+
mockSpawn.mockImplementation(() => {
|
|
297
|
+
setImmediate(() => {
|
|
298
|
+
mockProcess.emit('error', new Error('spawn git EACCES'));
|
|
299
|
+
});
|
|
300
|
+
return mockProcess;
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const result = await cloneRepository({
|
|
304
|
+
url: 'https://github.com/user/repo.git',
|
|
305
|
+
targetDir: join(tempDir, 'repo'),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(result.success).toBe(false);
|
|
309
|
+
if (!result.success) {
|
|
310
|
+
expect(result.error.message).toContain('EACCES');
|
|
311
|
+
}
|
|
312
|
+
});
|
|
269
313
|
});
|
|
270
314
|
|
|
271
315
|
describe('GitClone - isGitUrl', () => {
|
package/src/plugin/git-clone.ts
CHANGED
|
@@ -29,6 +29,10 @@ export async function cloneRepository(options: CloneOptions): Promise<Result<str
|
|
|
29
29
|
stderr += data.toString();
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
git.on('error', (error: Error) => {
|
|
33
|
+
resolve(err(error));
|
|
34
|
+
});
|
|
35
|
+
|
|
32
36
|
git.on('close', (code: number | null) => {
|
|
33
37
|
if (code === 0) {
|
|
34
38
|
resolve(ok(targetDir));
|
|
@@ -345,7 +345,7 @@ function testLonger() {
|
|
|
345
345
|
expect(result?.fullContent).not.toContain('testLonger');
|
|
346
346
|
});
|
|
347
347
|
|
|
348
|
-
it('
|
|
348
|
+
it('extracts interface definitions', () => {
|
|
349
349
|
const code = `
|
|
350
350
|
interface User {
|
|
351
351
|
name: string;
|
|
@@ -355,11 +355,31 @@ interface User {
|
|
|
355
355
|
|
|
356
356
|
const result = service.extractCodeUnit(code, 'User', 'typescript');
|
|
357
357
|
|
|
358
|
-
|
|
359
|
-
expect(result).
|
|
358
|
+
expect(result).toBeDefined();
|
|
359
|
+
expect(result?.type).toBe('interface');
|
|
360
|
+
expect(result?.name).toBe('User');
|
|
361
|
+
expect(result?.startLine).toBe(1);
|
|
362
|
+
expect(result?.endLine).toBe(4);
|
|
363
|
+
expect(result?.fullContent).toContain('name: string');
|
|
364
|
+
expect(result?.fullContent).toContain('age: number');
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('extracts exported interface definitions', () => {
|
|
368
|
+
const code = `
|
|
369
|
+
export interface Config {
|
|
370
|
+
host: string;
|
|
371
|
+
port: number;
|
|
372
|
+
}
|
|
373
|
+
`.trim();
|
|
374
|
+
|
|
375
|
+
const result = service.extractCodeUnit(code, 'Config', 'typescript');
|
|
376
|
+
|
|
377
|
+
expect(result).toBeDefined();
|
|
378
|
+
expect(result?.type).toBe('interface');
|
|
379
|
+
expect(result?.signature).toBe('interface Config');
|
|
360
380
|
});
|
|
361
381
|
|
|
362
|
-
it('
|
|
382
|
+
it('extracts type definitions', () => {
|
|
363
383
|
const code = `
|
|
364
384
|
type Config = {
|
|
365
385
|
host: string;
|
|
@@ -369,8 +389,41 @@ type Config = {
|
|
|
369
389
|
|
|
370
390
|
const result = service.extractCodeUnit(code, 'Config', 'typescript');
|
|
371
391
|
|
|
372
|
-
|
|
373
|
-
expect(result).
|
|
392
|
+
expect(result).toBeDefined();
|
|
393
|
+
expect(result?.type).toBe('type');
|
|
394
|
+
expect(result?.name).toBe('Config');
|
|
395
|
+
expect(result?.startLine).toBe(1);
|
|
396
|
+
expect(result?.endLine).toBe(4);
|
|
397
|
+
expect(result?.fullContent).toContain('host: string');
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('extracts exported type definitions', () => {
|
|
401
|
+
const code = `
|
|
402
|
+
export type Result<T> = {
|
|
403
|
+
success: boolean;
|
|
404
|
+
data: T;
|
|
405
|
+
};
|
|
406
|
+
`.trim();
|
|
407
|
+
|
|
408
|
+
const result = service.extractCodeUnit(code, 'Result', 'typescript');
|
|
409
|
+
|
|
410
|
+
expect(result).toBeDefined();
|
|
411
|
+
expect(result?.type).toBe('type');
|
|
412
|
+
expect(result?.signature).toContain('type Result');
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('extracts type alias (non-object)', () => {
|
|
416
|
+
const code = `
|
|
417
|
+
type UserId = string;
|
|
418
|
+
`.trim();
|
|
419
|
+
|
|
420
|
+
const result = service.extractCodeUnit(code, 'UserId', 'typescript');
|
|
421
|
+
|
|
422
|
+
expect(result).toBeDefined();
|
|
423
|
+
expect(result?.type).toBe('type');
|
|
424
|
+
expect(result?.name).toBe('UserId');
|
|
425
|
+
expect(result?.startLine).toBe(1);
|
|
426
|
+
expect(result?.endLine).toBe(1);
|
|
374
427
|
});
|
|
375
428
|
|
|
376
429
|
it('handles function with generics in signature', () => {
|
|
@@ -16,8 +16,6 @@ export class CodeUnitService {
|
|
|
16
16
|
let startLine = -1;
|
|
17
17
|
let type: CodeUnit['type'] = 'function';
|
|
18
18
|
|
|
19
|
-
// NOTE: Now supports function declarations, class declarations, and arrow functions (const/let/var).
|
|
20
|
-
// Does not handle interfaces or type definitions yet.
|
|
21
19
|
for (let i = 0; i < lines.length; i++) {
|
|
22
20
|
const line = lines[i] ?? '';
|
|
23
21
|
|
|
@@ -33,6 +31,20 @@ export class CodeUnitService {
|
|
|
33
31
|
break;
|
|
34
32
|
}
|
|
35
33
|
|
|
34
|
+
// Check for interface declarations
|
|
35
|
+
if (line.match(new RegExp(`interface\\s+${symbolName}(?:\\s|{|<)`))) {
|
|
36
|
+
startLine = i + 1;
|
|
37
|
+
type = 'interface';
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check for type declarations
|
|
42
|
+
if (line.match(new RegExp(`type\\s+${symbolName}(?:\\s|=|<)`))) {
|
|
43
|
+
startLine = i + 1;
|
|
44
|
+
type = 'type';
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
|
|
36
48
|
// Check for arrow functions: const/let/var name = ...
|
|
37
49
|
if (line.match(new RegExp(`(?:const|let|var)\\s+${symbolName}\\s*=`))) {
|
|
38
50
|
startLine = i + 1;
|
|
@@ -48,6 +60,26 @@ export class CodeUnitService {
|
|
|
48
60
|
let braceCount = 0;
|
|
49
61
|
let foundFirstBrace = false;
|
|
50
62
|
|
|
63
|
+
// For type aliases without braces (e.g., "type UserId = string;"), find semicolon
|
|
64
|
+
if (type === 'type') {
|
|
65
|
+
const firstLine = lines[startLine - 1] ?? '';
|
|
66
|
+
if (!firstLine.includes('{') && firstLine.includes(';')) {
|
|
67
|
+
// Single-line type alias
|
|
68
|
+
endLine = startLine;
|
|
69
|
+
const fullContent = firstLine;
|
|
70
|
+
const signature = this.extractSignature(firstLine, symbolName, type);
|
|
71
|
+
return {
|
|
72
|
+
type,
|
|
73
|
+
name: symbolName,
|
|
74
|
+
signature,
|
|
75
|
+
fullContent,
|
|
76
|
+
startLine,
|
|
77
|
+
endLine,
|
|
78
|
+
language,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
51
83
|
// State machine for tracking context
|
|
52
84
|
let inSingleQuote = false;
|
|
53
85
|
let inDoubleQuote = false;
|
|
@@ -168,6 +200,19 @@ export class CodeUnitService {
|
|
|
168
200
|
return `class ${name}`;
|
|
169
201
|
}
|
|
170
202
|
|
|
203
|
+
if (type === 'interface') {
|
|
204
|
+
return `interface ${name}`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (type === 'type') {
|
|
208
|
+
// For type aliases, include generics if present
|
|
209
|
+
const typeMatch = sig.match(new RegExp(`type\\s+(${name}(?:<[^>]+>)?)\\s*=`));
|
|
210
|
+
if (typeMatch?.[1] !== undefined && typeMatch[1].length > 0) {
|
|
211
|
+
return `type ${typeMatch[1]}`;
|
|
212
|
+
}
|
|
213
|
+
return `type ${name}`;
|
|
214
|
+
}
|
|
215
|
+
|
|
171
216
|
if (type === 'const') {
|
|
172
217
|
// For arrow functions, extract the variable declaration part
|
|
173
218
|
// Example: const myFunc = (param: string): void => ...
|
package/src/services/index.ts
CHANGED
|
@@ -73,22 +73,38 @@ export async function createServices(
|
|
|
73
73
|
/**
|
|
74
74
|
* Cleanly shut down all services, stopping background processes.
|
|
75
75
|
* Call this after CLI commands complete to allow the process to exit.
|
|
76
|
+
* Attempts all cleanup operations and throws if any fail.
|
|
76
77
|
*/
|
|
77
78
|
export async function destroyServices(services: ServiceContainer): Promise<void> {
|
|
78
79
|
logger.info('Shutting down services');
|
|
80
|
+
const errors: Error[] = [];
|
|
81
|
+
|
|
82
|
+
// Use async close to allow native threads time to cleanup
|
|
79
83
|
try {
|
|
80
|
-
// Use async close to allow native threads time to cleanup
|
|
81
84
|
await services.lance.closeAsync();
|
|
82
85
|
} catch (e) {
|
|
83
|
-
|
|
86
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
87
|
+
logger.error({ error }, 'Error closing LanceStore');
|
|
88
|
+
errors.push(error);
|
|
84
89
|
}
|
|
90
|
+
|
|
85
91
|
try {
|
|
86
92
|
await services.pythonBridge.stop();
|
|
87
93
|
} catch (e) {
|
|
88
|
-
|
|
94
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
95
|
+
logger.error({ error }, 'Error stopping Python bridge');
|
|
96
|
+
errors.push(error);
|
|
89
97
|
}
|
|
98
|
+
|
|
90
99
|
// Additional delay to allow native threads (LanceDB, tree-sitter, transformers)
|
|
91
100
|
// to fully complete their cleanup before process exit
|
|
92
101
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
93
102
|
await shutdownLogger();
|
|
103
|
+
|
|
104
|
+
// Throw if any errors occurred during cleanup
|
|
105
|
+
if (errors.length === 1 && errors[0] !== undefined) {
|
|
106
|
+
throw new Error(`Service shutdown failed: ${errors[0].message}`, { cause: errors[0] });
|
|
107
|
+
} else if (errors.length > 1) {
|
|
108
|
+
throw new AggregateError(errors, 'Multiple errors during service shutdown');
|
|
109
|
+
}
|
|
94
110
|
}
|
|
@@ -111,6 +111,14 @@ describe('JobService', () => {
|
|
|
111
111
|
const job = jobService.getJob('non-existent-job');
|
|
112
112
|
expect(job).toBeNull();
|
|
113
113
|
});
|
|
114
|
+
|
|
115
|
+
it('should throw on corrupted job file', () => {
|
|
116
|
+
// Create a corrupted job file
|
|
117
|
+
const jobFile = join(tempDir, 'jobs', 'corrupted-job.json');
|
|
118
|
+
writeFileSync(jobFile, 'invalid json{{{', 'utf-8');
|
|
119
|
+
|
|
120
|
+
expect(() => jobService.getJob('corrupted-job')).toThrow('Failed to read job');
|
|
121
|
+
});
|
|
114
122
|
});
|
|
115
123
|
|
|
116
124
|
describe('listJobs', () => {
|
|
@@ -138,17 +146,12 @@ describe('JobService', () => {
|
|
|
138
146
|
expect(activeJobs.length).toBe(2);
|
|
139
147
|
});
|
|
140
148
|
|
|
141
|
-
it('should
|
|
142
|
-
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
143
|
-
|
|
149
|
+
it('should throw on corrupted job files', () => {
|
|
144
150
|
// Create a corrupted job file
|
|
145
151
|
const jobFile = join(tempDir, 'jobs', 'corrupted.json');
|
|
146
152
|
writeFileSync(jobFile, 'invalid json{{{', 'utf-8');
|
|
147
153
|
|
|
148
|
-
|
|
149
|
-
expect(jobs.length).toBe(3); // Should skip corrupted file
|
|
150
|
-
|
|
151
|
-
consoleErrorSpy.mockRestore();
|
|
154
|
+
expect(() => jobService.listJobs()).toThrow('Failed to read job file');
|
|
152
155
|
});
|
|
153
156
|
|
|
154
157
|
it('should skip non-JSON files', () => {
|
|
@@ -89,8 +89,9 @@ export class JobService {
|
|
|
89
89
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
90
90
|
return JSON.parse(content) as Job;
|
|
91
91
|
} catch (error) {
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Failed to read job ${jobId}: ${error instanceof Error ? error.message : String(error)}`
|
|
94
|
+
);
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -124,7 +125,9 @@ export class JobService {
|
|
|
124
125
|
jobs.push(job);
|
|
125
126
|
}
|
|
126
127
|
} catch (error) {
|
|
127
|
-
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Failed to read job file ${file}: ${error instanceof Error ? error.message : String(error)}`
|
|
130
|
+
);
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
|
|
@@ -209,7 +212,9 @@ export class JobService {
|
|
|
209
212
|
fs.unlinkSync(jobFile);
|
|
210
213
|
cleaned++;
|
|
211
214
|
} catch (error) {
|
|
212
|
-
|
|
215
|
+
throw new Error(
|
|
216
|
+
`Failed to delete job file ${job.id}: ${error instanceof Error ? error.message : String(error)}`
|
|
217
|
+
);
|
|
213
218
|
}
|
|
214
219
|
}
|
|
215
220
|
}
|
|
@@ -231,8 +236,9 @@ export class JobService {
|
|
|
231
236
|
fs.unlinkSync(jobFile);
|
|
232
237
|
return true;
|
|
233
238
|
} catch (error) {
|
|
234
|
-
|
|
235
|
-
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Failed to delete job ${jobId}: ${error instanceof Error ? error.message : String(error)}`
|
|
241
|
+
);
|
|
236
242
|
}
|
|
237
243
|
}
|
|
238
244
|
|
|
@@ -30,11 +30,12 @@ describe('destroyServices', () => {
|
|
|
30
30
|
expect(mockPythonBridge.stop).toHaveBeenCalledTimes(1);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
it('
|
|
33
|
+
it('throws on stop errors', async () => {
|
|
34
34
|
mockPythonBridge.stop.mockRejectedValue(new Error('stop failed'));
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
await expect(destroyServices(mockServices)).rejects.toThrow(
|
|
37
|
+
'Service shutdown failed: stop failed'
|
|
38
|
+
);
|
|
38
39
|
});
|
|
39
40
|
|
|
40
41
|
it('is idempotent - multiple calls work correctly', async () => {
|
|
@@ -52,11 +53,23 @@ describe('destroyServices', () => {
|
|
|
52
53
|
expect(mockLance.close).not.toHaveBeenCalled();
|
|
53
54
|
});
|
|
54
55
|
|
|
55
|
-
it('
|
|
56
|
+
it('throws on LanceStore closeAsync errors', async () => {
|
|
56
57
|
mockLance.closeAsync.mockRejectedValue(new Error('closeAsync failed'));
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
await expect(destroyServices(mockServices)).rejects.toThrow(
|
|
60
|
+
'Service shutdown failed: closeAsync failed'
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('attempts all cleanup even if first fails, then throws aggregate', async () => {
|
|
65
|
+
mockLance.closeAsync.mockRejectedValue(new Error('lance failed'));
|
|
66
|
+
mockPythonBridge.stop.mockRejectedValue(new Error('bridge failed'));
|
|
67
|
+
|
|
68
|
+
await expect(destroyServices(mockServices)).rejects.toThrow();
|
|
69
|
+
|
|
70
|
+
// Both should have been called even though first failed
|
|
71
|
+
expect(mockLance.closeAsync).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(mockPythonBridge.stop).toHaveBeenCalledTimes(1);
|
|
60
73
|
});
|
|
61
74
|
|
|
62
75
|
it('waits for LanceStore async cleanup before returning', async () => {
|