gaunt-sloth-assistant 0.1.2 → 0.1.4
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/.github/workflows/ci.yml +33 -0
- package/README.md +59 -33
- package/ROADMAP.md +20 -20
- package/UX-RESEARCH.md +1 -1
- package/docs/CONFIGURATION.md +96 -6
- package/docs/DEVELOPMENT.md +12 -0
- package/eslint.config.js +38 -0
- package/index.js +4 -2
- package/package.json +7 -3
- package/spec/.gsloth.config.js +1 -1
- package/spec/.gsloth.config.json +25 -0
- package/spec/askCommand.spec.js +39 -5
- package/spec/config.spec.js +421 -0
- package/spec/initCommand.spec.js +3 -2
- package/spec/predefinedConfigs.spec.js +100 -0
- package/spec/questionAnsweringModule.spec.js +14 -14
- package/spec/reviewCommand.spec.js +86 -8
- package/spec/reviewModule.spec.js +7 -1
- package/src/commands/askCommand.js +5 -4
- package/src/commands/initCommand.js +2 -1
- package/src/commands/reviewCommand.js +19 -12
- package/src/config.js +139 -25
- package/src/configs/anthropic.js +28 -5
- package/src/configs/fake.js +15 -0
- package/src/configs/groq.js +27 -4
- package/src/configs/vertexai.js +23 -2
- package/src/consoleUtils.js +15 -5
- package/src/modules/questionAnsweringModule.js +9 -14
- package/src/modules/reviewModule.js +11 -16
- package/src/prompt.js +4 -3
- package/src/providers/file.js +19 -0
- package/src/providers/ghPrDiffProvider.js +2 -2
- package/src/providers/jiraIssueLegacyAccessTokenProvider.js +33 -30
- package/src/providers/text.js +1 -1
- package/src/systemUtils.js +32 -0
- package/src/utils.js +49 -13
@@ -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
|
+
});
|
package/spec/initCommand.spec.js
CHANGED
@@ -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()
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
73
|
-
await td.replaceEsm("../src/utils.js", this.
|
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.
|
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.
|
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.
|
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.
|
135
|
-
td.verify(this.
|
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
|
});
|