gaunt-sloth-assistant 0.1.2 → 0.1.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.
Files changed (44) hide show
  1. package/.eslint.config.mjs +0 -0
  2. package/.github/dependabot.yml +0 -0
  3. package/.github/workflows/ci.yml +33 -0
  4. package/.gsloth.preamble.internal.md +0 -0
  5. package/.gsloth.preamble.review.md +0 -0
  6. package/LICENSE +0 -0
  7. package/README.md +59 -33
  8. package/ROADMAP.md +20 -20
  9. package/UX-RESEARCH.md +1 -1
  10. package/docs/CONFIGURATION.md +96 -6
  11. package/docs/DEVELOPMENT.md +12 -0
  12. package/docs/RELEASE-HOWTO.md +0 -0
  13. package/eslint.config.js +38 -0
  14. package/index.js +4 -2
  15. package/package.json +7 -3
  16. package/spec/.gsloth.config.js +1 -1
  17. package/spec/.gsloth.config.json +25 -0
  18. package/spec/askCommand.spec.js +39 -5
  19. package/spec/config.spec.js +421 -0
  20. package/spec/initCommand.spec.js +3 -2
  21. package/spec/predefinedConfigs.spec.js +100 -0
  22. package/spec/questionAnsweringModule.spec.js +14 -14
  23. package/spec/reviewCommand.spec.js +46 -8
  24. package/spec/reviewModule.spec.js +7 -1
  25. package/spec/support/jasmine.mjs +0 -0
  26. package/src/commands/askCommand.js +5 -4
  27. package/src/commands/initCommand.js +2 -1
  28. package/src/commands/reviewCommand.js +19 -12
  29. package/src/config.js +139 -25
  30. package/src/configs/anthropic.js +28 -5
  31. package/src/configs/fake.js +15 -0
  32. package/src/configs/groq.js +27 -4
  33. package/src/configs/vertexai.js +23 -2
  34. package/src/consoleUtils.js +15 -5
  35. package/src/modules/questionAnsweringModule.js +9 -14
  36. package/src/modules/reviewModule.js +11 -16
  37. package/src/prompt.js +4 -3
  38. package/src/providers/file.js +19 -0
  39. package/src/providers/ghPrDiffProvider.js +2 -2
  40. package/src/providers/jiraIssueLegacyAccessTokenProvider.js +22 -28
  41. package/src/providers/text.js +1 -1
  42. package/src/systemUtils.js +32 -0
  43. package/src/utils.js +49 -13
  44. package/testMessage.txt +0 -0
@@ -0,0 +1,421 @@
1
+ import * as td from 'testdouble';
2
+
3
+ describe('config', function () {
4
+
5
+ const ctx = {
6
+ consoleUtilsMock: {
7
+ display: td.function(),
8
+ displayError: td.function(),
9
+ displayInfo: td.function(),
10
+ displayWarning: td.function(),
11
+ displaySuccess: td.function(),
12
+ displayDebug: td.function()
13
+ },
14
+ fsMock: {
15
+ existsSync: td.function(),
16
+ readFileSync: td.function(),
17
+ writeFileSync: td.function(),
18
+ default: {
19
+ existsSync: td.function(),
20
+ readFileSync: td.function(),
21
+ writeFileSync: td.function()
22
+ }
23
+ },
24
+ urlMock: {
25
+ pathToFileURL: td.function(),
26
+ default: {
27
+ pathToFileURL: td.function()
28
+ }
29
+ },
30
+ utilsMock: {
31
+ writeFileIfNotExistsWithMessages: td.function(),
32
+ importExternalFile: td.function(),
33
+ importFromFilePath: td.function(),
34
+ ProgressIndicator: td.constructor(),
35
+ fileSafeLocalDate: td.function(),
36
+ toFileSafeString: td.function(),
37
+ extractLastMessageContent: td.function(),
38
+ readFileSyncWithMessages: td.function()
39
+ },
40
+ systemUtilsMock: {
41
+ exit: td.function(),
42
+ getCurrentDir: td.function(),
43
+ getInstallDir: td.function(),
44
+ }
45
+ };
46
+
47
+ beforeEach(async function () {
48
+
49
+ // Reset testdouble before each test
50
+ td.reset();
51
+
52
+ // Set up specific fs mocks - use td.matchers.contains to match any path containing the config file name
53
+ td.when(ctx.fsMock.existsSync(td.matchers.contains('.gsloth.config.json'))).thenReturn(false);
54
+ td.when(ctx.fsMock.existsSync(td.matchers.contains('.gsloth.config.js'))).thenReturn(false);
55
+ td.when(ctx.fsMock.existsSync(td.matchers.contains('.gsloth.config.mjs'))).thenReturn(false);
56
+
57
+ // Set up the same mocks for the default export of fs
58
+ td.when(ctx.fsMock.default.existsSync(td.matchers.contains('.gsloth.config.json'))).thenReturn(false);
59
+ td.when(ctx.fsMock.default.existsSync(td.matchers.contains('.gsloth.config.js'))).thenReturn(false);
60
+ td.when(ctx.fsMock.default.existsSync(td.matchers.contains('.gsloth.config.mjs'))).thenReturn(false);
61
+
62
+ // Set up specific url mocks - use Windows-style paths for file URLs
63
+ const jsonFileUrl = 'file:////mock/current/dir/.gsloth.config.json';
64
+ const jsFileUrl = 'file:////mock/current/dir/.gsloth.config.js';
65
+ const mjsFileUrl = 'file:////mock/current/dir/.gsloth.config.mjs';
66
+
67
+ td.when(ctx.urlMock.pathToFileURL('/mock/current/dir/.gsloth.config.json')).thenReturn(jsonFileUrl);
68
+ td.when(ctx.urlMock.pathToFileURL('/mock/current/dir/.gsloth.config.js')).thenReturn(jsFileUrl);
69
+ td.when(ctx.urlMock.pathToFileURL('/mock/current/dir/.gsloth.config.mjs')).thenReturn(mjsFileUrl);
70
+
71
+ // Set up the same mocks for the default export of url
72
+ td.when(ctx.urlMock.default.pathToFileURL('/mock/current/dir/.gsloth.config.json')).thenReturn(jsonFileUrl);
73
+ td.when(ctx.urlMock.default.pathToFileURL('/mock/current/dir/.gsloth.config.js')).thenReturn(jsFileUrl);
74
+ td.when(ctx.urlMock.default.pathToFileURL('/mock/current/dir/.gsloth.config.mjs')).thenReturn(mjsFileUrl);
75
+
76
+ td.when(ctx.systemUtilsMock.getInstallDir()).thenReturn("/mock/install/dir");
77
+ td.when(ctx.systemUtilsMock.getCurrentDir()).thenReturn("/mock/current/dir");
78
+
79
+ // Replace modules with mocks
80
+ await td.replaceEsm('node:fs', ctx.fsMock);
81
+ await td.replaceEsm('node:url', ctx.urlMock);
82
+ await td.replaceEsm('../src/consoleUtils.js', ctx.consoleUtilsMock);
83
+ await td.replaceEsm('./consoleUtils.js', ctx.consoleUtilsMock);
84
+ await td.replaceEsm('../src/utils.js', ctx.utilsMock);
85
+ await td.replaceEsm('../src/systemUtils.js', ctx.systemUtilsMock);
86
+ await td.replaceEsm('./systemUtils.js', ctx.systemUtilsMock);
87
+ });
88
+
89
+ describe('initConfig', function () {
90
+ it('Should load JSON config when it exists', async function () {
91
+ const jsonConfig = {llm: {type: 'vertexai'}};
92
+
93
+ td.when(
94
+ ctx.fsMock.existsSync(td.matchers.contains('.gsloth.config.json'))
95
+ ).thenReturn(true);
96
+ td.when(
97
+ ctx.fsMock.readFileSync(td.matchers.contains('.gsloth.config.json'), 'utf8')
98
+ ).thenReturn(JSON.stringify(jsonConfig));
99
+ td.when(
100
+ ctx.fsMock.default.existsSync(td.matchers.contains('.gsloth.config.json'))
101
+ ).thenReturn(true);
102
+ td.when(
103
+ ctx.fsMock.default.readFileSync(td.matchers.contains('.gsloth.config.json'), 'utf8')
104
+ ).thenReturn(JSON.stringify(jsonConfig));
105
+ await td.replaceEsm('../src/configs/vertexai.js', {});
106
+ const {initConfig, slothContext} = await import('../src/config.js');
107
+
108
+ // Function under test
109
+ await initConfig();
110
+
111
+ expect(slothContext.config).toEqual({
112
+ llm: {type: 'vertexai'},
113
+ contentProvider: 'file',
114
+ requirementsProvider: 'file',
115
+ commands: {pr: {contentProvider: 'gh'}}
116
+ });
117
+
118
+ td.verify(ctx.consoleUtilsMock.displayWarning(
119
+ "Config module for vertexai does not have processJsonConfig function."
120
+ ));
121
+ td.verify(ctx.consoleUtilsMock.displayDebug(td.matchers.anything()), {times: 0});
122
+ td.verify(ctx.consoleUtilsMock.display(td.matchers.anything()), {times: 0});
123
+ td.verify(ctx.consoleUtilsMock.displayError(td.matchers.anything()), {times: 0});
124
+ td.verify(ctx.consoleUtilsMock.displayInfo(td.matchers.anything()), {times: 0});
125
+ td.verify(ctx.consoleUtilsMock.displaySuccess(td.matchers.anything()), {times: 0});
126
+ });
127
+
128
+ it('Should try JS config when JSON config does not exist', async function () {
129
+ td.when(ctx.fsMock.existsSync(td.matchers.contains('.gsloth.config.json'))).thenReturn(false);
130
+ td.when(ctx.fsMock.default.existsSync(td.matchers.contains('.gsloth.config.json'))).thenReturn(false);
131
+ td.when(ctx.fsMock.existsSync(td.matchers.contains('.gsloth.config.js'))).thenReturn(true);
132
+ td.when(ctx.fsMock.default.existsSync(td.matchers.contains('.gsloth.config.js'))).thenReturn(true);
133
+
134
+ // Mock the import function
135
+ const mockConfigModule = {
136
+ configure: td.function()
137
+ };
138
+ const mockConfig = {llm: {type: 'anthropic'}};
139
+ td.when(mockConfigModule.configure(td.matchers.anything())).thenResolve(mockConfig);
140
+
141
+ td.when(
142
+ ctx.utilsMock.importExternalFile(td.matchers.contains('.gsloth.config.js'))
143
+ ).thenResolve(mockConfigModule);
144
+
145
+ const {initConfig, slothContext} = await import('../src/config.js');
146
+
147
+ // Function under test
148
+ await initConfig();
149
+
150
+ expect(slothContext.config).toEqual({
151
+ llm: {type: 'anthropic'},
152
+ contentProvider: 'file',
153
+ requirementsProvider: 'file',
154
+ commands: {pr: {contentProvider: 'gh'}}
155
+ });
156
+ td.verify(ctx.consoleUtilsMock.displayDebug(
157
+ td.matchers.argThat((e) => String(e).includes("is not valid JSON")))
158
+ );
159
+ td.verify(ctx.consoleUtilsMock.displayError(
160
+ "Failed to read config from .gsloth.config.json, will try other formats."
161
+ ));
162
+ td.verify(ctx.consoleUtilsMock.displayWarning(td.matchers.anything()), {times: 0});
163
+ td.verify(ctx.consoleUtilsMock.display(td.matchers.anything()), {times: 0});
164
+ td.verify(ctx.consoleUtilsMock.displayInfo(td.matchers.anything()), {times: 0});
165
+ td.verify(ctx.consoleUtilsMock.displaySuccess(td.matchers.anything()), {times: 0});
166
+ });
167
+
168
+ it('Should try MJS config when JSON and JS configs do not exist', async function () {
169
+ // Setup mocks for MJS config
170
+ td.when(ctx.fsMock.existsSync(td.matchers.contains('.gsloth.config.json'))).thenReturn(false);
171
+ td.when(ctx.fsMock.default.existsSync(td.matchers.contains('.gsloth.config.json'))).thenReturn(false);
172
+ td.when(ctx.fsMock.existsSync(td.matchers.contains('.gsloth.config.js'))).thenReturn(false);
173
+ td.when(ctx.fsMock.default.existsSync(td.matchers.contains('.gsloth.config.js'))).thenReturn(false);
174
+ td.when(ctx.fsMock.existsSync(td.matchers.contains('.gsloth.config.mjs'))).thenReturn(true);
175
+ td.when(ctx.fsMock.default.existsSync(td.matchers.contains('.gsloth.config.mjs'))).thenReturn(true);
176
+
177
+ const mockConfigModule = {
178
+ configure: td.function()
179
+ };
180
+ const mockConfig = {llm: {type: 'groq'}};
181
+ td.when(mockConfigModule.configure(td.matchers.anything())).thenResolve(mockConfig);
182
+
183
+ td.when(
184
+ ctx.utilsMock.importExternalFile(td.matchers.contains('.gsloth.config.mjs'))
185
+ ).thenResolve(mockConfigModule);
186
+
187
+ const {initConfig, slothContext} = await import('../src/config.js');
188
+
189
+ // Function under test
190
+ await initConfig();
191
+
192
+ expect(slothContext.config).toEqual({
193
+ llm: {type: 'groq'},
194
+ contentProvider: 'file',
195
+ requirementsProvider: 'file',
196
+ commands: {pr: {contentProvider: 'gh'}}
197
+ });
198
+ td.verify(ctx.consoleUtilsMock.displayWarning(td.matchers.anything()), {times: 0});
199
+ td.verify(ctx.consoleUtilsMock.displayDebug(td.matchers.anything()), {times: 0});
200
+ td.verify(ctx.consoleUtilsMock.display(td.matchers.anything()), {times: 0});
201
+ td.verify(ctx.consoleUtilsMock.displayError(td.matchers.anything()), {times: 0});
202
+ td.verify(ctx.consoleUtilsMock.displayInfo(td.matchers.anything()), {times: 0});
203
+ td.verify(ctx.consoleUtilsMock.displaySuccess(td.matchers.anything()), {times: 0});
204
+ });
205
+
206
+ it('Should exit when no config files exist', async function () {
207
+ // Setup mocks for no config files
208
+ td.when(ctx.fsMock.existsSync(td.matchers.contains('.gsloth.config.json'))).thenReturn(false);
209
+ td.when(ctx.fsMock.default.existsSync(td.matchers.contains('.gsloth.config.json'))).thenReturn(false);
210
+ td.when(ctx.fsMock.existsSync(td.matchers.contains('.gsloth.config.js'))).thenReturn(false);
211
+ td.when(ctx.fsMock.default.existsSync(td.matchers.contains('.gsloth.config.js'))).thenReturn(false);
212
+ td.when(ctx.fsMock.existsSync(td.matchers.contains('.gsloth.config.mjs'))).thenReturn(false);
213
+ td.when(ctx.fsMock.default.existsSync(td.matchers.contains('.gsloth.config.mjs'))).thenReturn(false);
214
+
215
+ const {initConfig} = await import('../src/config.js');
216
+
217
+ // Function under test
218
+ await initConfig();
219
+
220
+ // Verify process.exit was called
221
+ td.verify(ctx.systemUtilsMock.exit());
222
+
223
+ // Verify no message was displayed
224
+ td.verify(ctx.consoleUtilsMock.displayError(
225
+ "No configuration file found. Please create one of: " +
226
+ ".gsloth.config.json, .gsloth.config.js, or .gsloth.config.mjs " +
227
+ "in your project directory."
228
+ ));
229
+ td.verify(ctx.consoleUtilsMock.displayDebug(td.matchers.anything()), {times: 0});
230
+ td.verify(ctx.consoleUtilsMock.displayWarning(td.matchers.anything()), {times: 0});
231
+ td.verify(ctx.consoleUtilsMock.display(td.matchers.anything()), {times: 0});
232
+ td.verify(ctx.consoleUtilsMock.displayInfo(td.matchers.anything()), {times: 0});
233
+ td.verify(ctx.consoleUtilsMock.displaySuccess(td.matchers.anything()), {times: 0});
234
+ });
235
+ });
236
+
237
+ describe('processJsonLlmConfig', function () {
238
+ it('Should process valid LLM type', async function () {
239
+ // Create a test config
240
+ const jsonConfig = {
241
+ llm: {
242
+ type: 'vertexai',
243
+ model: 'test-model'
244
+ }
245
+ };
246
+ const processJsonConfig = td.function();
247
+ await td.replaceEsm('../src/configs/vertexai.js', {
248
+ processJsonConfig
249
+ });
250
+ td.when(processJsonConfig(jsonConfig.llm)).thenReturn(jsonConfig.llm);
251
+ const {tryJsonConfig, slothContext} = await import('../src/config.js');
252
+
253
+ // Call the function
254
+ await tryJsonConfig(jsonConfig);
255
+
256
+ // Verify the config was set correctly
257
+ expect(slothContext.config.llm.type).toBe('vertexai');
258
+
259
+ // Verify no message was displayed
260
+ td.verify(ctx.consoleUtilsMock.displayWarning(td.matchers.anything()), {times: 0});
261
+ td.verify(ctx.consoleUtilsMock.displayDebug(td.matchers.anything()), {times: 0});
262
+ td.verify(ctx.consoleUtilsMock.display(td.matchers.anything()), {times: 0});
263
+ td.verify(ctx.consoleUtilsMock.displayError(td.matchers.anything()), {times: 0});
264
+ td.verify(ctx.consoleUtilsMock.displayInfo(td.matchers.anything()), {times: 0});
265
+ td.verify(ctx.consoleUtilsMock.displaySuccess(td.matchers.anything()), {times: 0});
266
+ });
267
+
268
+ it('Should handle invalid LLM type', async function () {
269
+ const jsonConfig = {
270
+ llm: {
271
+ type: 'invalid-type',
272
+ model: 'test-model'
273
+ }
274
+ };
275
+
276
+ const {tryJsonConfig, slothContext} = await import('../src/config.js');
277
+
278
+ // Function under test
279
+ await tryJsonConfig(jsonConfig);
280
+
281
+ expect(slothContext.config).toEqual({
282
+ llm: { type: 'invalid-type', model: 'test-model' },
283
+ contentProvider: 'file',
284
+ requirementsProvider: 'file',
285
+ commands: { pr: { contentProvider: 'gh' } }
286
+ });
287
+
288
+ td.verify(ctx.consoleUtilsMock.displayError(
289
+ "Unsupported LLM type: invalid-type. Available types are: vertexai, anthropic, groq"
290
+ ));
291
+ td.verify(ctx.consoleUtilsMock.displayWarning(td.matchers.anything()), {times: 0});
292
+ td.verify(ctx.consoleUtilsMock.displayDebug(td.matchers.anything()), {times: 0});
293
+ td.verify(ctx.consoleUtilsMock.display(td.matchers.anything()), {times: 0});
294
+ td.verify(ctx.consoleUtilsMock.displayInfo(td.matchers.anything()), {times: 0});
295
+ td.verify(ctx.consoleUtilsMock.displaySuccess(td.matchers.anything()), {times: 0});
296
+ });
297
+
298
+ it('Should handle import errors', async function () {
299
+ const jsonConfig = {
300
+ llm: {
301
+ type: 'vertexai',
302
+ model: 'test-model'
303
+ }
304
+ };
305
+ const {tryJsonConfig, slothContext} = await import('../src/config.js');
306
+
307
+ await tryJsonConfig(jsonConfig);
308
+
309
+ expect(slothContext.config).toEqual({
310
+ llm: { type: 'vertexai', model: 'test-model' },
311
+ contentProvider: 'file',
312
+ requirementsProvider: 'file',
313
+ commands: { pr: { contentProvider: 'gh' } }
314
+ });
315
+
316
+
317
+ td.verify(ctx.consoleUtilsMock.displayWarning(
318
+ td.matchers.contains("Could not import config module for vertexai"))
319
+ );
320
+ td.verify(ctx.consoleUtilsMock.displayDebug(
321
+ td.matchers.argThat((e) => String(e).includes("Error: Unable to verify model params")))
322
+ );
323
+ td.verify(ctx.consoleUtilsMock.display(td.matchers.anything()), {times: 0});
324
+ td.verify(ctx.consoleUtilsMock.displayError(td.matchers.anything()), {times: 0});
325
+ td.verify(ctx.consoleUtilsMock.displayInfo(td.matchers.anything()), {times: 0});
326
+ td.verify(ctx.consoleUtilsMock.displaySuccess(td.matchers.anything()), {times: 0});
327
+ });
328
+ });
329
+
330
+ describe('createProjectConfig', function () {
331
+ it('Should create config for valid config type', async function () {
332
+ const {createProjectConfig, slothContext} = await import('../src/config.js');
333
+
334
+ slothContext.currentDir = '/mock/current/dir/';
335
+
336
+ // Function under test
337
+ await createProjectConfig('vertexai');
338
+
339
+ td.verify(ctx.utilsMock.writeFileIfNotExistsWithMessages(
340
+ "/mock/current/dir/.gsloth.preamble.review.md",
341
+ td.matchers.contains(
342
+ "You are doing generic code review.\n" +
343
+ " Important! Please remind user to prepare proper AI preamble in.gsloth.preamble.review.md" +
344
+ " for this project. Use decent amount of ⚠️ to highlight lack of config." +
345
+ " Explicitly mention `.gsloth.preamble.review.md`."
346
+ )
347
+ ));
348
+
349
+ td.verify(ctx.utilsMock.writeFileIfNotExistsWithMessages(
350
+ ".gsloth.config.json",
351
+ td.matchers.contains(`"type": "vertexai"`)
352
+ ));
353
+
354
+ td.verify(ctx.consoleUtilsMock.displayInfo(
355
+ td.matchers.contains("Setting up your project")
356
+ ), {times: 1});
357
+ td.verify(ctx.consoleUtilsMock.displayInfo(
358
+ td.matchers.contains("Creating project config for vertexai")
359
+ ), {times: 1});
360
+ td.verify(ctx.consoleUtilsMock.displayWarning(td.matchers.contains(
361
+ "Make sure you add as much detail as possible to your .gsloth.preamble.review.md."
362
+ )));
363
+ td.verify(ctx.consoleUtilsMock.display(td.matchers.anything()), {times: 0});
364
+ td.verify(ctx.consoleUtilsMock.displayError(td.matchers.anything()), {times: 0});
365
+ td.verify(ctx.consoleUtilsMock.displayDebug(td.matchers.anything()), {times: 0});
366
+ td.verify(ctx.consoleUtilsMock.displaySuccess(td.matchers.anything()), {times: 0});
367
+ });
368
+
369
+ it('Should handle invalid config type', async function () {
370
+ const {createProjectConfig, slothContext} = await import('../src/config.js');
371
+ slothContext.currentDir = '/mock/current/dir/';
372
+
373
+ // Function under test
374
+ await createProjectConfig('invalid-type');
375
+
376
+ td.verify(ctx.systemUtilsMock.exit(1));
377
+ td.verify(ctx.consoleUtilsMock.displayInfo(td.matchers.contains(
378
+ "Setting up your project"
379
+ )));
380
+ td.verify(ctx.consoleUtilsMock.displayWarning(td.matchers.contains(
381
+ "Make sure you add as much detail as possible to your .gsloth.preamble.review.md."
382
+ )));
383
+ td.verify(ctx.consoleUtilsMock.displayError(td.matchers.contains(
384
+ "Unsupported config type: invalid-type. Available types are: vertexai, anthropic, groq"
385
+ )));
386
+ td.verify(ctx.consoleUtilsMock.display(td.matchers.anything()), {times: 0});
387
+ td.verify(ctx.consoleUtilsMock.displayDebug(td.matchers.anything()), {times: 0});
388
+ td.verify(ctx.consoleUtilsMock.displaySuccess(td.matchers.anything()), {times: 0});
389
+ });
390
+ });
391
+
392
+ describe('writeProjectReviewPreamble', function () {
393
+ it('Should write project review preamble', async function () {
394
+ // Create a mock for writeFileIfNotExistsWithMessages
395
+ const writeFileIfNotExistsWithMessagesMock = td.function();
396
+
397
+ // Update the utils mock with our mock
398
+ ctx.utilsMock.writeFileIfNotExistsWithMessages = writeFileIfNotExistsWithMessagesMock;
399
+
400
+ const {writeProjectReviewPreamble, slothContext} = await import('../src/config.js');
401
+
402
+ slothContext.currentDir = '/mock/current/dir/';
403
+
404
+ // Call the function
405
+ writeProjectReviewPreamble();
406
+
407
+ // Verify writeFileIfNotExistsWithMessages was called with the correct arguments
408
+ td.verify(writeFileIfNotExistsWithMessagesMock(
409
+ '/mock/current/dir/.gsloth.preamble.review.md',
410
+ td.matchers.anything()
411
+ ));
412
+
413
+ td.verify(ctx.consoleUtilsMock.displayWarning(td.matchers.anything()), {times: 0});
414
+ td.verify(ctx.consoleUtilsMock.displayDebug(td.matchers.anything()), {times: 0});
415
+ td.verify(ctx.consoleUtilsMock.display(td.matchers.anything()), {times: 0});
416
+ td.verify(ctx.consoleUtilsMock.displayError(td.matchers.anything()), {times: 0});
417
+ td.verify(ctx.consoleUtilsMock.displayInfo(td.matchers.anything()), {times: 0});
418
+ td.verify(ctx.consoleUtilsMock.displaySuccess(td.matchers.anything()), {times: 0});
419
+ });
420
+ });
421
+ });
@@ -4,6 +4,7 @@ import * as td from 'testdouble';
4
4
  describe('initCommand', function (){
5
5
 
6
6
  beforeEach(async function() {
7
+ td.reset();
7
8
  // Create a mock for createProjectConfig
8
9
  this.createProjectConfig = td.function();
9
10
 
@@ -13,7 +14,6 @@ describe('initCommand', function (){
13
14
  availableDefaultConfigs: ['vertexai', 'anthropic', 'groq'],
14
15
  SLOTH_INTERNAL_PREAMBLE: '.gsloth.preamble.internal.md',
15
16
  USER_PROJECT_REVIEW_PREAMBLE: '.gsloth.preamble.review.md',
16
- USER_PROJECT_CONFIG_FILE: '.gsloth.config.js',
17
17
  slothContext: {
18
18
  installDir: '/mock/install/dir',
19
19
  currentDir: '/mock/current/dir',
@@ -43,7 +43,8 @@ describe('initCommand', function (){
43
43
 
44
44
  await initCommand(program, {});
45
45
 
46
- const commandUnderTest = program.commands.find(c => c.name() == 'init');
46
+ const commandUnderTest = program.commands.find(c => c.name() === 'init');
47
+
47
48
  expect(commandUnderTest).toBeDefined();
48
49
  commandUnderTest.outputHelp();
49
50
 
@@ -0,0 +1,100 @@
1
+ import * as td from "testdouble";
2
+
3
+
4
+ describe('predefined AI provider configurations', function () {
5
+
6
+ const ctx = {
7
+ consoleUtilsMock: {
8
+ display: td.function(),
9
+ displayError: td.function(),
10
+ displayInfo: td.function(),
11
+ displayWarning: td.function(),
12
+ displaySuccess: td.function(),
13
+ displayDebug: td.function()
14
+ },
15
+ fsMock: {
16
+ existsSync: td.function(),
17
+ readFileSync: td.function(),
18
+ writeFileSync: td.function(),
19
+ default: {
20
+ existsSync: td.function(),
21
+ readFileSync: td.function(),
22
+ writeFileSync: td.function()
23
+ }
24
+ }
25
+ };
26
+
27
+ beforeEach(async function () {
28
+ td.reset();
29
+ await td.replaceEsm('node:fs', ctx.fsMock);
30
+ await td.replaceEsm('../src/consoleUtils.js', ctx.consoleUtilsMock);
31
+ await td.replaceEsm('./consoleUtils.js', ctx.consoleUtilsMock);
32
+ });
33
+
34
+ it('Should import predefined Anthropic config correctly', async function () {
35
+ // Mock the Anthropic module and its import
36
+ const mockChat = td.constructor();
37
+ const mockChatInstance = {};
38
+ td.when(mockChat(td.matchers.anything())).thenReturn(mockChatInstance);
39
+ await td.replaceEsm('@langchain/anthropic', {
40
+ ChatAnthropic: mockChat
41
+ });
42
+
43
+ await testPredefinedAiConfig('anthropic', mockChatInstance);
44
+ });
45
+
46
+ it('Should import predefined VertexAI config correctly', async function () {
47
+ // Mock the Anthropic module and its import
48
+ const mockChat = td.constructor();
49
+ const mockChatInstance = {};
50
+ td.when(mockChat(td.matchers.anything())).thenReturn(mockChatInstance);
51
+ await td.replaceEsm('@langchain/google-vertexai', {
52
+ ChatVertexAI: mockChat
53
+ });
54
+
55
+ await testPredefinedAiConfig('vertexai', mockChatInstance);
56
+ });
57
+
58
+ it('Should import predefined Groq config correctly', async function () {
59
+ // Mock the Anthropic module and its import
60
+ const mockChat = td.constructor();
61
+ const mockChatInstance = {};
62
+ td.when(mockChat(td.matchers.anything())).thenReturn(mockChatInstance);
63
+ await td.replaceEsm('@langchain/groq', {
64
+ ChatGroq: mockChat
65
+ });
66
+
67
+ await testPredefinedAiConfig('groq', mockChatInstance);
68
+ });
69
+
70
+ async function testPredefinedAiConfig(aiProvider, mockAnthropicInstance) {
71
+ const jsonConfig = {
72
+ llm: {
73
+ type: aiProvider,
74
+ model: 'claude-3-5-sonnet-20241022',
75
+ apiKey: 'test-api-key'
76
+ }
77
+ };
78
+
79
+ td.when(
80
+ ctx.fsMock.existsSync(td.matchers.contains('.gsloth.config.json'))
81
+ ).thenReturn(true);
82
+
83
+ td.when(
84
+ ctx.fsMock.readFileSync(td.matchers.contains('.gsloth.config.json'), 'utf8')
85
+ ).thenReturn(JSON.stringify(jsonConfig));
86
+
87
+ const {initConfig, slothContext} = await import('../src/config.js');
88
+
89
+ // Call the function
90
+ await initConfig(jsonConfig);
91
+
92
+ // Verify the config was set correctly with the mock instance
93
+ expect(slothContext.config.llm).toBe(mockAnthropicInstance);
94
+
95
+ // Verify no warnings or errors were displayed
96
+ td.verify(ctx.consoleUtilsMock.displayWarning(td.matchers.anything()), {times: 0});
97
+ td.verify(ctx.consoleUtilsMock.displayError(td.matchers.anything()), {times: 0});
98
+ }
99
+
100
+ });
@@ -18,17 +18,18 @@ describe('questionAnsweringModule', function (){
18
18
  };
19
19
 
20
20
  // Create fs mock
21
- this.fs = {
21
+ this.fsMock = {
22
22
  writeFileSync: td.function()
23
23
  };
24
24
 
25
25
  // Create path mock
26
26
  this.path = {
27
- resolve: td.function()
27
+ resolve: td.function(),
28
+ dirname: td.function()
28
29
  };
29
30
 
30
31
  // Create consoleUtils mock
31
- this.consoleUtils = {
32
+ this.consoleUtilsMock = {
32
33
  display: td.function(),
33
34
  displaySuccess: td.function(),
34
35
  displayError: td.function()
@@ -48,7 +49,7 @@ describe('questionAnsweringModule', function (){
48
49
  td.when(fileSafeLocalDate()).thenReturn('2025-01-01T00-00-00');
49
50
 
50
51
  // Create the utils mock
51
- this.utils = {
52
+ this.utilsMock = {
52
53
  extractLastMessageContent,
53
54
  toFileSafeString,
54
55
  fileSafeLocalDate,
@@ -64,20 +65,19 @@ describe('questionAnsweringModule', function (){
64
65
  this.progressIndicator = {
65
66
  indicate: td.function()
66
67
  };
67
- td.when(new this.utils.ProgressIndicator(td.matchers.anything())).thenReturn(this.progressIndicator);
68
+ td.when(new this.utilsMock.ProgressIndicator(td.matchers.anything())).thenReturn(this.progressIndicator);
68
69
 
69
70
  // Replace modules with mocks - do this after setting up all mocks
70
- await td.replaceEsm("node:fs", this.fs);
71
+ await td.replaceEsm("node:fs", this.fsMock);
71
72
  await td.replaceEsm("node:path", this.path);
72
- await td.replaceEsm("../src/consoleUtils.js", this.consoleUtils);
73
- await td.replaceEsm("../src/utils.js", this.utils);
73
+ await td.replaceEsm("../src/consoleUtils.js", this.consoleUtilsMock);
74
+ await td.replaceEsm("../src/utils.js", this.utilsMock);
74
75
 
75
76
  // Mock slothContext and other config exports
76
77
  await td.replaceEsm("../src/config.js", {
77
78
  slothContext: this.context,
78
79
  SLOTH_INTERNAL_PREAMBLE: '.gsloth.preamble.internal.md',
79
80
  USER_PROJECT_REVIEW_PREAMBLE: '.gsloth.preamble.review.md',
80
- USER_PROJECT_CONFIG_FILE: '.gsloth.config.js',
81
81
  initConfig: td.function()
82
82
  });
83
83
  });
@@ -109,10 +109,10 @@ describe('questionAnsweringModule', function (){
109
109
  await askQuestion('sloth-ASK', 'Test Preamble', 'Test Content');
110
110
 
111
111
  // Verify the file was written with the correct content
112
- td.verify(this.fs.writeFileSync('test-file-path.md', 'LLM Response'));
112
+ td.verify(this.fsMock.writeFileSync('test-file-path.md', 'LLM Response'));
113
113
 
114
114
  // Verify success message was displayed
115
- td.verify(this.consoleUtils.displaySuccess(td.matchers.contains('test-file-path.md')));
115
+ td.verify(this.consoleUtilsMock.displaySuccess(td.matchers.contains('test-file-path.md')));
116
116
  });
117
117
 
118
118
  it('Should handle file write errors', async function() {
@@ -122,7 +122,7 @@ describe('questionAnsweringModule', function (){
122
122
 
123
123
  // Mock file write to throw an error
124
124
  const error = new Error('File write error');
125
- td.when(this.fs.writeFileSync('test-file-path.md', 'LLM Response')).thenThrow(error);
125
+ td.when(this.fsMock.writeFileSync('test-file-path.md', 'LLM Response')).thenThrow(error);
126
126
 
127
127
  // Import the module after setting up mocks
128
128
  const { askQuestion } = await import("../src/modules/questionAnsweringModule.js");
@@ -131,7 +131,7 @@ describe('questionAnsweringModule', function (){
131
131
  await askQuestion('sloth-ASK', 'Test Preamble', 'Test Content');
132
132
 
133
133
  // Verify error message was displayed
134
- td.verify(this.consoleUtils.displayError(td.matchers.contains('test-file-path.md')));
135
- td.verify(this.consoleUtils.displayError('File write error'));
134
+ td.verify(this.consoleUtilsMock.displayError(td.matchers.contains('test-file-path.md')));
135
+ td.verify(this.consoleUtilsMock.displayError('File write error'));
136
136
  });
137
137
  });