push-guardian 1.0.0

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 (96) hide show
  1. package/.dockerignore +15 -0
  2. package/.pushguardian-plugins.json +10 -0
  3. package/Dockerfile +41 -0
  4. package/Dockerfile.dev +20 -0
  5. package/README.md +386 -0
  6. package/TECHNO.md +139 -0
  7. package/babel.config.js +1 -0
  8. package/developper_utils.md +119 -0
  9. package/docker-compose.yml +41 -0
  10. package/docs/PLUGINS.md +223 -0
  11. package/docs/technical/architecture.md +298 -0
  12. package/docs/technical/performance-guide.md +390 -0
  13. package/docs/technical/plugin-persistence.md +169 -0
  14. package/docs/technical/plugins-guide.md +409 -0
  15. package/jest.config.js +22 -0
  16. package/package.json +53 -0
  17. package/plugins/example-plugin/index.js +55 -0
  18. package/plugins/example-plugin/plugin.json +8 -0
  19. package/scripts/coverage-report.js +75 -0
  20. package/src/cli/command/config.js +33 -0
  21. package/src/cli/command/install.js +137 -0
  22. package/src/cli/command/mirror.js +90 -0
  23. package/src/cli/command/performance.js +160 -0
  24. package/src/cli/command/plugin.js +171 -0
  25. package/src/cli/command/security.js +152 -0
  26. package/src/cli/command/shell.js +238 -0
  27. package/src/cli/command/validate.js +54 -0
  28. package/src/cli/index.js +23 -0
  29. package/src/cli/install/codeQualityTools.js +156 -0
  30. package/src/cli/install/hooks.js +89 -0
  31. package/src/cli/install/mirroring.js +299 -0
  32. package/src/core/codeQualityTools/configAnalyzer.js +216 -0
  33. package/src/core/codeQualityTools/configGenerator.js +381 -0
  34. package/src/core/codeQualityTools/configManager.js +65 -0
  35. package/src/core/codeQualityTools/fileDetector.js +62 -0
  36. package/src/core/codeQualityTools/languageTools.js +104 -0
  37. package/src/core/codeQualityTools/toolInstaller.js +53 -0
  38. package/src/core/configManager.js +43 -0
  39. package/src/core/errorCMD.js +9 -0
  40. package/src/core/interactiveMenu/interactiveMenu.js +73 -0
  41. package/src/core/mirroring/branchSynchronizer.js +59 -0
  42. package/src/core/mirroring/generate.js +114 -0
  43. package/src/core/mirroring/repoManager.js +112 -0
  44. package/src/core/mirroring/syncManager.js +176 -0
  45. package/src/core/module/env-loader.js +109 -0
  46. package/src/core/performance/metricsCollector.js +217 -0
  47. package/src/core/performance/performanceAnalyzer.js +182 -0
  48. package/src/core/plugins/basePlugin.js +89 -0
  49. package/src/core/plugins/pluginManager.js +123 -0
  50. package/src/core/plugins/pluginRegistry.js +215 -0
  51. package/src/core/validator.js +53 -0
  52. package/src/hooks/constrains/constrains.js +174 -0
  53. package/src/hooks/constrains/constraintEngine.js +140 -0
  54. package/src/utils/chalk-wrapper.js +26 -0
  55. package/src/utils/exec-wrapper.js +6 -0
  56. package/tests/fixtures/mock-eslint-config-array.js +8 -0
  57. package/tests/fixtures/mock-eslint-config-single.js +6 -0
  58. package/tests/fixtures/mockLoadedPlugin.js +11 -0
  59. package/tests/setup.js +28 -0
  60. package/tests/unit/basePlugin.test.js +355 -0
  61. package/tests/unit/branchSynchronizer.test.js +308 -0
  62. package/tests/unit/cli-commands.test.js +144 -0
  63. package/tests/unit/codeQualityConfigManager.test.js +233 -0
  64. package/tests/unit/codeQualityTools.test.js +36 -0
  65. package/tests/unit/command-install.test.js +247 -0
  66. package/tests/unit/command-mirror.test.js +179 -0
  67. package/tests/unit/command-performance.test.js +169 -0
  68. package/tests/unit/command-plugin.test.js +288 -0
  69. package/tests/unit/command-security.test.js +277 -0
  70. package/tests/unit/command-shell.test.js +325 -0
  71. package/tests/unit/configAnalyzer.test.js +593 -0
  72. package/tests/unit/configGenerator.test.js +808 -0
  73. package/tests/unit/configManager.test.js +195 -0
  74. package/tests/unit/constrains.test.js +463 -0
  75. package/tests/unit/constraint.test.js +554 -0
  76. package/tests/unit/env-loader.test.js +279 -0
  77. package/tests/unit/fileDetector.test.js +171 -0
  78. package/tests/unit/install-codeQualityTools.test.js +343 -0
  79. package/tests/unit/install-hooks.test.js +280 -0
  80. package/tests/unit/install-mirroring.test.js +731 -0
  81. package/tests/unit/install-modules.test.js +81 -0
  82. package/tests/unit/interactiveMenu.test.js +426 -0
  83. package/tests/unit/languageTools.test.js +244 -0
  84. package/tests/unit/metricsCollector.test.js +354 -0
  85. package/tests/unit/mirroring-generate.test.js +96 -0
  86. package/tests/unit/modules-exist.test.js +96 -0
  87. package/tests/unit/performanceAnalyzer.test.js +473 -0
  88. package/tests/unit/pluginManager.test.js +427 -0
  89. package/tests/unit/pluginRegistry.test.js +592 -0
  90. package/tests/unit/repoManager.test.js +469 -0
  91. package/tests/unit/reviewAppManager.test.js +5 -0
  92. package/tests/unit/security-command.test.js +43 -0
  93. package/tests/unit/syncManager.test.js +494 -0
  94. package/tests/unit/toolInstaller.test.js +240 -0
  95. package/tests/unit/utils.test.js +144 -0
  96. package/tests/unit/validator.test.js +215 -0
@@ -0,0 +1,81 @@
1
+ // Mock interactiveMenu au niveau du module pour éviter les problèmes avec stdin
2
+ jest.mock('../../src/core/interactiveMenu/interactiveMenu', () => {
3
+ return jest.fn(() => Promise.resolve(['Hooks Git']));
4
+ });
5
+
6
+ const interactiveMenu = require('../../src/core/interactiveMenu/interactiveMenu');
7
+
8
+ describe('Core InteractiveMenu', () => {
9
+ test('le module doit être une fonction', () => {
10
+ expect(typeof interactiveMenu).toBe('function');
11
+ });
12
+
13
+ test('doit retourner une Promise', () => {
14
+ const result = interactiveMenu('Test', ['Option 1', 'Option 2']);
15
+ expect(result).toBeInstanceOf(Promise);
16
+ });
17
+ });
18
+
19
+ describe('CLI Install - hooks', () => {
20
+ const fs = require('fs');
21
+
22
+ beforeEach(() => {
23
+ jest.clearAllMocks();
24
+ jest.spyOn(console, 'log').mockImplementation(() => {});
25
+ jest.spyOn(console, 'error').mockImplementation(() => {});
26
+ });
27
+
28
+ afterEach(() => {
29
+ console.log.mockRestore();
30
+ console.error.mockRestore();
31
+ });
32
+
33
+ test('le module doit être chargeable', () => {
34
+ expect(() => require('../../src/cli/install/hooks')).not.toThrow();
35
+ });
36
+
37
+ test('installHooks doit être une fonction', () => {
38
+ const { installHooks } = require('../../src/cli/install/hooks');
39
+ expect(typeof installHooks).toBe('function');
40
+ });
41
+ });
42
+
43
+ describe('CLI Install - codeQualityTools', () => {
44
+ beforeEach(() => {
45
+ jest.clearAllMocks();
46
+ jest.spyOn(console, 'log').mockImplementation(() => {});
47
+ });
48
+
49
+ afterEach(() => {
50
+ console.log.mockRestore();
51
+ });
52
+
53
+ test('le module doit être chargeable', () => {
54
+ expect(() => require('../../src/cli/install/codeQualityTools')).not.toThrow();
55
+ });
56
+
57
+ test('installCodeQualityTools doit être une fonction', () => {
58
+ const { installCodeQualityTools } = require('../../src/cli/install/codeQualityTools');
59
+ expect(typeof installCodeQualityTools).toBe('function');
60
+ });
61
+ });
62
+
63
+ describe('CLI Install - mirroring', () => {
64
+ beforeEach(() => {
65
+ jest.clearAllMocks();
66
+ jest.spyOn(console, 'log').mockImplementation(() => {});
67
+ });
68
+
69
+ afterEach(() => {
70
+ console.log.mockRestore();
71
+ });
72
+
73
+ test('le module doit être chargeable', () => {
74
+ expect(() => require('../../src/cli/install/mirroring')).not.toThrow();
75
+ });
76
+
77
+ test('installMirroringTools doit être une fonction', () => {
78
+ const { installMirroringTools } = require('../../src/cli/install/mirroring');
79
+ expect(typeof installMirroringTools).toBe('function');
80
+ });
81
+ });
@@ -0,0 +1,426 @@
1
+ // Mock stdin before requiring the module
2
+ const mockStdin = {
3
+ setRawMode: jest.fn(),
4
+ resume: jest.fn(),
5
+ pause: jest.fn(),
6
+ setEncoding: jest.fn(),
7
+ removeListener: jest.fn(),
8
+ on: jest.fn(),
9
+ removeAllListeners: jest.fn(),
10
+ setMaxListeners: jest.fn()
11
+ };
12
+
13
+ // Replace stdin with mock
14
+ const originalStdin = process.stdin;
15
+ Object.defineProperty(process, 'stdin', {
16
+ value: mockStdin,
17
+ writable: true,
18
+ configurable: true
19
+ });
20
+
21
+ const interactiveMenu = require('../../src/core/interactiveMenu/interactiveMenu');
22
+
23
+ describe('Core InteractiveMenu - interactiveMenu', () => {
24
+ beforeEach(() => {
25
+ jest.clearAllMocks();
26
+ jest.spyOn(console, 'clear').mockImplementation(() => {});
27
+ jest.spyOn(console, 'log').mockImplementation(() => {});
28
+ jest.spyOn(process, 'exit').mockImplementation(() => {});
29
+
30
+ // Reset mock implementations
31
+ mockStdin.setRawMode.mockReturnValue(undefined);
32
+ mockStdin.resume.mockReturnValue(undefined);
33
+ mockStdin.pause.mockReturnValue(undefined);
34
+ mockStdin.setEncoding.mockReturnValue(undefined);
35
+ mockStdin.removeListener.mockReturnValue(undefined);
36
+ mockStdin.on.mockReturnValue(undefined);
37
+ mockStdin.removeAllListeners.mockReturnValue(undefined);
38
+ });
39
+
40
+ afterEach(() => {
41
+ console.clear.mockRestore();
42
+ console.log.mockRestore();
43
+ if (process.exit && process.exit.mockRestore) process.exit.mockRestore();
44
+ });
45
+
46
+ describe('module export', () => {
47
+ test('doit être une fonction', () => {
48
+ expect(typeof interactiveMenu).toBe('function');
49
+ });
50
+
51
+ test('doit retourner une Promise', () => {
52
+ const result = interactiveMenu('Test', ['Option 1', 'Option 2']);
53
+ expect(result).toBeInstanceOf(Promise);
54
+ });
55
+ });
56
+
57
+ describe('affichage', () => {
58
+ test('doit afficher le message', () => {
59
+ interactiveMenu('Test message', ['Option 1']);
60
+
61
+ expect(console.clear).toHaveBeenCalled();
62
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Test message'));
63
+ });
64
+
65
+ test('doit afficher toutes les options', () => {
66
+ const choices = ['Option 1', 'Option 2', 'Option 3'];
67
+ interactiveMenu('Test', choices);
68
+
69
+ choices.forEach(choice => {
70
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining(choice));
71
+ });
72
+ });
73
+
74
+ test('doit afficher les instructions', () => {
75
+ interactiveMenu('Test', ['Option 1']);
76
+
77
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Up'));
78
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Down'));
79
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Select'));
80
+ });
81
+
82
+ test('doit mettre en surbrillance première option', () => {
83
+ interactiveMenu('Test', ['Option 1', 'Option 2']);
84
+
85
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('>'));
86
+ });
87
+
88
+ test('doit marquer options présélectionnées', () => {
89
+ interactiveMenu('Test', ['Option 1', 'Option 2'], [1]);
90
+
91
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('*'));
92
+ });
93
+ });
94
+
95
+ describe('comportement', () => {
96
+ test('doit accepter présélections vides', () => {
97
+ expect(() => {
98
+ interactiveMenu('Test', ['Option 1', 'Option 2'], []);
99
+ }).not.toThrow();
100
+ });
101
+
102
+ test('doit accepter présélections multiples', () => {
103
+ expect(() => {
104
+ interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3'], [0, 2]);
105
+ }).not.toThrow();
106
+ });
107
+
108
+ test('doit gérer une seule option', () => {
109
+ interactiveMenu('Test', ['Unique option']);
110
+
111
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('Unique option'));
112
+ });
113
+
114
+ test('doit gérer liste vide', () => {
115
+ expect(() => {
116
+ interactiveMenu('Test', []);
117
+ }).not.toThrow();
118
+ });
119
+
120
+ test('doit créer Promise pour chaque appel', () => {
121
+ const promise1 = interactiveMenu('Test', ['Option 1']);
122
+ const promise2 = interactiveMenu('Test', ['Option 2']);
123
+
124
+ expect(promise1).toBeInstanceOf(Promise);
125
+ expect(promise2).toBeInstanceOf(Promise);
126
+ expect(promise1).not.toBe(promise2);
127
+ });
128
+ });
129
+
130
+ describe('paramètres', () => {
131
+ test('doit accepter message et choices', () => {
132
+ expect(() => {
133
+ interactiveMenu('Message', ['Choice 1', 'Choice 2']);
134
+ }).not.toThrow();
135
+ });
136
+
137
+ test('doit accepter présélections optionnelles', () => {
138
+ expect(() => {
139
+ interactiveMenu('Test', ['Option']);
140
+ }).not.toThrow();
141
+ });
142
+
143
+ test('doit gérer tableau présélections', () => {
144
+ expect(() => {
145
+ interactiveMenu('Test', ['A', 'B', 'C'], [0, 1]);
146
+ }).not.toThrow();
147
+ });
148
+ });
149
+
150
+ describe('navigation avec touches', () => {
151
+ let handleInput;
152
+
153
+ beforeEach(() => {
154
+ mockStdin.on.mockImplementation((event, callback) => {
155
+ if (event === 'data') {
156
+ handleInput = callback;
157
+ }
158
+ });
159
+ });
160
+
161
+ test('doit naviguer vers le haut avec flèche haut', () => {
162
+ interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
163
+
164
+ const initialLogCount = console.log.mock.calls.length;
165
+ handleInput('\u001B[A'); // UP Arrow
166
+
167
+ expect(console.log.mock.calls.length).toBeGreaterThan(initialLogCount);
168
+ expect(console.clear).toHaveBeenCalled();
169
+ });
170
+
171
+ test('doit naviguer vers le bas avec flèche bas', () => {
172
+ interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
173
+
174
+ const initialLogCount = console.log.mock.calls.length;
175
+ handleInput('\u001B[B'); // DOWN Arrow
176
+
177
+ expect(console.log.mock.calls.length).toBeGreaterThan(initialLogCount);
178
+ expect(console.clear).toHaveBeenCalled();
179
+ });
180
+
181
+ test('doit boucler navigation vers le haut depuis première option', () => {
182
+ interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
183
+
184
+ console.clear.mockClear();
185
+ handleInput('\u001B[A'); // UP Arrow from first item
186
+
187
+ expect(console.clear).toHaveBeenCalled();
188
+ });
189
+
190
+ test('doit boucler navigation vers le bas depuis dernière option', () => {
191
+ interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
192
+
193
+ // Navigate to last item
194
+ handleInput('\u001B[B');
195
+ handleInput('\u001B[B');
196
+ console.clear.mockClear();
197
+
198
+ // Try to go down from last item
199
+ handleInput('\u001B[B');
200
+
201
+ expect(console.clear).toHaveBeenCalled();
202
+ });
203
+
204
+ test('doit sélectionner option avec flèche droite', () => {
205
+ interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
206
+
207
+ const initialLogCount = console.log.mock.calls.length;
208
+ handleInput('\u001B[C'); // Right Arrow
209
+
210
+ expect(console.log.mock.calls.length).toBeGreaterThan(initialLogCount);
211
+ expect(console.clear).toHaveBeenCalled();
212
+ });
213
+
214
+ test('doit permettre sélection multiple de la même option', () => {
215
+ interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
216
+
217
+ handleInput('\u001B[C'); // SELECT Option 1
218
+ console.clear.mockClear();
219
+ handleInput('\u001B[C'); // SELECT Option 1 again
220
+
221
+ expect(console.clear).toHaveBeenCalled();
222
+ });
223
+
224
+ test('doit désélectionner option avec flèche gauche', () => {
225
+ interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3'], [0]);
226
+
227
+ const initialLogCount = console.log.mock.calls.length;
228
+ handleInput('\u001B[D'); // Left Arrow
229
+
230
+ expect(console.log.mock.calls.length).toBeGreaterThan(initialLogCount);
231
+ expect(console.clear).toHaveBeenCalled();
232
+ });
233
+
234
+ test('ne doit rien faire si désélection sur option non sélectionnée', () => {
235
+ interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
236
+
237
+ const initialLogCount = console.log.mock.calls.length;
238
+ handleInput('\u001B[D'); // Left Arrow on unselected item
239
+
240
+ expect(console.log.mock.calls.length).toBeGreaterThan(initialLogCount);
241
+ });
242
+
243
+ test('doit quitter avec Ctrl+C', () => {
244
+ // Mock process.exit to throw to simulate actual exit
245
+ const exitMock = jest.spyOn(process, 'exit').mockImplementation((code) => {
246
+ throw new Error('Process exit called');
247
+ });
248
+
249
+ interactiveMenu('Test', ['Option 1', 'Option 2']);
250
+
251
+ expect(() => {
252
+ handleInput('\u0003'); // Ctrl+C
253
+ }).toThrow('Process exit called');
254
+
255
+ expect(exitMock).toHaveBeenCalled();
256
+
257
+ // Restore the mock to the default behavior for other tests
258
+ exitMock.mockRestore();
259
+ jest.spyOn(process, 'exit').mockImplementation(() => {});
260
+ });
261
+
262
+ test('doit appeler process.exit avec Ctrl+C sans throw', () => {
263
+ const exitMock = jest.spyOn(process, 'exit').mockImplementation(() => {});
264
+
265
+ interactiveMenu('Test', ['Option 1', 'Option 2']);
266
+ handleInput('\u0003');
267
+
268
+ expect(exitMock).toHaveBeenCalled();
269
+ });
270
+
271
+ test('doit ignorer une touche inconnue', () => {
272
+ interactiveMenu('Test', ['Option 1', 'Option 2']);
273
+
274
+ const clearCount = console.clear.mock.calls.length;
275
+ handleInput('x');
276
+
277
+ expect(console.clear.mock.calls.length).toBe(clearCount);
278
+ });
279
+ });
280
+
281
+ describe('validation avec Entrée', () => {
282
+ let handleInput;
283
+
284
+ beforeEach(() => {
285
+ mockStdin.on.mockImplementation((event, callback) => {
286
+ if (event === 'data') {
287
+ handleInput = callback;
288
+ }
289
+ });
290
+ });
291
+
292
+ test('doit retourner option courante si aucune sélection', async () => {
293
+ const promise = interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
294
+
295
+ handleInput('\r'); // ENTER
296
+
297
+ const result = await promise;
298
+ expect(result).toEqual(['Option 1']);
299
+ expect(mockStdin.setRawMode).toHaveBeenCalledWith(false);
300
+ expect(mockStdin.pause).toHaveBeenCalled();
301
+ });
302
+
303
+ test('doit retourner options sélectionnées', async () => {
304
+ const promise = interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3'], [1, 2]);
305
+
306
+ handleInput('\r'); // ENTER
307
+
308
+ const result = await promise;
309
+ expect(result).toEqual(['Option 2', 'Option 3']);
310
+ });
311
+
312
+ test('doit nettoyer listeners après validation', async () => {
313
+ const promise = interactiveMenu('Test', ['Option 1']);
314
+
315
+ handleInput('\r'); // ENTER
316
+
317
+ await promise;
318
+ expect(mockStdin.removeListener).toHaveBeenCalledWith('data', handleInput);
319
+ });
320
+
321
+ test('doit retourner sélections après navigation et sélection', async () => {
322
+ const promise = interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
323
+
324
+ handleInput('\u001B[B'); // DOWN to Option 2
325
+ handleInput('\u001B[C'); // SELECT Option 2
326
+ handleInput('\u001B[B'); // DOWN to Option 3
327
+ handleInput('\u001B[C'); // SELECT Option 3
328
+ handleInput('\r'); // ENTER
329
+
330
+ const result = await promise;
331
+ expect(result).toEqual(['Option 2', 'Option 3']);
332
+ });
333
+
334
+ test('doit gérer navigation puis validation sans sélection', async () => {
335
+ const promise = interactiveMenu('Test', ['Option 1', 'Option 2', 'Option 3']);
336
+
337
+ handleInput('\u001B[B'); // DOWN to Option 2
338
+ handleInput('\u001B[B'); // DOWN to Option 3
339
+ handleInput('\r'); // ENTER without selecting
340
+
341
+ const result = await promise;
342
+ expect(result).toEqual(['Option 3']);
343
+ });
344
+ });
345
+
346
+ describe('gestion stdin', () => {
347
+ test('doit configurer stdin en mode raw', () => {
348
+ interactiveMenu('Test', ['Option 1']);
349
+
350
+ expect(mockStdin.setRawMode).toHaveBeenCalledWith(true);
351
+ expect(mockStdin.resume).toHaveBeenCalled();
352
+ expect(mockStdin.setEncoding).toHaveBeenCalledWith('utf8');
353
+ });
354
+
355
+ test('doit enregistrer listener data', () => {
356
+ interactiveMenu('Test', ['Option 1']);
357
+
358
+ expect(mockStdin.on).toHaveBeenCalledWith('data', expect.any(Function));
359
+ });
360
+
361
+ test('doit restaurer stdin après validation', async () => {
362
+ let handleInput;
363
+ mockStdin.on.mockImplementation((event, callback) => {
364
+ if (event === 'data') handleInput = callback;
365
+ });
366
+
367
+ const promise = interactiveMenu('Test', ['Option 1']);
368
+ handleInput('\r');
369
+
370
+ await promise;
371
+
372
+ expect(mockStdin.setRawMode).toHaveBeenCalledWith(false);
373
+ expect(mockStdin.pause).toHaveBeenCalled();
374
+ });
375
+ });
376
+
377
+ describe('scénarios complexes', () => {
378
+ let handleInput;
379
+
380
+ beforeEach(() => {
381
+ mockStdin.on.mockImplementation((event, callback) => {
382
+ if (event === 'data') handleInput = callback;
383
+ });
384
+ });
385
+
386
+ test('doit gérer sélection multiple puis désélection', async () => {
387
+ const promise = interactiveMenu('Test', ['A', 'B', 'C']);
388
+
389
+ handleInput('\u001B[C'); // SELECT A
390
+ handleInput('\u001B[B'); // DOWN to B
391
+ handleInput('\u001B[C'); // SELECT B
392
+ handleInput('\u001B[D'); // DESELECT B
393
+ handleInput('\r'); // ENTER
394
+
395
+ const result = await promise;
396
+ expect(result).toEqual(['A']);
397
+ });
398
+
399
+ test('doit gérer navigation circulaire complète', () => {
400
+ interactiveMenu('Test', ['A', 'B', 'C']);
401
+
402
+ // Navigate through all items and back
403
+ handleInput('\u001B[B'); // A -> B
404
+ handleInput('\u001B[B'); // B -> C
405
+ handleInput('\u001B[B'); // C -> A
406
+ handleInput('\u001B[A'); // A -> C
407
+ handleInput('\u001B[A'); // C -> B
408
+ handleInput('\u001B[A'); // B -> A
409
+
410
+ expect(console.clear).toHaveBeenCalled();
411
+ });
412
+
413
+ test('doit gérer présélections puis modifications', async () => {
414
+ const promise = interactiveMenu('Test', ['A', 'B', 'C'], [0, 1]);
415
+
416
+ handleInput('\u001B[D'); // DESELECT A
417
+ handleInput('\u001B[B'); // DOWN to B
418
+ handleInput('\u001B[B'); // DOWN to C
419
+ handleInput('\u001B[C'); // SELECT C
420
+ handleInput('\r'); // ENTER
421
+
422
+ const result = await promise;
423
+ expect(result).toEqual(['B', 'C']);
424
+ });
425
+ });
426
+ });