gaunt-sloth-assistant 0.1.1 → 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.
- package/.eslint.config.mjs +0 -0
- package/.github/dependabot.yml +0 -0
- package/.github/workflows/ci.yml +33 -0
- package/.gsloth.preamble.internal.md +0 -0
- package/.gsloth.preamble.review.md +1 -0
- package/LICENSE +0 -0
- package/README.md +66 -103
- package/ROADMAP.md +20 -20
- package/UX-RESEARCH.md +1 -1
- package/docs/CONFIGURATION.md +190 -0
- package/docs/DEVELOPMENT.md +21 -0
- package/{RELEASE-HOWTO.md → docs/RELEASE-HOWTO.md} +0 -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 +46 -8
- package/spec/reviewModule.spec.js +7 -1
- package/spec/support/jasmine.mjs +0 -0
- 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 +22 -28
- package/src/providers/text.js +1 -1
- package/src/systemUtils.js +32 -0
- package/src/utils.js +49 -13
- package/testMessage.txt +0 -0
- package/DEVELOPMENT.md +0 -9
@@ -0,0 +1,25 @@
|
|
1
|
+
{
|
2
|
+
"llm": {
|
3
|
+
"type": "fake",
|
4
|
+
"responses": ["First LLM message", "Second LLM message"]
|
5
|
+
},
|
6
|
+
"requirementsProviderConfig": {
|
7
|
+
"jira-legacy": {
|
8
|
+
"username": "user.name@company.com",
|
9
|
+
"token": "YoUrToKeN",
|
10
|
+
"baseUrl": "https://company.atlassian.net/rest/api/2/issue/"
|
11
|
+
}
|
12
|
+
},
|
13
|
+
"requirementsProvider": "file",
|
14
|
+
"contentProvider": "somethingSpecial",
|
15
|
+
"contentProviderConfig": {
|
16
|
+
"somethingSpecial": {
|
17
|
+
"test": "example"
|
18
|
+
}
|
19
|
+
},
|
20
|
+
"commands": {
|
21
|
+
"pr": {
|
22
|
+
"requirementsProvider": "jira-legacy"
|
23
|
+
}
|
24
|
+
}
|
25
|
+
}
|
package/spec/askCommand.spec.js
CHANGED
@@ -4,13 +4,36 @@ import * as td from 'testdouble';
|
|
4
4
|
describe('askCommand', function (){
|
5
5
|
|
6
6
|
beforeEach(async function() {
|
7
|
+
td.reset();
|
7
8
|
this.askQuestion = td.function();
|
8
9
|
this.prompt = await td.replaceEsm("../src/prompt.js");
|
9
10
|
td.when(this.prompt.readInternalPreamble()).thenReturn("INTERNAL PREAMBLE");
|
10
11
|
this.questionAnsweringMock = await td.replaceEsm("../src/modules/questionAnsweringModule.js");
|
11
|
-
await td.replaceEsm("../src/config.js"
|
12
|
-
|
13
|
-
|
12
|
+
await td.replaceEsm("../src/config.js", {
|
13
|
+
SLOTH_INTERNAL_PREAMBLE: '.gsloth.preamble.internal.md',
|
14
|
+
USER_PROJECT_REVIEW_PREAMBLE: '.gsloth.preamble.review.md',
|
15
|
+
slothContext: {
|
16
|
+
config: {},
|
17
|
+
currentDir: '/mock/current/dir'
|
18
|
+
},
|
19
|
+
initConfig: td.function()
|
20
|
+
});
|
21
|
+
const readFileFromCurrentDir = td.function();
|
22
|
+
const readMultipleFilesFromCurrentDir = td.function();
|
23
|
+
const extractLastMessageContent = td.function();
|
24
|
+
const toFileSafeString = td.function();
|
25
|
+
const fileSafeLocalDate = td.function();
|
26
|
+
this.utilsMock = {
|
27
|
+
readFileFromCurrentDir,
|
28
|
+
readMultipleFilesFromCurrentDir,
|
29
|
+
ProgressIndicator: td.constructor(),
|
30
|
+
extractLastMessageContent,
|
31
|
+
toFileSafeString,
|
32
|
+
fileSafeLocalDate
|
33
|
+
};
|
34
|
+
await td.replaceEsm("../src/utils.js", this.utilsMock);
|
35
|
+
td.when(this.utilsMock.readFileFromCurrentDir("test.file")).thenReturn("FILE CONTENT");
|
36
|
+
td.when(this.utilsMock.readMultipleFilesFromCurrentDir(["test.file"])).thenReturn("test.file:\n```\nFILE CONTENT\n```");
|
14
37
|
td.when(this.questionAnsweringMock.askQuestion(
|
15
38
|
'sloth-ASK',
|
16
39
|
td.matchers.anything(),
|
@@ -31,7 +54,17 @@ describe('askCommand', function (){
|
|
31
54
|
const program = new Command();
|
32
55
|
await askCommand(program, {});
|
33
56
|
await program.parseAsync(['na', 'na', 'ask', 'test message', '-f', 'test.file']);
|
34
|
-
td.verify(this.askQuestion('sloth-ASK', "INTERNAL PREAMBLE", "test message\nFILE CONTENT"));
|
57
|
+
td.verify(this.askQuestion('sloth-ASK', "INTERNAL PREAMBLE", "test message\ntest.file:\n```\nFILE CONTENT\n```"));
|
58
|
+
});
|
59
|
+
|
60
|
+
it('Should call askQuestion with message and multiple file contents', async function() {
|
61
|
+
const { askCommand } = await import("../src/commands/askCommand.js");
|
62
|
+
const program = new Command();
|
63
|
+
await askCommand(program, {});
|
64
|
+
td.when(this.utilsMock.readMultipleFilesFromCurrentDir(["test.file", "test2.file"]))
|
65
|
+
.thenReturn("test.file:\n```\nFILE CONTENT\n```\n\ntest2.file:\n```\nFILE2 CONTENT\n```");
|
66
|
+
await program.parseAsync(['na', 'na', 'ask', 'test message', '-f', 'test.file', 'test2.file']);
|
67
|
+
td.verify(this.askQuestion('sloth-ASK', "INTERNAL PREAMBLE", "test message\ntest.file:\n```\nFILE CONTENT\n```\n\ntest2.file:\n```\nFILE2 CONTENT\n```"));
|
35
68
|
});
|
36
69
|
|
37
70
|
it('Should display help correctly', async function() {
|
@@ -47,6 +80,7 @@ describe('askCommand', function (){
|
|
47
80
|
await askCommand(program, {});
|
48
81
|
|
49
82
|
const commandUnderTest = program.commands.find(c => c.name() == 'ask');
|
83
|
+
|
50
84
|
expect(commandUnderTest).toBeDefined();
|
51
85
|
commandUnderTest.outputHelp();
|
52
86
|
|
@@ -55,4 +89,4 @@ describe('askCommand', function (){
|
|
55
89
|
expect(testOutput.text).toContain('<message>');
|
56
90
|
expect(testOutput.text).toContain('-f, --file');
|
57
91
|
});
|
58
|
-
});
|
92
|
+
});
|
@@ -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
|
+
});
|