@anonympins/fingerprint 0.4.2 → 0.4.3

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.
@@ -1,323 +1,359 @@
1
- import {beforeEach, describe, expect, it, vi} from 'vitest';
2
- import {promises as fs} from 'node:fs';
3
- import {__internal as problemManagerInternal, getProblemManager} from '../problem-manager.js';
4
- import {Optimization} from '../library.js';
5
-
6
- // Mock the in-memory store for testing
7
- const inMemoryStore = {
8
- _map: new Map(),
9
- async get(key) { return this._map.get(key); },
10
- async set(key, value, ttl) { this._map.set(key, value); },
11
- async has(key) { return this._map.has(key); },
12
- async delete(key) { this._map.delete(key); },
13
- clear() { this._map.clear(); }
14
- };
15
-
16
- // Mock the 'fs' module
17
- vi.mock('node:fs', async () => {
18
- const actualFs = await vi.importActual('node:fs');
19
- return {
20
- ...actualFs, // Import all actual functions first
21
- promises: {
22
- ...actualFs.promises, // Spread the actual promises implementation
23
- readFile: vi.fn()
24
- },
25
- };
26
- });
27
-
28
- // Mock the specific utility function used for energy calculation
29
- vi.spyOn(Optimization.Utils, 'evaluatePathDistance').mockImplementation(path => {
30
- // For tests, the energy is the sum of the IDs in the solution path.
31
- return path.reduce((sum, val) => sum + (val.id || 0), 0);
32
- });
33
-
34
- describe('ProblemManager', () => {
35
- let manager;
36
- let mockConfig;
37
- const configPath = 'fake/path.json';
38
- const store = inMemoryStore;
39
-
40
- beforeEach(async () => {
41
- // Reset mocks before each test
42
- // This also resets the singleton instance of the problem manager.
43
- problemManagerInternal.resetManager();
44
-
45
- vi.clearAllMocks();
46
- store.clear();
47
-
48
- // Default mock config for most tests
49
- mockConfig = [
50
- {
51
- "id": "tsp_10_cities",
52
- "workUnit": {
53
- "type": "simulated_annealing_iterations",
54
- "scoreFunction": "tsp.calculateEnergy", // Add the score function
55
- "baseIterations": 1000, // Lower value for testing
56
- "initialSolutionSource": "points", // Specify where to get the initial solution from
57
- "scalingFactor": 2.0
58
- },
59
- "payload": {
60
- // In tests, we use a more generic 'points' to match the implementation
61
- // of _ensureInitialSolution
62
- "points": {
63
- "$init": "generate:randomPoints",
64
- "params": { "count": 10 }
65
- },
66
- "options": { "initialTemperature": 1000, "coolingRate": 0.99 }
67
- },
68
- "state": { "bestSolution": null, "bestEnergy": "Infinity" }
69
- },
70
- {
71
- "id": "portfolio_5_assets",
72
- "workUnit": {
73
- "type": "genetic_algorithm_generations",
74
- "baseGenerations": 10, // Lower value for testing
75
- "scalingFactor": 1.5
76
- },
77
- "payload": {
78
- "assets": {
79
- "$init": "generate:randomAssets",
80
- "params": { "count": 5 }
81
- },
82
- "maxVolatility": 0.25
83
- },
84
- "state": { "population": null }
85
- }
86
- ];
87
-
88
- // Mock readFile to return our config. This needs to be in the top-level
89
- // beforeEach to apply to all test suites within this describe block.
90
- fs.readFile.mockResolvedValue(JSON.stringify(mockConfig));
91
- manager = await getProblemManager({ configPath }, store);
92
- });
93
-
94
- describe('Initialization and Loading', () => {
95
- it('should load and parse problems from the config file', async () => {
96
- expect(fs.readFile).toHaveBeenCalledWith(configPath, 'utf-8');
97
- expect(manager.problems.length).toBe(mockConfig.length);
98
- expect(manager.problems[0].id).toBe('tsp_10_cities');
99
- // Check if initial state was saved to the store
100
- const storedState = await store.get('problem-state:tsp_10_cities');
101
- expect(storedState).toEqual(mockConfig[0].state);
102
- });
103
-
104
- it('should dynamically generate cities and assets', async () => {
105
- const tspProblem = manager.problems.find(p => p.id === 'tsp_10_cities');
106
- const portfolioProblem = manager.problems.find(p => p.id === 'portfolio_5_assets');
107
-
108
- expect(tspProblem.payload.points.length).toBe(10);
109
- expect(tspProblem.payload.points[0]).toHaveProperty('x');
110
- expect(tspProblem.payload.points[0]).toHaveProperty('y');
111
-
112
- expect(Array.isArray(portfolioProblem.payload.assets)).toBe(true);
113
- expect(portfolioProblem.payload.assets.length).toBe(5);
114
- expect(portfolioProblem.payload.assets[0]).toHaveProperty('expectedReturn');
115
- expect(portfolioProblem.payload.assets[0]).toHaveProperty('volatility');
116
- });
117
-
118
- it('should handle file read errors gracefully', async () => {
119
- fs.readFile.mockRejectedValue(new Error('File not found'));
120
- const manager = await getProblemManager({ configPath: 'nonexistent.json' }, store);
121
- expect(manager.problems).toEqual([]);
122
- });
123
- });
124
-
125
- describe('dispatchWork', () => {
126
- it('should return null if no problems are loaded', async () => {
127
- fs.readFile.mockRejectedValue(new Error('File read error'));
128
- const manager = await getProblemManager({ configPath: 'bad.json' }, store);
129
- expect(manager.dispatchWork(0.5)).toBeNull();
130
- });
131
-
132
- it('should cycle through problems in a round-robin fashion', async () => {
133
- const work1 = manager.dispatchWork(0.1);
134
- const work2 = manager.dispatchWork(0.1);
135
- const work3 = manager.dispatchWork(0.1);
136
-
137
- expect(work1.problemId).toBe('tsp_10_cities');
138
- expect(work2.problemId).toBe('portfolio_5_assets');
139
- expect(work3.problemId).toBe('tsp_10_cities'); // Cycled back
140
- });
141
-
142
- it('should calculate exponential difficulty based on suspicionFactor', async () => {
143
- const suspicionFactor = 0.5;
144
-
145
- // Test for TSP problem
146
- const { task: tspTask } = manager.dispatchWork(suspicionFactor);
147
-
148
- // The base is now the MAX of the config and the hardcoded minimum (15000)
149
- const expectedBaseIterations = Math.max(15000, mockConfig[0].workUnit.baseIterations);
150
- const expectedTspIterations = Math.floor(expectedBaseIterations * Math.pow(2.0, suspicionFactor));
151
- expect(tspTask.iterations).toBe(expectedTspIterations);
152
-
153
- // Test for Portfolio problem
154
- const { task: portfolioTask } = manager.dispatchWork(suspicionFactor);
155
- const expectedBaseGenerations = Math.max(50, mockConfig[1].workUnit.baseGenerations);
156
- const expectedPortfolioGenerations = Math.floor(expectedBaseGenerations * Math.pow(1.5, suspicionFactor));
157
- expect(portfolioTask.generations).toBe(expectedPortfolioGenerations);
158
- });
159
- });
160
-
161
- describe('integrateSolution', () => {
162
- it('should update the best solution if a better one is provided', async () => {
163
- const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
164
- problem.state.bestEnergy = 1000; // Set a high initial energy
165
-
166
- const newBetterSolution = { solution: [{ id: 1 }], energy: 500 };
167
- // Le mock de `evaluatePathDistance` va retourner 1 (la somme des IDs).
168
- // C'est cette valeur qui doit être stockée, pas 500.
169
- const expectedRecalculatedEnergy = 1;
170
-
171
- await manager.integrateSolution('tsp_10_cities', newBetterSolution);
172
-
173
- expect(problem.state.bestSolution).toEqual(newBetterSolution.solution);
174
- expect(problem.state.bestEnergy).toBe(expectedRecalculatedEnergy); // Vérifier le score recalculé
175
- const storedState = await store.get('problem-state:tsp_10_cities');
176
- expect(storedState.bestEnergy).toBe(expectedRecalculatedEnergy);
177
- });
178
-
179
- it('should not update the best solution if a worse one is provided', async () => {
180
- const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
181
- const initialSolution = [{ id: 10 }];
182
- problem.state.bestSolution = initialSolution;
183
- problem.state.bestEnergy = 10; // Le score initial est 10 (calculé à partir de l'ID)
184
-
185
- const newWorseSolution = { solution: [{ id: 20 }], energy: 200 };
186
- // Le mock de `evaluatePathDistance` va retourner 20.
187
- // Comme 20 n'est pas meilleur que 10, la solution ne doit pas changer.
188
-
189
- await manager.integrateSolution('tsp_10_cities', newWorseSolution);
190
-
191
- expect(problem.state.bestSolution).toEqual(initialSolution);
192
- expect(problem.state.bestEnergy).toBe(10);
193
- const storedState = await store.get('problem-state:tsp_10_cities');
194
- expect(storedState.bestEnergy).toBe(10);
195
- });
196
-
197
- it('should handle solutions for non-existent problems gracefully', async () => {
198
- // This should not throw an error
199
- await manager.integrateSolution('non_existent_problem', { energy: 1 });
200
- expect(await store.has('problem-state:non_existent_problem')).toBe(false);
201
- });
202
-
203
- it('should integrate a new Pareto front for multi-objective problems', async () => {
204
- // **LA CORRECTION** : Réinitialiser le singleton avant de modifier la configuration.
205
- problemManagerInternal.resetManager();
206
-
207
- // Add a multi-objective problem to the config for this test
208
- mockConfig.push({
209
- "id": "multi_obj_test",
210
- "workUnit": { "type": "multi_objective_genetic_algorithm" },
211
- "state": { "paretoFront": [{ solution: 'A', objectives: [10, 20] }] }
212
- });
213
- fs.readFile.mockResolvedValue(JSON.stringify(mockConfig));
214
- manager = await getProblemManager(configPath, store);
215
-
216
- const problem = manager.problems.find(p => p.id === 'multi_obj_test');
217
-
218
- // The new front contains a solution that dominates the old one.
219
- const newFrontFromClient = [{ solution: 'B', objectives: [5, 15] }];
220
-
221
- await manager.integrateSolution('multi_obj_test', { paretoFront: newFrontFromClient });
222
-
223
- // The new front should contain only the new, dominant solution.
224
- // We check the content instead of object equality for robustness.
225
- expect(problem.state.paretoFront).toHaveLength(1);
226
- expect(problem.state.paretoFront[0]).toEqual({ solution: 'B', objectives: [5, 15] });
227
- expect(problem.state.lastUpdate).toBeDefined();
228
- const storedState = await store.get('problem-state:multi_obj_test');
229
- expect(storedState.paretoFront[0].solution).toBe('B');
230
- });
231
- });
232
-
233
- describe('getBestSolutions', () => {
234
- beforeEach(async () => {
235
- // Mock the initial solution generation to be predictable
236
- mockConfig[0].payload.points = [
237
- { id: 1, x: 10, y: 10 },
238
- { id: 2, x: 20, y: 20 },
239
- { id: 3, x: 30, y: 30 }
240
- ];
241
-
242
- // The function that is actually called is `Optimization.Utils.evaluatePathDistance`.
243
- // We need to mock its return value to be predictable for the test.
244
- // The mock implementation `solution.reduce(...)` will sum the `id` properties.
245
- // For the points above, the sum is 1 + 2 + 3 = 6.
246
- vi.spyOn(Optimization.Utils, 'evaluatePathDistance').mockReturnValue(6);
247
- });
248
-
249
- it('should generate an initial solution if none exists', async () => {
250
- const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
251
- problem.state.bestSolution = null; // Ensure no solution exists
252
-
253
- await manager.getBestSolutions('tsp_10_cities');
254
-
255
- expect(problem.state.bestSolution).not.toBeNull();
256
- expect(problem.state.bestEnergy).toBe(6); // Mocked energy value
257
- const storedState = await store.get('problem-state:tsp_10_cities');
258
- expect(storedState.bestEnergy).toBe(6);
259
- });
260
-
261
- it('should return the best solution for a specific problem ID', async () => {
262
- const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
263
- problem.state.bestSolution = [{ id: 'A' }];
264
- problem.state.bestEnergy = 123;
265
- problem.state.lastUpdate = '2023-01-01T00:00:00.000Z';
266
-
267
- const result = await manager.getBestSolutions('tsp_10_cities');
268
-
269
- expect(result).toEqual({
270
- id: 'tsp_10_cities',
271
- solution: [{ id: 'A' }],
272
- score: 123,
273
- lastUpdate: '2023-01-01T00:00:00.000Z'
274
- });
275
- });
276
-
277
- it('should return an array of all best solutions if no ID is provided', async () => {
278
- const problem1 = manager.problems.find(p => p.id === 'tsp_10_cities');
279
- problem1.state.bestSolution = [{ id: 'A' }];
280
- problem1.state.bestEnergy = 123;
281
-
282
- // The portfolio problem has no solution, so it should be filtered out
283
- const results = await manager.getBestSolutions();
284
-
285
- expect(Array.isArray(results)).toBe(true);
286
- expect(results.length).toBe(1);
287
- expect(results[0].id).toBe('tsp_10_cities');
288
- expect(results[0].score).toBe(123);
289
- });
290
-
291
- it('should return null if a non-existent problem ID is requested', async () => {
292
- const result = await manager.getBestSolutions('non_existent_problem');
293
- expect(result).toBeNull();
294
- });
295
- });
296
-
297
- describe('updateProblemPayload', () => {
298
- it('should update the payload of a specific problem', async () => {
299
- const newPayload = {
300
- "points": [{ "x": 0, "y": 0 }],
301
- "options": { "initialTemperature": 500 }
302
- };
303
-
304
- const success = await manager.updateProblemPayload('tsp_10_cities', newPayload);
305
- expect(success).toBe(true);
306
-
307
- const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
308
- expect(problem.payload).toEqual(newPayload);
309
- });
310
-
311
- it('should reset the state of the updated problem', async () => {
312
- const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
313
- problem.state.bestSolution = [{ id: 1 }];
314
- problem.state.bestEnergy = 10;
315
-
316
- await manager.updateProblemPayload('tsp_10_cities', { new: 'payload' });
317
- expect(problem.state.bestSolution).toBeNull();
318
- expect(problem.state.bestEnergy).toBe("Infinity");
319
- const storedState = await store.get('problem-state:tsp_10_cities');
320
- expect(storedState.bestEnergy).toBe("Infinity");
321
- });
322
- });
1
+ import {beforeEach, describe, expect, it, vi} from 'vitest';
2
+ import {promises as fs} from 'node:fs';
3
+ import {__internal as problemManagerInternal, getProblemManager} from '../problem-manager.js';
4
+ import {Optimization} from '../library.js';
5
+
6
+ // Mock the in-memory store for testing
7
+ const inMemoryStore = {
8
+ _map: new Map(),
9
+ async get(key) { return this._map.get(key); },
10
+ async set(key, value, ttl) { this._map.set(key, value); },
11
+ async has(key) { return this._map.has(key); },
12
+ async delete(key) { this._map.delete(key); },
13
+ clear() { this._map.clear(); }
14
+ };
15
+
16
+ // Mock the 'fs' module
17
+ vi.mock('node:fs', async () => {
18
+ const actualFs = await vi.importActual('node:fs');
19
+ return {
20
+ ...actualFs, // Import all actual functions first
21
+ promises: {
22
+ ...actualFs.promises, // Spread the actual promises implementation
23
+ readFile: vi.fn()
24
+ },
25
+ };
26
+ });
27
+
28
+ // Mock the specific utility function used for energy calculation
29
+ vi.spyOn(Optimization.Utils, 'evaluatePathDistance').mockImplementation(path => {
30
+ // For tests, the energy is the sum of the IDs in the solution path.
31
+ return path.reduce((sum, val) => sum + (val.id || 0), 0);
32
+ });
33
+
34
+ describe('ProblemManager', () => {
35
+ let manager;
36
+ let mockConfig;
37
+ const configPath = 'fake/path.json';
38
+ const store = inMemoryStore;
39
+
40
+ beforeEach(async () => {
41
+ // Reset mocks before each test
42
+ // This also resets the singleton instance of the problem manager.
43
+ problemManagerInternal.resetManager();
44
+
45
+ vi.clearAllMocks();
46
+ store.clear();
47
+
48
+ // Default mock config for most tests
49
+ mockConfig = [
50
+ {
51
+ "id": "tsp_10_cities",
52
+ "workUnit": {
53
+ "type": "simulated_annealing_iterations",
54
+ "scoreFunction": "tsp.calculateEnergy", // Add the score function
55
+ "baseIterations": 1000, // Lower value for testing
56
+ "initialSolutionSource": "points", // Specify where to get the initial solution from
57
+ "scalingFactor": 2.0
58
+ },
59
+ "payload": {
60
+ // In tests, we use a more generic 'points' to match the implementation
61
+ // of _ensureInitialSolution
62
+ "points": {
63
+ "$init": "generate:randomPoints",
64
+ "params": { "count": 10 }
65
+ },
66
+ "options": { "initialTemperature": 1000, "coolingRate": 0.99 }
67
+ },
68
+ "state": { "bestSolution": null, "bestEnergy": "Infinity" }
69
+ },
70
+ {
71
+ "id": "portfolio_5_assets",
72
+ "workUnit": {
73
+ "type": "genetic_algorithm_generations",
74
+ "baseGenerations": 10, // Lower value for testing
75
+ "scalingFactor": 1.5
76
+ },
77
+ "payload": {
78
+ "assets": {
79
+ "$init": "generate:randomAssets",
80
+ "params": { "count": 5 }
81
+ },
82
+ "maxVolatility": 0.25
83
+ },
84
+ "state": { "population": null }
85
+ }
86
+ ];
87
+
88
+ // Mock readFile to return our config. This needs to be in the top-level
89
+ // beforeEach to apply to all test suites within this describe block.
90
+ fs.readFile.mockResolvedValue(JSON.stringify(mockConfig));
91
+ manager = await getProblemManager({ configPath }, store);
92
+ });
93
+
94
+ describe('Initialization and Loading', () => {
95
+ it('should load and parse problems from the config file', async () => {
96
+ expect(fs.readFile).toHaveBeenCalledWith(configPath, 'utf-8');
97
+ expect(manager.problems.length).toBe(mockConfig.length);
98
+ expect(manager.problems[0].id).toBe('tsp_10_cities');
99
+ // Check if initial state was saved to the store
100
+ const storedState = await store.get('problem-state:tsp_10_cities');
101
+ expect(storedState).toEqual(mockConfig[0].state);
102
+ });
103
+
104
+ it('should dynamically generate cities and assets', async () => {
105
+ const tspProblem = manager.problems.find(p => p.id === 'tsp_10_cities');
106
+ const portfolioProblem = manager.problems.find(p => p.id === 'portfolio_5_assets');
107
+
108
+ expect(tspProblem.payload.points.length).toBe(10);
109
+ expect(tspProblem.payload.points[0]).toHaveProperty('x');
110
+ expect(tspProblem.payload.points[0]).toHaveProperty('y');
111
+
112
+ expect(Array.isArray(portfolioProblem.payload.assets)).toBe(true);
113
+ expect(portfolioProblem.payload.assets.length).toBe(5);
114
+ expect(portfolioProblem.payload.assets[0]).toHaveProperty('expectedReturn');
115
+ expect(portfolioProblem.payload.assets[0]).toHaveProperty('volatility');
116
+ });
117
+
118
+ it('should handle file read errors gracefully', async () => {
119
+ fs.readFile.mockRejectedValue(new Error('File not found'));
120
+ const manager = await getProblemManager({ configPath: 'nonexistent.json' }, store);
121
+ expect(manager.problems).toEqual([]);
122
+ });
123
+ });
124
+
125
+ describe('dispatchWork', () => {
126
+ it('should return null if no problems are loaded', async () => {
127
+ fs.readFile.mockRejectedValue(new Error('File read error'));
128
+ const manager = await getProblemManager({ configPath: 'bad.json' }, store);
129
+ expect(manager.dispatchWork(0.5)).toBeNull();
130
+ });
131
+
132
+ it('should cycle through problems in a round-robin fashion', async () => {
133
+ const work1 = manager.dispatchWork(0.1);
134
+ const work2 = manager.dispatchWork(0.1);
135
+ const work3 = manager.dispatchWork(0.1);
136
+
137
+ expect(work1.problemId).toBe('tsp_10_cities');
138
+ expect(work2.problemId).toBe('portfolio_5_assets');
139
+ expect(work3.problemId).toBe('tsp_10_cities'); // Cycled back
140
+ });
141
+
142
+ it('should calculate exponential difficulty based on suspicionFactor', async () => {
143
+ const suspicionFactor = 0.5;
144
+
145
+ // Test for TSP problem
146
+ const { task: tspTask } = manager.dispatchWork(suspicionFactor);
147
+
148
+ // The base is now the MAX of the config and the hardcoded minimum (15000)
149
+ const expectedBaseIterations = Math.max(15000, mockConfig[0].workUnit.baseIterations);
150
+ const expectedTspIterations = Math.floor(expectedBaseIterations * Math.pow(2.0, suspicionFactor));
151
+ expect(tspTask.iterations).toBe(expectedTspIterations);
152
+
153
+ // Test for Portfolio problem
154
+ const { task: portfolioTask } = manager.dispatchWork(suspicionFactor);
155
+ const expectedBaseGenerations = Math.max(50, mockConfig[1].workUnit.baseGenerations);
156
+ const expectedPortfolioGenerations = Math.floor(expectedBaseGenerations * Math.pow(1.5, suspicionFactor));
157
+ expect(portfolioTask.generations).toBe(expectedPortfolioGenerations);
158
+ });
159
+ });
160
+
161
+ describe('integrateSolution', () => {
162
+ it('should update the best solution if a better one is provided', async () => {
163
+ const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
164
+ problem.state.bestEnergy = 1000; // Set a high initial energy
165
+
166
+ const newBetterSolution = { solution: [{ id: 1 }], energy: 500 };
167
+ // Le mock de `evaluatePathDistance` va retourner 1 (la somme des IDs).
168
+ // C'est cette valeur qui doit être stockée, pas 500.
169
+ const expectedRecalculatedEnergy = 1;
170
+
171
+ await manager.integrateSolution('tsp_10_cities', newBetterSolution);
172
+
173
+ expect(problem.state.bestSolution).toEqual(newBetterSolution.solution);
174
+ expect(problem.state.bestEnergy).toBe(expectedRecalculatedEnergy); // Vérifier le score recalculé
175
+ const storedState = await store.get('problem-state:tsp_10_cities');
176
+ expect(storedState.bestEnergy).toBe(expectedRecalculatedEnergy);
177
+ });
178
+
179
+ it('should not update the best solution if a worse one is provided', async () => {
180
+ const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
181
+ const initialSolution = [{ id: 10 }];
182
+ problem.state.bestSolution = initialSolution;
183
+ problem.state.bestEnergy = 10; // Le score initial est 10 (calculé à partir de l'ID)
184
+
185
+ const newWorseSolution = { solution: [{ id: 20 }], energy: 200 };
186
+ // Le mock de `evaluatePathDistance` va retourner 20.
187
+ // Comme 20 n'est pas meilleur que 10, la solution ne doit pas changer.
188
+
189
+ await manager.integrateSolution('tsp_10_cities', newWorseSolution);
190
+
191
+ expect(problem.state.bestSolution).toEqual(initialSolution);
192
+ expect(problem.state.bestEnergy).toBe(10);
193
+ const storedState = await store.get('problem-state:tsp_10_cities');
194
+ expect(storedState.bestEnergy).toBe(10);
195
+ });
196
+
197
+ it('should update the best facility location solution with recalculated energy', async () => {
198
+ problemManagerInternal.resetManager();
199
+ mockConfig.push({
200
+ "id": "facility_location_test",
201
+ "workUnit": {
202
+ "type": "simulated_annealing_iterations",
203
+ "scoreFunction": "facility.calculateEnergy"
204
+ },
205
+ "payload": {
206
+ "customers": [{"x": 100, "y": 100}, {"x": 200, "y": 200}],
207
+ "options": {"fixedCostPerFacility": 1500}
208
+ },
209
+ "state": { "bestSolution": null, "bestEnergy": "Infinity" }
210
+ });
211
+ fs.readFile.mockResolvedValue(JSON.stringify(mockConfig));
212
+ manager = await getProblemManager(configPath, store);
213
+
214
+ const problem = manager.problems.find(p => p.id === 'facility_location_test');
215
+ problem.state.bestEnergy = 99999.0;
216
+
217
+ const newBetterSolution = {
218
+ solution: [
219
+ { x: 100, y: 100 },
220
+ { x: 200, y: 200 }
221
+ ],
222
+ energy: 1500.0
223
+ };
224
+
225
+ await manager.integrateSolution('facility_location_test', newBetterSolution);
226
+
227
+ expect(problem.state.bestSolution).toEqual(newBetterSolution.solution);
228
+ expect(problem.state.bestEnergy).toBeLessThan(99999.0);
229
+ const storedState = await store.get('problem-state:facility_location_test');
230
+ expect(storedState.bestEnergy).toBeLessThan(99999.0);
231
+ });
232
+
233
+ it('should handle solutions for non-existent problems gracefully', async () => {
234
+ // This should not throw an error
235
+ await manager.integrateSolution('non_existent_problem', { energy: 1 });
236
+ expect(await store.has('problem-state:non_existent_problem')).toBe(false);
237
+ });
238
+
239
+ it('should integrate a new Pareto front for multi-objective problems', async () => {
240
+ // **LA CORRECTION** : Réinitialiser le singleton avant de modifier la configuration.
241
+ problemManagerInternal.resetManager();
242
+
243
+ // Add a multi-objective problem to the config for this test
244
+ mockConfig.push({
245
+ "id": "multi_obj_test",
246
+ "workUnit": { "type": "multi_objective_genetic_algorithm" },
247
+ "state": { "paretoFront": [{ solution: 'A', objectives: [10, 20] }] }
248
+ });
249
+ fs.readFile.mockResolvedValue(JSON.stringify(mockConfig));
250
+ manager = await getProblemManager(configPath, store);
251
+
252
+ const problem = manager.problems.find(p => p.id === 'multi_obj_test');
253
+
254
+ // The new front contains a solution that dominates the old one.
255
+ const newFrontFromClient = [{ solution: 'B', objectives: [5, 15] }];
256
+
257
+ await manager.integrateSolution('multi_obj_test', { paretoFront: newFrontFromClient });
258
+
259
+ // The new front should contain only the new, dominant solution.
260
+ // We check the content instead of object equality for robustness.
261
+ expect(problem.state.paretoFront).toHaveLength(1);
262
+ expect(problem.state.paretoFront[0]).toEqual({ solution: 'B', objectives: [5, 15] });
263
+ expect(problem.state.lastUpdate).toBeDefined();
264
+ const storedState = await store.get('problem-state:multi_obj_test');
265
+ expect(storedState.paretoFront[0].solution).toBe('B');
266
+ });
267
+ });
268
+
269
+ describe('getBestSolutions', () => {
270
+ beforeEach(async () => {
271
+ // Mock the initial solution generation to be predictable
272
+ mockConfig[0].payload.points = [
273
+ { id: 1, x: 10, y: 10 },
274
+ { id: 2, x: 20, y: 20 },
275
+ { id: 3, x: 30, y: 30 }
276
+ ];
277
+
278
+ // The function that is actually called is `Optimization.Utils.evaluatePathDistance`.
279
+ // We need to mock its return value to be predictable for the test.
280
+ // The mock implementation `solution.reduce(...)` will sum the `id` properties.
281
+ // For the points above, the sum is 1 + 2 + 3 = 6.
282
+ vi.spyOn(Optimization.Utils, 'evaluatePathDistance').mockReturnValue(6);
283
+ });
284
+
285
+ it('should generate an initial solution if none exists', async () => {
286
+ const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
287
+ problem.state.bestSolution = null; // Ensure no solution exists
288
+
289
+ await manager.getBestSolutions('tsp_10_cities');
290
+
291
+ expect(problem.state.bestSolution).not.toBeNull();
292
+ expect(problem.state.bestEnergy).toBe(6); // Mocked energy value
293
+ const storedState = await store.get('problem-state:tsp_10_cities');
294
+ expect(storedState.bestEnergy).toBe(6);
295
+ });
296
+
297
+ it('should return the best solution for a specific problem ID', async () => {
298
+ const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
299
+ problem.state.bestSolution = [{ id: 'A' }];
300
+ problem.state.bestEnergy = 123;
301
+ problem.state.lastUpdate = '2023-01-01T00:00:00.000Z';
302
+
303
+ const result = await manager.getBestSolutions('tsp_10_cities');
304
+
305
+ expect(result).toEqual({
306
+ id: 'tsp_10_cities',
307
+ solution: [{ id: 'A' }],
308
+ score: 123,
309
+ lastUpdate: '2023-01-01T00:00:00.000Z'
310
+ });
311
+ });
312
+
313
+ it('should return an array of all best solutions if no ID is provided', async () => {
314
+ const problem1 = manager.problems.find(p => p.id === 'tsp_10_cities');
315
+ problem1.state.bestSolution = [{ id: 'A' }];
316
+ problem1.state.bestEnergy = 123;
317
+
318
+ // The portfolio problem has no solution, so it should be filtered out
319
+ const results = await manager.getBestSolutions();
320
+
321
+ expect(Array.isArray(results)).toBe(true);
322
+ expect(results.length).toBe(1);
323
+ expect(results[0].id).toBe('tsp_10_cities');
324
+ expect(results[0].score).toBe(123);
325
+ });
326
+
327
+ it('should return null if a non-existent problem ID is requested', async () => {
328
+ const result = await manager.getBestSolutions('non_existent_problem');
329
+ expect(result).toBeNull();
330
+ });
331
+ });
332
+
333
+ describe('updateProblemPayload', () => {
334
+ it('should update the payload of a specific problem', async () => {
335
+ const newPayload = {
336
+ "points": [{ "x": 0, "y": 0 }],
337
+ "options": { "initialTemperature": 500 }
338
+ };
339
+
340
+ const success = await manager.updateProblemPayload('tsp_10_cities', newPayload);
341
+ expect(success).toBe(true);
342
+
343
+ const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
344
+ expect(problem.payload).toEqual(newPayload);
345
+ });
346
+
347
+ it('should reset the state of the updated problem', async () => {
348
+ const problem = manager.problems.find(p => p.id === 'tsp_10_cities');
349
+ problem.state.bestSolution = [{ id: 1 }];
350
+ problem.state.bestEnergy = 10;
351
+
352
+ await manager.updateProblemPayload('tsp_10_cities', { new: 'payload' });
353
+ expect(problem.state.bestSolution).toBeNull();
354
+ expect(problem.state.bestEnergy).toBe("Infinity");
355
+ const storedState = await store.get('problem-state:tsp_10_cities');
356
+ expect(storedState.bestEnergy).toBe("Infinity");
357
+ });
358
+ });
323
359
  });