@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.
- package/dist/brain.js +14 -2
- package/dist/brain.js.map +1 -1
- package/dist/init/lifecycle.js +10 -0
- package/dist/init/lifecycle.js.map +1 -1
- package/dist/ipc/router.d.ts +1 -0
- package/dist/ipc/router.js +2 -0
- package/dist/ipc/router.js.map +1 -1
- package/dist/services/doc.service.js +8 -4
- package/dist/services/doc.service.js.map +1 -1
- package/package.json +1 -1
- package/dist/cli/commands/__tests__/borg.test.d.ts +0 -1
- package/dist/cli/commands/__tests__/borg.test.js +0 -84
- package/dist/cli/commands/__tests__/borg.test.js.map +0 -1
- package/dist/cli/commands/__tests__/plugins.test.d.ts +0 -1
- package/dist/cli/commands/__tests__/plugins.test.js +0 -69
- package/dist/cli/commands/__tests__/plugins.test.js.map +0 -1
- package/dist/cli/commands/__tests__/watchdog.test.d.ts +0 -1
- package/dist/cli/commands/__tests__/watchdog.test.js +0 -63
- package/dist/cli/commands/__tests__/watchdog.test.js.map +0 -1
- package/dist/hooks/__tests__/prompt-submit.test.d.ts +0 -1
- package/dist/hooks/__tests__/prompt-submit.test.js +0 -202
- package/dist/hooks/__tests__/prompt-submit.test.js.map +0 -1
- package/dist/ipc/__tests__/notification-bridge.test.d.ts +0 -1
- package/dist/ipc/__tests__/notification-bridge.test.js +0 -155
- package/dist/ipc/__tests__/notification-bridge.test.js.map +0 -1
- package/dist/ipc/__tests__/protocol.test.d.ts +0 -1
- package/dist/ipc/__tests__/protocol.test.js +0 -117
- package/dist/ipc/__tests__/protocol.test.js.map +0 -1
- package/dist/learning/__tests__/confidence-scorer.test.d.ts +0 -1
- package/dist/learning/__tests__/confidence-scorer.test.js +0 -75
- package/dist/learning/__tests__/confidence-scorer.test.js.map +0 -1
- package/dist/mcp/__tests__/prompts.test.d.ts +0 -1
- package/dist/mcp/__tests__/prompts.test.js +0 -419
- package/dist/mcp/__tests__/prompts.test.js.map +0 -1
- package/dist/services/__tests__/auto-resolution.service.test.d.ts +0 -1
- package/dist/services/__tests__/auto-resolution.service.test.js +0 -838
- package/dist/services/__tests__/auto-resolution.service.test.js.map +0 -1
- package/dist/services/__tests__/error.service.auto-resolution.test.d.ts +0 -1
- package/dist/services/__tests__/error.service.auto-resolution.test.js +0 -320
- package/dist/services/__tests__/error.service.auto-resolution.test.js.map +0 -1
- package/dist/signals/__tests__/fingerprint.test.d.ts +0 -1
- package/dist/signals/__tests__/fingerprint.test.js +0 -118
- package/dist/signals/__tests__/fingerprint.test.js.map +0 -1
- package/dist/utils/__tests__/hash.test.d.ts +0 -1
- package/dist/utils/__tests__/hash.test.js +0 -32
- package/dist/utils/__tests__/hash.test.js.map +0 -1
- package/dist/utils/__tests__/paths.test.d.ts +0 -1
- package/dist/utils/__tests__/paths.test.js +0 -75
- 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
|