@timmeck/brain 3.36.58 → 3.36.60

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.
Files changed (49) hide show
  1. package/dist/brain.js +14 -2
  2. package/dist/brain.js.map +1 -1
  3. package/dist/init/lifecycle.js +10 -0
  4. package/dist/init/lifecycle.js.map +1 -1
  5. package/dist/ipc/router.d.ts +1 -0
  6. package/dist/ipc/router.js +2 -0
  7. package/dist/ipc/router.js.map +1 -1
  8. package/dist/services/doc.service.js +8 -4
  9. package/dist/services/doc.service.js.map +1 -1
  10. package/package.json +1 -1
  11. package/dist/cli/commands/__tests__/borg.test.d.ts +0 -1
  12. package/dist/cli/commands/__tests__/borg.test.js +0 -84
  13. package/dist/cli/commands/__tests__/borg.test.js.map +0 -1
  14. package/dist/cli/commands/__tests__/plugins.test.d.ts +0 -1
  15. package/dist/cli/commands/__tests__/plugins.test.js +0 -69
  16. package/dist/cli/commands/__tests__/plugins.test.js.map +0 -1
  17. package/dist/cli/commands/__tests__/watchdog.test.d.ts +0 -1
  18. package/dist/cli/commands/__tests__/watchdog.test.js +0 -63
  19. package/dist/cli/commands/__tests__/watchdog.test.js.map +0 -1
  20. package/dist/hooks/__tests__/prompt-submit.test.d.ts +0 -1
  21. package/dist/hooks/__tests__/prompt-submit.test.js +0 -202
  22. package/dist/hooks/__tests__/prompt-submit.test.js.map +0 -1
  23. package/dist/ipc/__tests__/notification-bridge.test.d.ts +0 -1
  24. package/dist/ipc/__tests__/notification-bridge.test.js +0 -155
  25. package/dist/ipc/__tests__/notification-bridge.test.js.map +0 -1
  26. package/dist/ipc/__tests__/protocol.test.d.ts +0 -1
  27. package/dist/ipc/__tests__/protocol.test.js +0 -117
  28. package/dist/ipc/__tests__/protocol.test.js.map +0 -1
  29. package/dist/learning/__tests__/confidence-scorer.test.d.ts +0 -1
  30. package/dist/learning/__tests__/confidence-scorer.test.js +0 -75
  31. package/dist/learning/__tests__/confidence-scorer.test.js.map +0 -1
  32. package/dist/mcp/__tests__/prompts.test.d.ts +0 -1
  33. package/dist/mcp/__tests__/prompts.test.js +0 -419
  34. package/dist/mcp/__tests__/prompts.test.js.map +0 -1
  35. package/dist/services/__tests__/auto-resolution.service.test.d.ts +0 -1
  36. package/dist/services/__tests__/auto-resolution.service.test.js +0 -838
  37. package/dist/services/__tests__/auto-resolution.service.test.js.map +0 -1
  38. package/dist/services/__tests__/error.service.auto-resolution.test.d.ts +0 -1
  39. package/dist/services/__tests__/error.service.auto-resolution.test.js +0 -320
  40. package/dist/services/__tests__/error.service.auto-resolution.test.js.map +0 -1
  41. package/dist/signals/__tests__/fingerprint.test.d.ts +0 -1
  42. package/dist/signals/__tests__/fingerprint.test.js +0 -118
  43. package/dist/signals/__tests__/fingerprint.test.js.map +0 -1
  44. package/dist/utils/__tests__/hash.test.d.ts +0 -1
  45. package/dist/utils/__tests__/hash.test.js +0 -32
  46. package/dist/utils/__tests__/hash.test.js.map +0 -1
  47. package/dist/utils/__tests__/paths.test.d.ts +0 -1
  48. package/dist/utils/__tests__/paths.test.js +0 -75
  49. package/dist/utils/__tests__/paths.test.js.map +0 -1
@@ -1,838 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- import { describe, it, expect, vi, beforeEach } from 'vitest';
3
- // ─── Mocks ───────────────────────────────────────────────────────────────────
4
- vi.mock('../../utils/logger.js', () => ({
5
- getLogger: () => ({
6
- info: vi.fn(),
7
- warn: vi.fn(),
8
- error: vi.fn(),
9
- debug: vi.fn(),
10
- }),
11
- }));
12
- const mockEventBus = { emit: vi.fn(), on: vi.fn() };
13
- vi.mock('../../utils/events.js', () => ({
14
- getEventBus: () => mockEventBus,
15
- }));
16
- // Lazy-import after mocks are installed
17
- const { AutoResolutionService } = await import('../auto-resolution.service.js');
18
- // ─── Helpers ─────────────────────────────────────────────────────────────────
19
- function makeSolution(overrides = {}) {
20
- return {
21
- id: 1,
22
- description: 'Restart the service',
23
- commands: 'systemctl restart app',
24
- code_change: null,
25
- source: 'human',
26
- confidence: 0.9,
27
- success_count: 8,
28
- fail_count: 2,
29
- created_at: '2025-01-01T00:00:00Z',
30
- updated_at: '2025-01-01T00:00:00Z',
31
- ...overrides,
32
- };
33
- }
34
- function makeError(overrides = {}) {
35
- return {
36
- id: 100,
37
- project_id: 1,
38
- terminal_id: null,
39
- fingerprint: 'abc123',
40
- type: 'TypeError',
41
- message: 'Cannot read properties of undefined',
42
- raw_output: '',
43
- context: null,
44
- file_path: '/src/app.ts',
45
- line_number: 42,
46
- column_number: 10,
47
- occurrence_count: 1,
48
- first_seen: '2025-01-01T00:00:00Z',
49
- last_seen: '2025-01-01T00:00:00Z',
50
- resolved: 0,
51
- resolved_at: null,
52
- ...overrides,
53
- };
54
- }
55
- function makeMatch(overrides = {}) {
56
- return {
57
- errorId: 200,
58
- score: 0.9,
59
- signals: [],
60
- isStrong: true,
61
- ...overrides,
62
- };
63
- }
64
- function makeActivation(type, id, activation) {
65
- return {
66
- node: { type, id },
67
- activation,
68
- depth: 1,
69
- path: [],
70
- };
71
- }
72
- function emptyContext() {
73
- return {
74
- solutions: [],
75
- relatedErrors: [],
76
- relevantModules: [],
77
- preventionRules: [],
78
- insights: [],
79
- };
80
- }
81
- // ─── Test suite ──────────────────────────────────────────────────────────────
82
- describe('AutoResolutionService', () => {
83
- let solutionRepo;
84
- let errorRepo;
85
- let synapseManager;
86
- let service;
87
- beforeEach(() => {
88
- vi.clearAllMocks();
89
- solutionRepo = {
90
- findForError: vi.fn().mockReturnValue([]),
91
- getById: vi.fn().mockReturnValue(undefined),
92
- successRate: vi.fn().mockReturnValue(0),
93
- };
94
- errorRepo = {
95
- getById: vi.fn().mockReturnValue(undefined),
96
- };
97
- synapseManager = {
98
- getErrorContext: vi.fn().mockReturnValue(emptyContext()),
99
- };
100
- service = new AutoResolutionService(solutionRepo, errorRepo, synapseManager);
101
- });
102
- // ═══════════════════════════════════════════════════════════════════════════
103
- // getSuggestions
104
- // ═══════════════════════════════════════════════════════════════════════════
105
- describe('getSuggestions', () => {
106
- it('returns empty result when no matches provided', () => {
107
- const result = service.getSuggestions(1, []);
108
- expect(result.errorId).toBe(1);
109
- expect(result.suggestions).toHaveLength(0);
110
- expect(result.autoApply).toBeNull();
111
- expect(result.totalConsidered).toBe(0);
112
- });
113
- it('returns empty result when matches have no solutions', () => {
114
- solutionRepo.findForError.mockReturnValue([]);
115
- const matches = [makeMatch({ errorId: 200, score: 0.95 })];
116
- const result = service.getSuggestions(1, matches);
117
- expect(result.suggestions).toHaveLength(0);
118
- expect(result.totalConsidered).toBe(0);
119
- expect(solutionRepo.findForError).toHaveBeenCalledWith(200);
120
- });
121
- it('returns ranked suggestions when matches have solutions', () => {
122
- const solA = makeSolution({ id: 10, confidence: 0.9 });
123
- const solB = makeSolution({ id: 11, confidence: 0.6 });
124
- // Match #200 has solA, match #201 has solB
125
- solutionRepo.findForError.mockImplementation((errorId) => {
126
- if (errorId === 200)
127
- return [solA];
128
- if (errorId === 201)
129
- return [solB];
130
- return [];
131
- });
132
- solutionRepo.successRate.mockImplementation((solutionId) => {
133
- if (solutionId === 10)
134
- return 0.8;
135
- if (solutionId === 11)
136
- return 0.5;
137
- return 0;
138
- });
139
- const matches = [
140
- makeMatch({ errorId: 200, score: 0.95 }),
141
- makeMatch({ errorId: 201, score: 0.7 }),
142
- ];
143
- const result = service.getSuggestions(1, matches);
144
- expect(result.suggestions).toHaveLength(2);
145
- expect(result.totalConsidered).toBe(2);
146
- // First suggestion should be higher scored
147
- expect(result.suggestions[0].score).toBeGreaterThan(result.suggestions[1].score);
148
- expect(result.suggestions[0].solution.id).toBe(10);
149
- expect(result.suggestions[1].solution.id).toBe(11);
150
- });
151
- it('deduplicates solutions that appear across multiple matches', () => {
152
- const sharedSolution = makeSolution({ id: 10, confidence: 0.9 });
153
- // Both matches lead to the same solution
154
- solutionRepo.findForError.mockReturnValue([sharedSolution]);
155
- solutionRepo.successRate.mockReturnValue(0.8);
156
- const matches = [
157
- makeMatch({ errorId: 200, score: 0.95 }),
158
- makeMatch({ errorId: 201, score: 0.7 }),
159
- ];
160
- const result = service.getSuggestions(1, matches);
161
- // Should appear only once despite two matches referencing the same solution
162
- expect(result.suggestions).toHaveLength(1);
163
- expect(result.totalConsidered).toBe(1);
164
- expect(result.suggestions[0].solution.id).toBe(10);
165
- // It should keep the first occurrence (from match with errorId 200)
166
- expect(result.suggestions[0].matchedErrorId).toBe(200);
167
- });
168
- it('applies 20% discount to cross-project matches', () => {
169
- const sol = makeSolution({ id: 10, confidence: 0.9 });
170
- solutionRepo.findForError.mockReturnValue([sol]);
171
- solutionRepo.successRate.mockReturnValue(0.8);
172
- // Same match — once as local, once as cross-project
173
- const localMatch = makeMatch({ errorId: 200, score: 0.9 });
174
- const crossMatch = makeMatch({ errorId: 201, score: 0.9 });
175
- const crossSol = makeSolution({ id: 11, confidence: 0.9 });
176
- solutionRepo.findForError.mockImplementation((errorId) => {
177
- if (errorId === 200)
178
- return [sol];
179
- if (errorId === 201)
180
- return [crossSol];
181
- return [];
182
- });
183
- solutionRepo.successRate.mockReturnValue(0.8);
184
- const result = service.getSuggestions(1, [localMatch], [crossMatch]);
185
- expect(result.suggestions).toHaveLength(2);
186
- const local = result.suggestions.find(s => s.solution.id === 10);
187
- const cross = result.suggestions.find(s => s.solution.id === 11);
188
- // Cross-project gets 0.8x multiplier
189
- const expectedLocal = 0.9 * 0.4 + 0.9 * 0.3 + 0.8 * 0.3;
190
- const expectedCross = expectedLocal * 0.8;
191
- expect(local.score).toBeCloseTo(expectedLocal, 10);
192
- expect(cross.score).toBeCloseTo(expectedCross, 10);
193
- expect(cross.score).toBeLessThan(local.score);
194
- });
195
- it('finds auto-apply candidate when score >= 0.85', () => {
196
- const sol = makeSolution({ id: 10, confidence: 1.0 });
197
- solutionRepo.findForError.mockReturnValue([sol]);
198
- solutionRepo.successRate.mockReturnValue(1.0);
199
- // score = 1.0*0.4 + 1.0*0.3 + 1.0*0.3 = 1.0
200
- const matches = [makeMatch({ errorId: 200, score: 1.0 })];
201
- const result = service.getSuggestions(1, matches);
202
- expect(result.autoApply).not.toBeNull();
203
- expect(result.autoApply.solution.id).toBe(10);
204
- expect(result.autoApply.category).toBe('auto');
205
- });
206
- it('does not set autoApply when all scores are below 0.85', () => {
207
- const sol = makeSolution({ id: 10, confidence: 0.3 });
208
- solutionRepo.findForError.mockReturnValue([sol]);
209
- solutionRepo.successRate.mockReturnValue(0.2);
210
- // score = 0.5*0.4 + 0.3*0.3 + 0.2*0.3 = 0.20 + 0.09 + 0.06 = 0.35
211
- const matches = [makeMatch({ errorId: 200, score: 0.5 })];
212
- const result = service.getSuggestions(1, matches);
213
- expect(result.autoApply).toBeNull();
214
- expect(result.suggestions[0].category).toBe('learn');
215
- });
216
- it('limits suggestions to top 10', () => {
217
- const solutions = Array.from({ length: 15 }, (_, i) => makeSolution({ id: i + 1, confidence: 0.5 }));
218
- // Each match has a unique solution
219
- const matches = solutions.map((_, i) => makeMatch({ errorId: 300 + i, score: 0.5 }));
220
- solutionRepo.findForError.mockImplementation((errorId) => {
221
- const idx = errorId - 300;
222
- return [solutions[idx]];
223
- });
224
- solutionRepo.successRate.mockReturnValue(0.5);
225
- const result = service.getSuggestions(1, matches);
226
- expect(result.suggestions).toHaveLength(10);
227
- expect(result.totalConsidered).toBe(15);
228
- });
229
- it('emits resolution:suggested event when there are suggestions', () => {
230
- const sol = makeSolution({ id: 10, confidence: 1.0 });
231
- solutionRepo.findForError.mockReturnValue([sol]);
232
- solutionRepo.successRate.mockReturnValue(1.0);
233
- const matches = [makeMatch({ errorId: 200, score: 1.0 })];
234
- service.getSuggestions(42, matches);
235
- expect(mockEventBus.emit).toHaveBeenCalledWith('resolution:suggested', {
236
- errorId: 42,
237
- suggestionCount: 1,
238
- autoApply: true,
239
- bestScore: expect.closeTo(1.0, 5),
240
- });
241
- });
242
- it('does not emit event when there are no suggestions', () => {
243
- solutionRepo.findForError.mockReturnValue([]);
244
- service.getSuggestions(1, [makeMatch()]);
245
- expect(mockEventBus.emit).not.toHaveBeenCalled();
246
- });
247
- it('does not emit event when matches list is empty', () => {
248
- service.getSuggestions(1, []);
249
- expect(mockEventBus.emit).not.toHaveBeenCalled();
250
- });
251
- it('sorts suggestions by score descending', () => {
252
- const solHigh = makeSolution({ id: 1, confidence: 1.0 });
253
- const solMid = makeSolution({ id: 2, confidence: 0.5 });
254
- const solLow = makeSolution({ id: 3, confidence: 0.1 });
255
- solutionRepo.findForError.mockImplementation((errorId) => {
256
- if (errorId === 200)
257
- return [solLow];
258
- if (errorId === 201)
259
- return [solHigh];
260
- if (errorId === 202)
261
- return [solMid];
262
- return [];
263
- });
264
- solutionRepo.successRate.mockReturnValue(0.5);
265
- const matches = [
266
- makeMatch({ errorId: 200, score: 0.3 }),
267
- makeMatch({ errorId: 201, score: 0.95 }),
268
- makeMatch({ errorId: 202, score: 0.6 }),
269
- ];
270
- const result = service.getSuggestions(1, matches);
271
- expect(result.suggestions).toHaveLength(3);
272
- for (let i = 1; i < result.suggestions.length; i++) {
273
- expect(result.suggestions[i - 1].score).toBeGreaterThanOrEqual(result.suggestions[i].score);
274
- }
275
- });
276
- it('combines local and cross-project matches', () => {
277
- const localSol = makeSolution({ id: 1, confidence: 0.9 });
278
- const crossSol = makeSolution({ id: 2, confidence: 0.9 });
279
- solutionRepo.findForError.mockImplementation((errorId) => {
280
- if (errorId === 200)
281
- return [localSol];
282
- if (errorId === 300)
283
- return [crossSol];
284
- return [];
285
- });
286
- solutionRepo.successRate.mockReturnValue(0.7);
287
- const localMatches = [makeMatch({ errorId: 200, score: 0.8 })];
288
- const crossMatches = [makeMatch({ errorId: 300, score: 0.8 })];
289
- const result = service.getSuggestions(1, localMatches, crossMatches);
290
- expect(result.suggestions).toHaveLength(2);
291
- expect(result.totalConsidered).toBe(2);
292
- });
293
- it('handles undefined crossProjectMatches gracefully', () => {
294
- solutionRepo.findForError.mockReturnValue([makeSolution()]);
295
- solutionRepo.successRate.mockReturnValue(0.5);
296
- const result = service.getSuggestions(1, [makeMatch()], undefined);
297
- expect(result.suggestions).toHaveLength(1);
298
- });
299
- it('includes correct matchedErrorId and matchScore in suggestions', () => {
300
- const sol = makeSolution({ id: 10 });
301
- solutionRepo.findForError.mockReturnValue([sol]);
302
- solutionRepo.successRate.mockReturnValue(0.7);
303
- const match = makeMatch({ errorId: 555, score: 0.82 });
304
- const result = service.getSuggestions(1, [match]);
305
- expect(result.suggestions[0].matchedErrorId).toBe(555);
306
- expect(result.suggestions[0].matchScore).toBe(0.82);
307
- });
308
- it('includes the successRate in each suggestion', () => {
309
- const sol = makeSolution({ id: 10 });
310
- solutionRepo.findForError.mockReturnValue([sol]);
311
- solutionRepo.successRate.mockReturnValue(0.73);
312
- const result = service.getSuggestions(1, [makeMatch()]);
313
- expect(result.suggestions[0].successRate).toBe(0.73);
314
- });
315
- });
316
- // ═══════════════════════════════════════════════════════════════════════════
317
- // getSuggestionsForError
318
- // ═══════════════════════════════════════════════════════════════════════════
319
- describe('getSuggestionsForError', () => {
320
- it('returns empty result for unknown error', () => {
321
- errorRepo.getById.mockReturnValue(undefined);
322
- const result = service.getSuggestionsForError(999);
323
- expect(result.errorId).toBe(999);
324
- expect(result.suggestions).toHaveLength(0);
325
- expect(result.autoApply).toBeNull();
326
- expect(result.totalConsidered).toBe(0);
327
- });
328
- it('finds direct solutions for a known error', () => {
329
- const error = makeError({ id: 100 });
330
- const sol = makeSolution({ id: 10, confidence: 0.8 });
331
- errorRepo.getById.mockReturnValue(error);
332
- solutionRepo.findForError.mockReturnValue([sol]);
333
- solutionRepo.successRate.mockReturnValue(0.75);
334
- synapseManager.getErrorContext.mockReturnValue(emptyContext());
335
- const result = service.getSuggestionsForError(100);
336
- expect(result.suggestions).toHaveLength(1);
337
- expect(result.suggestions[0].solution.id).toBe(10);
338
- // Direct solutions use matchScore = 1.0
339
- expect(result.suggestions[0].matchScore).toBe(1.0);
340
- expect(result.suggestions[0].matchedErrorId).toBe(100);
341
- });
342
- it('uses matchScore 1.0 for direct solutions', () => {
343
- errorRepo.getById.mockReturnValue(makeError({ id: 100 }));
344
- const sol = makeSolution({ id: 10, confidence: 0.6 });
345
- solutionRepo.findForError.mockReturnValue([sol]);
346
- solutionRepo.successRate.mockReturnValue(0.5);
347
- synapseManager.getErrorContext.mockReturnValue(emptyContext());
348
- const result = service.getSuggestionsForError(100);
349
- // score = 1.0*0.4 + 0.6*0.3 + 0.5*0.3 = 0.4 + 0.18 + 0.15 = 0.73
350
- expect(result.suggestions[0].score).toBeCloseTo(0.73, 10);
351
- });
352
- it('builds correct reasoning for direct solutions', () => {
353
- errorRepo.getById.mockReturnValue(makeError({ id: 100 }));
354
- const sol = makeSolution({ id: 10, confidence: 0.85 });
355
- solutionRepo.findForError.mockReturnValue([sol]);
356
- solutionRepo.successRate.mockReturnValue(0.9);
357
- synapseManager.getErrorContext.mockReturnValue(emptyContext());
358
- const result = service.getSuggestionsForError(100);
359
- expect(result.suggestions[0].reasoning).toContain('Direct solution for this error');
360
- expect(result.suggestions[0].reasoning).toContain('85%');
361
- expect(result.suggestions[0].reasoning).toContain('90%');
362
- });
363
- it('finds solutions via synapse-connected related errors', () => {
364
- const error = makeError({ id: 100 });
365
- const relatedSol = makeSolution({ id: 20, confidence: 0.7 });
366
- errorRepo.getById.mockReturnValue(error);
367
- solutionRepo.findForError.mockImplementation((errorId) => {
368
- if (errorId === 100)
369
- return []; // No direct solutions
370
- if (errorId === 200)
371
- return [relatedSol]; // Related error has solution
372
- return [];
373
- });
374
- solutionRepo.successRate.mockReturnValue(0.6);
375
- const context = emptyContext();
376
- context.relatedErrors = [makeActivation('error', 200, 0.75)];
377
- synapseManager.getErrorContext.mockReturnValue(context);
378
- const result = service.getSuggestionsForError(100);
379
- expect(result.suggestions).toHaveLength(1);
380
- expect(result.suggestions[0].solution.id).toBe(20);
381
- expect(result.suggestions[0].matchedErrorId).toBe(200);
382
- // matchScore = min(1.0, activation) = min(1.0, 0.75) = 0.75
383
- expect(result.suggestions[0].matchScore).toBe(0.75);
384
- });
385
- it('caps matchScore from synapse activation at 1.0', () => {
386
- errorRepo.getById.mockReturnValue(makeError({ id: 100 }));
387
- const sol = makeSolution({ id: 20, confidence: 0.8 });
388
- solutionRepo.findForError.mockImplementation((errorId) => {
389
- if (errorId === 100)
390
- return [];
391
- if (errorId === 200)
392
- return [sol];
393
- return [];
394
- });
395
- solutionRepo.successRate.mockReturnValue(0.7);
396
- const context = emptyContext();
397
- // activation > 1.0 — should be capped
398
- context.relatedErrors = [makeActivation('error', 200, 1.5)];
399
- synapseManager.getErrorContext.mockReturnValue(context);
400
- const result = service.getSuggestionsForError(100);
401
- expect(result.suggestions[0].matchScore).toBe(1.0);
402
- });
403
- it('finds solutions via synapse activation context (solutions field)', () => {
404
- const error = makeError({ id: 100 });
405
- const sol = makeSolution({ id: 30, confidence: 0.8 });
406
- errorRepo.getById.mockReturnValue(error);
407
- solutionRepo.findForError.mockReturnValue([]); // No direct or related-error solutions
408
- solutionRepo.getById.mockReturnValue(sol);
409
- solutionRepo.successRate.mockReturnValue(0.65);
410
- const context = emptyContext();
411
- context.solutions = [makeActivation('solution', 30, 0.6)];
412
- synapseManager.getErrorContext.mockReturnValue(context);
413
- const result = service.getSuggestionsForError(100);
414
- expect(result.suggestions).toHaveLength(1);
415
- expect(result.suggestions[0].solution.id).toBe(30);
416
- expect(result.suggestions[0].matchedErrorId).toBe(100);
417
- expect(result.suggestions[0].matchScore).toBe(0.6);
418
- });
419
- it('skips synapse solutions when getById returns undefined', () => {
420
- errorRepo.getById.mockReturnValue(makeError({ id: 100 }));
421
- solutionRepo.findForError.mockReturnValue([]);
422
- solutionRepo.getById.mockReturnValue(undefined); // Solution not found
423
- const context = emptyContext();
424
- context.solutions = [makeActivation('solution', 999, 0.7)];
425
- synapseManager.getErrorContext.mockReturnValue(context);
426
- const result = service.getSuggestionsForError(100);
427
- expect(result.suggestions).toHaveLength(0);
428
- });
429
- it('deduplicates solutions found via direct and synapse paths', () => {
430
- const error = makeError({ id: 100 });
431
- const sol = makeSolution({ id: 10, confidence: 0.9 });
432
- errorRepo.getById.mockReturnValue(error);
433
- // Direct solutions include sol
434
- solutionRepo.findForError.mockImplementation((errorId) => {
435
- if (errorId === 100)
436
- return [sol];
437
- if (errorId === 200)
438
- return [sol]; // Also found via related error
439
- return [];
440
- });
441
- solutionRepo.getById.mockReturnValue(sol); // Also found via synapse
442
- solutionRepo.successRate.mockReturnValue(0.8);
443
- const context = emptyContext();
444
- context.relatedErrors = [makeActivation('error', 200, 0.7)];
445
- context.solutions = [makeActivation('solution', 10, 0.6)];
446
- synapseManager.getErrorContext.mockReturnValue(context);
447
- const result = service.getSuggestionsForError(100);
448
- // Same solution id=10 appears via 3 paths, but should appear only once
449
- const solutionIds = result.suggestions.map(s => s.solution.id);
450
- expect(solutionIds.filter(id => id === 10)).toHaveLength(1);
451
- });
452
- it('includes solutions from all three channels without duplication', () => {
453
- const error = makeError({ id: 100 });
454
- const directSol = makeSolution({ id: 1, confidence: 0.9 });
455
- const relatedSol = makeSolution({ id: 2, confidence: 0.7 });
456
- const synapseSol = makeSolution({ id: 3, confidence: 0.6 });
457
- errorRepo.getById.mockReturnValue(error);
458
- solutionRepo.findForError.mockImplementation((errorId) => {
459
- if (errorId === 100)
460
- return [directSol];
461
- if (errorId === 200)
462
- return [relatedSol];
463
- return [];
464
- });
465
- solutionRepo.getById.mockReturnValue(synapseSol);
466
- solutionRepo.successRate.mockReturnValue(0.5);
467
- const context = emptyContext();
468
- context.relatedErrors = [makeActivation('error', 200, 0.6)];
469
- context.solutions = [makeActivation('solution', 3, 0.5)];
470
- synapseManager.getErrorContext.mockReturnValue(context);
471
- const result = service.getSuggestionsForError(100);
472
- expect(result.suggestions).toHaveLength(3);
473
- expect(result.totalConsidered).toBe(3);
474
- });
475
- it('sorts all channels by score descending', () => {
476
- errorRepo.getById.mockReturnValue(makeError({ id: 100 }));
477
- const directSol = makeSolution({ id: 1, confidence: 0.3 }); // Low confidence
478
- const relatedSol = makeSolution({ id: 2, confidence: 0.95 }); // High confidence
479
- solutionRepo.findForError.mockImplementation((errorId) => {
480
- if (errorId === 100)
481
- return [directSol];
482
- if (errorId === 200)
483
- return [relatedSol];
484
- return [];
485
- });
486
- solutionRepo.successRate.mockReturnValue(0.5);
487
- const context = emptyContext();
488
- context.relatedErrors = [makeActivation('error', 200, 0.95)];
489
- synapseManager.getErrorContext.mockReturnValue(context);
490
- const result = service.getSuggestionsForError(100);
491
- expect(result.suggestions).toHaveLength(2);
492
- expect(result.suggestions[0].score).toBeGreaterThanOrEqual(result.suggestions[1].score);
493
- });
494
- it('builds reasoning for synapse-connected related error solutions', () => {
495
- errorRepo.getById.mockReturnValue(makeError({ id: 100 }));
496
- const sol = makeSolution({ id: 20, confidence: 0.7 });
497
- solutionRepo.findForError.mockImplementation((errorId) => {
498
- if (errorId === 100)
499
- return [];
500
- if (errorId === 200)
501
- return [sol];
502
- return [];
503
- });
504
- solutionRepo.successRate.mockReturnValue(0.6);
505
- const context = emptyContext();
506
- context.relatedErrors = [makeActivation('error', 200, 0.75)];
507
- synapseManager.getErrorContext.mockReturnValue(context);
508
- const result = service.getSuggestionsForError(100);
509
- expect(result.suggestions[0].reasoning).toContain('related error #200');
510
- expect(result.suggestions[0].reasoning).toContain('activation: 0.75');
511
- expect(result.suggestions[0].reasoning).toContain('60%');
512
- });
513
- it('builds reasoning for synapse-activated solutions', () => {
514
- errorRepo.getById.mockReturnValue(makeError({ id: 100 }));
515
- const sol = makeSolution({ id: 30, confidence: 0.8 });
516
- solutionRepo.findForError.mockReturnValue([]);
517
- solutionRepo.getById.mockReturnValue(sol);
518
- solutionRepo.successRate.mockReturnValue(0.45);
519
- const context = emptyContext();
520
- context.solutions = [makeActivation('solution', 30, 0.55)];
521
- synapseManager.getErrorContext.mockReturnValue(context);
522
- const result = service.getSuggestionsForError(100);
523
- expect(result.suggestions[0].reasoning).toContain('Connected solution via synapse network');
524
- expect(result.suggestions[0].reasoning).toContain('activation: 0.55');
525
- expect(result.suggestions[0].reasoning).toContain('45%');
526
- });
527
- it('limits to top 10 suggestions', () => {
528
- errorRepo.getById.mockReturnValue(makeError({ id: 100 }));
529
- // 12 direct solutions
530
- const solutions = Array.from({ length: 12 }, (_, i) => makeSolution({ id: i + 1, confidence: 0.5 }));
531
- solutionRepo.findForError.mockReturnValue(solutions);
532
- solutionRepo.successRate.mockReturnValue(0.5);
533
- synapseManager.getErrorContext.mockReturnValue(emptyContext());
534
- const result = service.getSuggestionsForError(100);
535
- expect(result.suggestions).toHaveLength(10);
536
- expect(result.totalConsidered).toBe(12);
537
- });
538
- it('sets autoApply when a suggestion qualifies', () => {
539
- errorRepo.getById.mockReturnValue(makeError({ id: 100 }));
540
- const sol = makeSolution({ id: 10, confidence: 1.0 });
541
- solutionRepo.findForError.mockReturnValue([sol]);
542
- solutionRepo.successRate.mockReturnValue(1.0);
543
- synapseManager.getErrorContext.mockReturnValue(emptyContext());
544
- const result = service.getSuggestionsForError(100);
545
- // score = 1.0*0.4 + 1.0*0.3 + 1.0*0.3 = 1.0 → auto
546
- expect(result.autoApply).not.toBeNull();
547
- expect(result.autoApply.solution.id).toBe(10);
548
- });
549
- });
550
- // ═══════════════════════════════════════════════════════════════════════════
551
- // computeScore (tested indirectly through getSuggestions)
552
- // ═══════════════════════════════════════════════════════════════════════════
553
- describe('score computation', () => {
554
- it('computes 0.4*match + 0.3*confidence + 0.3*successRate for local matches', () => {
555
- const sol = makeSolution({ id: 10, confidence: 0.7 });
556
- solutionRepo.findForError.mockReturnValue([sol]);
557
- solutionRepo.successRate.mockReturnValue(0.6);
558
- const match = makeMatch({ errorId: 200, score: 0.8 });
559
- const result = service.getSuggestions(1, [match]);
560
- // expected = 0.8*0.4 + 0.7*0.3 + 0.6*0.3 = 0.32 + 0.21 + 0.18 = 0.71
561
- expect(result.suggestions[0].score).toBeCloseTo(0.71, 10);
562
- });
563
- it('applies 0.8x multiplier for cross-project matches', () => {
564
- const sol = makeSolution({ id: 10, confidence: 0.7 });
565
- solutionRepo.findForError.mockReturnValue([sol]);
566
- solutionRepo.successRate.mockReturnValue(0.6);
567
- const crossMatch = makeMatch({ errorId: 200, score: 0.8 });
568
- const result = service.getSuggestions(1, [], [crossMatch]);
569
- // expected = (0.8*0.4 + 0.7*0.3 + 0.6*0.3) * 0.8 = 0.71 * 0.8 = 0.568
570
- expect(result.suggestions[0].score).toBeCloseTo(0.568, 10);
571
- });
572
- it('computes max score of 1.0 when all inputs are 1.0', () => {
573
- const sol = makeSolution({ id: 10, confidence: 1.0 });
574
- solutionRepo.findForError.mockReturnValue([sol]);
575
- solutionRepo.successRate.mockReturnValue(1.0);
576
- const match = makeMatch({ errorId: 200, score: 1.0 });
577
- const result = service.getSuggestions(1, [match]);
578
- expect(result.suggestions[0].score).toBeCloseTo(1.0, 10);
579
- });
580
- it('computes score of 0 when all inputs are 0', () => {
581
- const sol = makeSolution({ id: 10, confidence: 0 });
582
- solutionRepo.findForError.mockReturnValue([sol]);
583
- solutionRepo.successRate.mockReturnValue(0);
584
- const match = makeMatch({ errorId: 200, score: 0 });
585
- const result = service.getSuggestions(1, [match]);
586
- expect(result.suggestions[0].score).toBeCloseTo(0, 10);
587
- });
588
- it('cross-project 0.8x discount stacks with low scores', () => {
589
- const sol = makeSolution({ id: 10, confidence: 0.5 });
590
- solutionRepo.findForError.mockReturnValue([sol]);
591
- solutionRepo.successRate.mockReturnValue(0.3);
592
- const crossMatch = makeMatch({ errorId: 200, score: 0.4 });
593
- const result = service.getSuggestions(1, [], [crossMatch]);
594
- // raw = 0.4*0.4 + 0.5*0.3 + 0.3*0.3 = 0.16 + 0.15 + 0.09 = 0.40
595
- // cross = 0.40 * 0.8 = 0.32
596
- expect(result.suggestions[0].score).toBeCloseTo(0.32, 10);
597
- });
598
- });
599
- // ═══════════════════════════════════════════════════════════════════════════
600
- // categorize (tested indirectly)
601
- // ═══════════════════════════════════════════════════════════════════════════
602
- describe('categorization', () => {
603
- it('categorizes as "auto" when score >= 0.85', () => {
604
- const sol = makeSolution({ id: 10, confidence: 1.0 });
605
- solutionRepo.findForError.mockReturnValue([sol]);
606
- solutionRepo.successRate.mockReturnValue(1.0);
607
- // score = 0.9*0.4 + 1.0*0.3 + 1.0*0.3 = 0.36+0.30+0.30 = 0.96 → auto
608
- const match = makeMatch({ errorId: 200, score: 0.9 });
609
- const result = service.getSuggestions(1, [match]);
610
- expect(result.suggestions[0].category).toBe('auto');
611
- expect(result.suggestions[0].score).toBeGreaterThanOrEqual(0.85);
612
- });
613
- it('categorizes as "auto" at exactly 0.85', () => {
614
- // Need: 0.4m + 0.3c + 0.3s = 0.85
615
- // m = 1.0, c = 0.75, s = 0.75: 0.4 + 0.225 + 0.225 = 0.85
616
- const sol = makeSolution({ id: 10, confidence: 0.75 });
617
- solutionRepo.findForError.mockReturnValue([sol]);
618
- solutionRepo.successRate.mockReturnValue(0.75);
619
- const match = makeMatch({ errorId: 200, score: 1.0 });
620
- const result = service.getSuggestions(1, [match]);
621
- expect(result.suggestions[0].score).toBeCloseTo(0.85, 10);
622
- expect(result.suggestions[0].category).toBe('auto');
623
- });
624
- it('categorizes as "suggest" when 0.5 <= score < 0.85', () => {
625
- const sol = makeSolution({ id: 10, confidence: 0.6 });
626
- solutionRepo.findForError.mockReturnValue([sol]);
627
- solutionRepo.successRate.mockReturnValue(0.5);
628
- // score = 0.7*0.4 + 0.6*0.3 + 0.5*0.3 = 0.28+0.18+0.15 = 0.61 → suggest
629
- const match = makeMatch({ errorId: 200, score: 0.7 });
630
- const result = service.getSuggestions(1, [match]);
631
- expect(result.suggestions[0].category).toBe('suggest');
632
- expect(result.suggestions[0].score).toBeGreaterThanOrEqual(0.5);
633
- expect(result.suggestions[0].score).toBeLessThan(0.85);
634
- });
635
- it('categorizes as "suggest" at exactly 0.5', () => {
636
- // Need: 0.4m + 0.3c + 0.3s = 0.5
637
- // m = 0.5, c = 0.5, s = 0.5: 0.2 + 0.15 + 0.15 = 0.5
638
- const sol = makeSolution({ id: 10, confidence: 0.5 });
639
- solutionRepo.findForError.mockReturnValue([sol]);
640
- solutionRepo.successRate.mockReturnValue(0.5);
641
- const match = makeMatch({ errorId: 200, score: 0.5 });
642
- const result = service.getSuggestions(1, [match]);
643
- expect(result.suggestions[0].score).toBeCloseTo(0.5, 10);
644
- expect(result.suggestions[0].category).toBe('suggest');
645
- });
646
- it('categorizes as "learn" when score < 0.5', () => {
647
- const sol = makeSolution({ id: 10, confidence: 0.2 });
648
- solutionRepo.findForError.mockReturnValue([sol]);
649
- solutionRepo.successRate.mockReturnValue(0.1);
650
- // score = 0.3*0.4 + 0.2*0.3 + 0.1*0.3 = 0.12+0.06+0.03 = 0.21 → learn
651
- const match = makeMatch({ errorId: 200, score: 0.3 });
652
- const result = service.getSuggestions(1, [match]);
653
- expect(result.suggestions[0].category).toBe('learn');
654
- expect(result.suggestions[0].score).toBeLessThan(0.5);
655
- });
656
- it('score just below 0.85 is "suggest", not "auto"', () => {
657
- // 0.849... should be "suggest"
658
- // m = 1.0, c = 0.74, s = 0.75: 0.4 + 0.222 + 0.225 = 0.847
659
- const sol = makeSolution({ id: 10, confidence: 0.74 });
660
- solutionRepo.findForError.mockReturnValue([sol]);
661
- solutionRepo.successRate.mockReturnValue(0.75);
662
- const match = makeMatch({ errorId: 200, score: 1.0 });
663
- const result = service.getSuggestions(1, [match]);
664
- expect(result.suggestions[0].score).toBeLessThan(0.85);
665
- expect(result.suggestions[0].category).toBe('suggest');
666
- });
667
- it('score just below 0.5 is "learn", not "suggest"', () => {
668
- // 0.499... should be "learn"
669
- // m = 0.49, c = 0.5, s = 0.5: 0.196 + 0.15 + 0.15 = 0.496
670
- const sol = makeSolution({ id: 10, confidence: 0.5 });
671
- solutionRepo.findForError.mockReturnValue([sol]);
672
- solutionRepo.successRate.mockReturnValue(0.5);
673
- const match = makeMatch({ errorId: 200, score: 0.49 });
674
- const result = service.getSuggestions(1, [match]);
675
- expect(result.suggestions[0].score).toBeLessThan(0.5);
676
- expect(result.suggestions[0].category).toBe('learn');
677
- });
678
- });
679
- // ═══════════════════════════════════════════════════════════════════════════
680
- // buildReasoning (tested indirectly through getSuggestions)
681
- // ═══════════════════════════════════════════════════════════════════════════
682
- describe('reasoning string', () => {
683
- it('includes matched error ID and similarity percentage', () => {
684
- const sol = makeSolution({ id: 10, confidence: 0.8, success_count: 5, fail_count: 1 });
685
- solutionRepo.findForError.mockReturnValue([sol]);
686
- solutionRepo.successRate.mockReturnValue(0.83);
687
- const match = makeMatch({ errorId: 456, score: 0.72 });
688
- const result = service.getSuggestions(1, [match]);
689
- const reasoning = result.suggestions[0].reasoning;
690
- expect(reasoning).toContain('Matched error #456');
691
- expect(reasoning).toContain('72% similar');
692
- });
693
- it('includes "from another project" for cross-project matches', () => {
694
- const sol = makeSolution({ id: 10, confidence: 0.8, success_count: 3, fail_count: 1 });
695
- solutionRepo.findForError.mockReturnValue([sol]);
696
- solutionRepo.successRate.mockReturnValue(0.75);
697
- const crossMatch = makeMatch({ errorId: 456, score: 0.72 });
698
- const result = service.getSuggestions(1, [], [crossMatch]);
699
- const reasoning = result.suggestions[0].reasoning;
700
- expect(reasoning).toContain('from another project');
701
- });
702
- it('does not include "from another project" for local matches', () => {
703
- const sol = makeSolution({ id: 10, confidence: 0.8, success_count: 3, fail_count: 1 });
704
- solutionRepo.findForError.mockReturnValue([sol]);
705
- solutionRepo.successRate.mockReturnValue(0.75);
706
- const match = makeMatch({ errorId: 456, score: 0.72 });
707
- const result = service.getSuggestions(1, [match]);
708
- const reasoning = result.suggestions[0].reasoning;
709
- expect(reasoning).not.toContain('from another project');
710
- });
711
- it('includes success rate and attempt counts when attempts exist', () => {
712
- const sol = makeSolution({ id: 10, confidence: 0.8, success_count: 7, fail_count: 3 });
713
- solutionRepo.findForError.mockReturnValue([sol]);
714
- solutionRepo.successRate.mockReturnValue(0.7);
715
- const match = makeMatch({ errorId: 200, score: 0.9 });
716
- const result = service.getSuggestions(1, [match]);
717
- const reasoning = result.suggestions[0].reasoning;
718
- expect(reasoning).toContain('70% success rate');
719
- expect(reasoning).toContain('7/10 attempts');
720
- });
721
- it('includes "no prior attempts" when success_count + fail_count is 0', () => {
722
- const sol = makeSolution({ id: 10, confidence: 0.8, success_count: 0, fail_count: 0 });
723
- solutionRepo.findForError.mockReturnValue([sol]);
724
- solutionRepo.successRate.mockReturnValue(0);
725
- const match = makeMatch({ errorId: 200, score: 0.9 });
726
- const result = service.getSuggestions(1, [match]);
727
- const reasoning = result.suggestions[0].reasoning;
728
- expect(reasoning).toContain('no prior attempts');
729
- });
730
- it('includes solution confidence percentage', () => {
731
- const sol = makeSolution({ id: 10, confidence: 0.65, success_count: 2, fail_count: 1 });
732
- solutionRepo.findForError.mockReturnValue([sol]);
733
- solutionRepo.successRate.mockReturnValue(0.67);
734
- const match = makeMatch({ errorId: 200, score: 0.9 });
735
- const result = service.getSuggestions(1, [match]);
736
- const reasoning = result.suggestions[0].reasoning;
737
- expect(reasoning).toContain('confidence: 65%');
738
- });
739
- it('ends with a period', () => {
740
- const sol = makeSolution({ id: 10, confidence: 0.8, success_count: 5, fail_count: 1 });
741
- solutionRepo.findForError.mockReturnValue([sol]);
742
- solutionRepo.successRate.mockReturnValue(0.83);
743
- const match = makeMatch({ errorId: 200, score: 0.9 });
744
- const result = service.getSuggestions(1, [match]);
745
- const reasoning = result.suggestions[0].reasoning;
746
- expect(reasoning).toMatch(/\.$/);
747
- });
748
- it('joins parts with ". " separator', () => {
749
- const sol = makeSolution({ id: 10, confidence: 0.8, success_count: 3, fail_count: 1 });
750
- solutionRepo.findForError.mockReturnValue([sol]);
751
- solutionRepo.successRate.mockReturnValue(0.75);
752
- const match = makeMatch({ errorId: 200, score: 0.85 });
753
- const result = service.getSuggestions(1, [match]);
754
- const reasoning = result.suggestions[0].reasoning;
755
- // Should be: "Matched error #200 (85% similar). 75% success rate (3/4 attempts). confidence: 80%."
756
- const parts = reasoning.slice(0, -1).split('. ');
757
- expect(parts.length).toBeGreaterThanOrEqual(3);
758
- });
759
- });
760
- // ═══════════════════════════════════════════════════════════════════════════
761
- // Edge cases
762
- // ═══════════════════════════════════════════════════════════════════════════
763
- describe('edge cases', () => {
764
- it('handles empty local matches with non-empty cross-project matches', () => {
765
- const sol = makeSolution({ id: 10, confidence: 0.8 });
766
- solutionRepo.findForError.mockReturnValue([sol]);
767
- solutionRepo.successRate.mockReturnValue(0.7);
768
- const result = service.getSuggestions(1, [], [makeMatch({ errorId: 300, score: 0.8 })]);
769
- expect(result.suggestions).toHaveLength(1);
770
- });
771
- it('totalConsidered counts unique solutions even when limited to 10', () => {
772
- const solutions = Array.from({ length: 15 }, (_, i) => makeSolution({ id: i + 1, confidence: Math.random() }));
773
- const matches = solutions.map((_, i) => makeMatch({ errorId: 300 + i, score: 0.6 }));
774
- solutionRepo.findForError.mockImplementation((errorId) => {
775
- const idx = errorId - 300;
776
- return [solutions[idx]];
777
- });
778
- solutionRepo.successRate.mockReturnValue(0.5);
779
- const result = service.getSuggestions(1, matches);
780
- expect(result.totalConsidered).toBe(15);
781
- expect(result.suggestions).toHaveLength(10);
782
- });
783
- it('autoApply picks the first "auto" category from sorted results', () => {
784
- // Two solutions that both qualify as "auto"
785
- const sol1 = makeSolution({ id: 1, confidence: 1.0 });
786
- const sol2 = makeSolution({ id: 2, confidence: 0.95 });
787
- solutionRepo.findForError.mockImplementation((errorId) => {
788
- if (errorId === 200)
789
- return [sol1];
790
- if (errorId === 201)
791
- return [sol2];
792
- return [];
793
- });
794
- solutionRepo.successRate.mockReturnValue(1.0);
795
- const matches = [
796
- makeMatch({ errorId: 200, score: 1.0 }),
797
- makeMatch({ errorId: 201, score: 0.95 }),
798
- ];
799
- const result = service.getSuggestions(1, matches);
800
- // Both are auto, but autoApply should be the highest-scored one (first in sorted list)
801
- expect(result.autoApply).not.toBeNull();
802
- expect(result.autoApply.solution.id).toBe(1);
803
- expect(result.autoApply.score).toBeGreaterThanOrEqual(result.suggestions[1].score);
804
- });
805
- it('event payload contains correct bestScore', () => {
806
- const sol1 = makeSolution({ id: 1, confidence: 0.5 });
807
- const sol2 = makeSolution({ id: 2, confidence: 0.9 });
808
- solutionRepo.findForError.mockImplementation((errorId) => {
809
- if (errorId === 200)
810
- return [sol1];
811
- if (errorId === 201)
812
- return [sol2];
813
- return [];
814
- });
815
- solutionRepo.successRate.mockReturnValue(0.7);
816
- const matches = [
817
- makeMatch({ errorId: 200, score: 0.5 }),
818
- makeMatch({ errorId: 201, score: 0.95 }),
819
- ];
820
- const result = service.getSuggestions(1, matches);
821
- expect(mockEventBus.emit).toHaveBeenCalledWith('resolution:suggested', expect.objectContaining({
822
- bestScore: result.suggestions[0].score,
823
- }));
824
- });
825
- it('event payload autoApply is boolean false when no auto candidate', () => {
826
- const sol = makeSolution({ id: 10, confidence: 0.3 });
827
- solutionRepo.findForError.mockReturnValue([sol]);
828
- solutionRepo.successRate.mockReturnValue(0.3);
829
- // score = 0.5*0.4 + 0.3*0.3 + 0.3*0.3 = 0.2+0.09+0.09 = 0.38 → learn
830
- const match = makeMatch({ errorId: 200, score: 0.5 });
831
- service.getSuggestions(1, [match]);
832
- expect(mockEventBus.emit).toHaveBeenCalledWith('resolution:suggested', expect.objectContaining({
833
- autoApply: false,
834
- }));
835
- });
836
- });
837
- });
838
- //# sourceMappingURL=auto-resolution.service.test.js.map