gaunt-sloth-assistant 0.0.8 → 0.1.1
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/.gsloth.preamble.internal.md +0 -0
- package/.gsloth.preamble.review.md +0 -0
- package/DEVELOPMENT.md +9 -0
- package/LICENSE +0 -0
- package/README.md +57 -14
- package/RELEASE-HOWTO.md +8 -0
- package/ROADMAP.md +2 -2
- package/UX-RESEARCH.md +78 -0
- package/index.js +12 -74
- package/package.json +9 -8
- package/spec/.gsloth.config.js +22 -0
- package/spec/askCommand.spec.js +58 -0
- package/spec/initCommand.spec.js +54 -0
- package/spec/questionAnsweringModule.spec.js +137 -0
- package/spec/reviewCommand.spec.js +144 -0
- package/spec/{codeReview.spec.js → reviewModule.spec.js} +2 -2
- package/spec/support/jasmine.mjs +0 -0
- package/src/commands/askCommand.js +26 -0
- package/src/commands/initCommand.js +16 -0
- package/src/commands/reviewCommand.js +147 -0
- package/src/config.js +1 -1
- package/src/configs/anthropic.js +0 -0
- package/src/configs/groq.js +0 -0
- package/src/configs/vertexai.js +0 -0
- package/src/consoleUtils.js +2 -0
- package/src/{questionAnswering.js → modules/questionAnsweringModule.js} +87 -68
- package/src/{codeReview.js → modules/reviewModule.js} +4 -6
- package/src/prompt.js +0 -0
- package/src/providers/ghPrDiffProvider.js +11 -0
- package/src/providers/jiraIssueLegacyAccessTokenProvider.js +81 -0
- package/src/providers/text.js +6 -0
- package/src/utils.js +35 -20
- package/testMessage.txt +0 -0
package/.eslint.config.mjs
CHANGED
File without changes
|
package/.github/dependabot.yml
CHANGED
File without changes
|
File without changes
|
File without changes
|
package/DEVELOPMENT.md
ADDED
package/LICENSE
CHANGED
File without changes
|
package/README.md
CHANGED
@@ -1,13 +1,19 @@
|
|
1
1
|
# Gaunt Sloth Assistant
|
2
|
-
Simplistic assistant helping to do code reviews from command line based on [Langchain.js](https://github.com/langchain-ai/langchainjs)
|
2
|
+
Simplistic AI assistant helping to do **code reviews from command line** based on [Langchain.js](https://github.com/langchain-ai/langchainjs)
|
3
|
+
|
4
|
+
## Review PR (Pull Request)
|
5
|
+
To review PR by PR number:
|
6
|
+
|
7
|
+
First make sure the official [GitHub cli (gh)](https://cli.github.com/) is installed
|
8
|
+
and authenticated to have access to your project.
|
9
|
+
|
10
|
+
Open terminal (command line) in your project directory.
|
11
|
+
|
12
|
+
Type command: `gsloth pr [desired pull request number]`, for example:
|
3
13
|
|
4
|
-
## Review PR
|
5
|
-
Review PR by PR number:
|
6
14
|
```shell
|
7
15
|
gsloth pr 42
|
8
16
|
```
|
9
|
-
Official [GitHub cli (gh)](https://cli.github.com/) should be installed
|
10
|
-
and authenticated to have access to your project.
|
11
17
|
|
12
18
|
Review providing markdown file with requirements and notes.
|
13
19
|
```shell
|
@@ -19,6 +25,44 @@ open Jira XML with "Export XML" in jira and to copy `<description></description>
|
|
19
25
|
This block contains HTML and AI understands it easily
|
20
26
|
(most importantly it understand nested lists like ul>li).
|
21
27
|
|
28
|
+
## JIRA Integration
|
29
|
+
|
30
|
+
When JIRA integration is configured, the JIRA issue text can be included alongside the diff for review.
|
31
|
+
The project review preamble can be modified to reject a pull request immediately
|
32
|
+
if it appears to implement something different from what was requested in the requirements.
|
33
|
+
|
34
|
+
The command syntax is generally `gsloth pr <prId> [requirementsId]`,
|
35
|
+
for example, the snippet below does review of PR 42 and
|
36
|
+
supplies description of JIRA issue with number PP-4242:
|
37
|
+
|
38
|
+
```shell
|
39
|
+
gsloth pr 42 PP-4242
|
40
|
+
```
|
41
|
+
|
42
|
+
Example configuration setting up JIRA integration using a legacy API token.
|
43
|
+
Make sure you use your actual company domain in `baseUrl` and your personal legacy `token`.
|
44
|
+
|
45
|
+
A legacy token can be acquired from `Atlassian Account Settings -> Security -> Create and manage API tokens`.
|
46
|
+
|
47
|
+
```javascript
|
48
|
+
export async function configure(importFunction, global) {
|
49
|
+
const vertexAi = await importFunction('@langchain/google-vertexai');
|
50
|
+
return {
|
51
|
+
llm: new vertexAi.ChatVertexAI({
|
52
|
+
model: "gemini-2.5-pro-exp-03-25"
|
53
|
+
}),
|
54
|
+
requirementsProvider: 'jira-legacy',
|
55
|
+
requirementsProviderConfig: {
|
56
|
+
'jira-legacy': {
|
57
|
+
username: 'andrei.kondratev@unimarket.com', // Your Jira username/email
|
58
|
+
token: 'YOURSECRETTOKEN', // Replace with your real Jira API token
|
59
|
+
baseUrl: 'https://yourcompany.atlassian.net/rest/api/2/issue/' // Your Jira instance base URL
|
60
|
+
}
|
61
|
+
}
|
62
|
+
}
|
63
|
+
}
|
64
|
+
```
|
65
|
+
|
22
66
|
## Review any Diff
|
23
67
|
```shell
|
24
68
|
git --no-pager diff origin/master...yourgitcommithash | gsloth review
|
@@ -48,14 +92,6 @@ Tested with Node 22 LTS.
|
|
48
92
|
npm install gaunt-sloth-assistant -g
|
49
93
|
```
|
50
94
|
|
51
|
-
## GitHub (master)
|
52
|
-
|
53
|
-
```shell
|
54
|
-
git clone https://github.com/andruhon/gaunt-sloth.git
|
55
|
-
npm install
|
56
|
-
npm install -g ./
|
57
|
-
```
|
58
|
-
|
59
95
|
## Configuration
|
60
96
|
Go to your project directory and init sloth with vendor of your choice.
|
61
97
|
|
@@ -72,7 +108,14 @@ gcloud auth application-default login
|
|
72
108
|
cd ./your-project
|
73
109
|
gsloth init anthropic
|
74
110
|
```
|
75
|
-
Make sure you edit `.gsloth.config.js` and set up your key.
|
111
|
+
Make sure you either define `ANTHROPIC_API_KEY` environment variable or edit `.gsloth.config.js` and set up your key.
|
112
|
+
|
113
|
+
### Groq
|
114
|
+
```shell
|
115
|
+
cd ./your-project
|
116
|
+
gsloth init groq
|
117
|
+
```
|
118
|
+
Make sure you either define `GROQ_API_KEY` environment variable or edit `.gsloth.config.js` and set up your key.
|
76
119
|
|
77
120
|
### Further configuration
|
78
121
|
|
package/RELEASE-HOWTO.md
CHANGED
@@ -1,11 +1,19 @@
|
|
1
1
|
Make sure `npm config set git-tag-version true`
|
2
2
|
|
3
|
+
For patch, e.g., from 0.0.8 to 0.0.9
|
3
4
|
```shell
|
4
5
|
npm version patch
|
5
6
|
git push
|
6
7
|
git push --tags
|
7
8
|
```
|
8
9
|
|
10
|
+
For minor, e.g., from 0.0.8 to 0.1.0
|
11
|
+
```shell
|
12
|
+
npm version minor
|
13
|
+
git push
|
14
|
+
git push --tags
|
15
|
+
```
|
16
|
+
|
9
17
|
Note the release version and do
|
10
18
|
```shell
|
11
19
|
gh release create --generate-notes
|
package/ROADMAP.md
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
## 1.0.0
|
5
5
|
Doing the following below and making it work stably should be sufficient to call it version 1.
|
6
6
|
|
7
|
-
### Add tests and gain reasonable coverage
|
7
|
+
### ⌛Add tests and gain reasonable coverage
|
8
8
|
### Configure eslint for code quality checks
|
9
9
|
### Automate release process
|
10
10
|
### Add project init command
|
@@ -12,7 +12,7 @@ Add a command to init certain model in certain project, for example `gsloth init
|
|
12
12
|
or `gsloth init` and select one of the provided options.
|
13
13
|
-[x] VertexAI
|
14
14
|
-[x] Anthropic
|
15
|
-
-[
|
15
|
+
-[x] Groq
|
16
16
|
-[ ] Local LLm
|
17
17
|
### Allow global configuration
|
18
18
|
### Streamline and stabilize configuration
|
package/UX-RESEARCH.md
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# UX Research
|
2
|
+
|
3
|
+
## Currently available commands:
|
4
|
+
```
|
5
|
+
gsloth --help
|
6
|
+
Usage: gsloth [options] [command]
|
7
|
+
|
8
|
+
Gaunt Sloth Assistant reviewing your PRs
|
9
|
+
|
10
|
+
Options:
|
11
|
+
-V, --version output the version number
|
12
|
+
-h, --help display help for command
|
13
|
+
|
14
|
+
Commands:
|
15
|
+
init <type> Initialize the Gaunt Sloth Assistant in your project.
|
16
|
+
This will write necessary config files.
|
17
|
+
pr [options] <prNumber> Review a PR in current git directory (assuming that
|
18
|
+
GH cli is installed and authenticated for current
|
19
|
+
project
|
20
|
+
review [options] Review provided diff or other content
|
21
|
+
ask [options] <message> Ask a question
|
22
|
+
help [command] display help for command
|
23
|
+
```
|
24
|
+
|
25
|
+
pr (we decided to rename it to r as shortcut for review with pull request provider)
|
26
|
+
```
|
27
|
+
gsloth pr --help
|
28
|
+
Usage: gsloth pr [options] <prNumber>
|
29
|
+
|
30
|
+
Review a PR in current git directory (assuming that GH cli is installed and
|
31
|
+
authenticated for current project
|
32
|
+
|
33
|
+
Arguments:
|
34
|
+
prNumber PR number to review
|
35
|
+
|
36
|
+
Options:
|
37
|
+
-f, --file <file> Input file. Context of this file will be added BEFORE the
|
38
|
+
diff
|
39
|
+
-h, --help display help for command
|
40
|
+
```
|
41
|
+
|
42
|
+
ask
|
43
|
+
```
|
44
|
+
gsloth ask --help
|
45
|
+
Usage: gsloth ask [options] <message>
|
46
|
+
|
47
|
+
Ask a question
|
48
|
+
|
49
|
+
Arguments:
|
50
|
+
message A message
|
51
|
+
|
52
|
+
Options:
|
53
|
+
-f, --file <file> Input file. Context of this file will be added BEFORE the
|
54
|
+
diff
|
55
|
+
-h, --help display help for command
|
56
|
+
```
|
57
|
+
|
58
|
+
review (lacks documentaion, it also accepts pipe with stdin)
|
59
|
+
```
|
60
|
+
gsloth review --help
|
61
|
+
Usage: gsloth review [options]
|
62
|
+
|
63
|
+
Review provided diff or other content
|
64
|
+
|
65
|
+
Options:
|
66
|
+
-f, --file <file> Input file. Context of this file will be added BEFORE the
|
67
|
+
diff
|
68
|
+
-h, --help display help for command
|
69
|
+
```
|
70
|
+
|
71
|
+
## Future functions
|
72
|
+
|
73
|
+
- JIRA. We need to privide a jira number as a criteria, this should somehow go through separate provider and using config from .gsloth.config.
|
74
|
+
- External links (simple public links)
|
75
|
+
- Editing files
|
76
|
+
- Improve experience with specs or criteria (or requirements?)
|
77
|
+
- Should we allow to mention that the data is diff or plain code? Maybe we can somehow deduct it or ask smaller model to guess?
|
78
|
+
- Slot editing local files
|
package/index.js
CHANGED
@@ -1,16 +1,12 @@
|
|
1
1
|
#!/usr/bin/env node
|
2
|
-
import {
|
3
|
-
import {dirname} from 'node:path';
|
4
|
-
import {
|
5
|
-
import {
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
} from "./src/config.js";
|
11
|
-
import {fileURLToPath} from "url";
|
12
|
-
import {getSlothVersion, readFileFromCurrentDir, readStdin} from "./src/utils.js";
|
13
|
-
import {getPrDiff, readInternalPreamble, readPreamble} from "./src/prompt.js";
|
2
|
+
import { Command } from 'commander';
|
3
|
+
import { dirname } from 'node:path';
|
4
|
+
import { fileURLToPath } from "url";
|
5
|
+
import { reviewCommand } from "./src/commands/reviewCommand.js";
|
6
|
+
import { initCommand } from "./src/commands/initCommand.js";
|
7
|
+
import { askCommand } from "./src/commands/askCommand.js";
|
8
|
+
import { slothContext } from "./src/config.js";
|
9
|
+
import { getSlothVersion, readStdin } from "./src/utils.js";
|
14
10
|
|
15
11
|
const program = new Command();
|
16
12
|
|
@@ -22,70 +18,12 @@ program
|
|
22
18
|
.description('Gaunt Sloth Assistant reviewing your PRs')
|
23
19
|
.version(getSlothVersion());
|
24
20
|
|
25
|
-
program
|
26
|
-
.description('Initialize the Gaunt Sloth Assistant in your project. This will write necessary config files.')
|
27
|
-
.addArgument(new Argument('<type>', 'Config type').choices(availableDefaultConfigs))
|
28
|
-
.action(async (config) => {
|
29
|
-
await createProjectConfig(config);
|
30
|
-
});
|
21
|
+
initCommand(program, slothContext);
|
31
22
|
|
32
|
-
program
|
33
|
-
.description('Review a PR in current git directory (assuming that GH cli is installed and authenticated for current project')
|
34
|
-
.argument('<prNumber>', 'PR number to review')
|
35
|
-
.option('-f, --file <file>', 'Input file. Context of this file will be added BEFORE the diff')
|
36
|
-
// TODO add option consuming extra message as argument
|
37
|
-
.action(async (pr, options) => {
|
38
|
-
if (slothContext.stdin) {
|
39
|
-
displayError('`gsloth pr` does not expect stdin, use `gsloth review` instead');
|
40
|
-
return;
|
41
|
-
}
|
42
|
-
displayInfo(`Starting review of PR ${pr}`);
|
43
|
-
const diff = await getPrDiff(pr);
|
44
|
-
const preamble = [readInternalPreamble(), readPreamble(USER_PROJECT_REVIEW_PREAMBLE)];
|
45
|
-
const content = [diff];
|
46
|
-
if (options.file) {
|
47
|
-
content.push(readFileFromCurrentDir(options.file));
|
48
|
-
}
|
49
|
-
const { review } = await import('./src/codeReview.js');
|
50
|
-
await review('sloth-PR-review-'+pr, preamble.join("\n"), content.join("\n"));
|
51
|
-
});
|
23
|
+
reviewCommand(program, slothContext)
|
52
24
|
|
53
|
-
program
|
54
|
-
.description('Review provided diff or other content')
|
55
|
-
.option('-f, --file <file>', 'Input file. Context of this file will be added BEFORE the diff')
|
56
|
-
// TODO add option consuming extra message as argument
|
57
|
-
.action(async (options) => {
|
58
|
-
if (!slothContext.stdin && !options.file) {
|
59
|
-
displayError('gsloth review expects stdin with github diff stdin or a file');
|
60
|
-
return
|
61
|
-
}
|
62
|
-
const preamble = [readInternalPreamble(), readPreamble(USER_PROJECT_REVIEW_PREAMBLE)];
|
63
|
-
const content = [];
|
64
|
-
if (slothContext.stdin) {
|
65
|
-
content.push(slothContext.stdin);
|
66
|
-
}
|
67
|
-
if (options.file) {
|
68
|
-
content.push(readFileFromCurrentDir(options.file));
|
69
|
-
}
|
70
|
-
const { review } = await import('./src/codeReview.js');
|
71
|
-
await review('sloth-DIFF-review', preamble.join("\n"), content.join("\n"));
|
72
|
-
});
|
73
|
-
|
74
|
-
program.command('ask')
|
75
|
-
.description('Ask a question')
|
76
|
-
.argument('<message>', 'A message')
|
77
|
-
.option('-f, --file <file>', 'Input file. Context of this file will be added BEFORE the diff')
|
78
|
-
// TODO add option consuming extra message as argument
|
79
|
-
.action(async (message, options) => {
|
80
|
-
const preamble = [readInternalPreamble()];
|
81
|
-
const content = [message];
|
82
|
-
if (options.file) {
|
83
|
-
content.push(readFileFromCurrentDir(options.file));
|
84
|
-
}
|
85
|
-
const { askQuestion } = await import('./src/questionAnswering.js');
|
86
|
-
await askQuestion('sloth-ASK', preamble.join("\n"), content.join("\n"));
|
87
|
-
});
|
25
|
+
askCommand(program, slothContext);
|
88
26
|
|
89
27
|
// TODO add general interactive chat command
|
90
28
|
|
91
|
-
readStdin(program);
|
29
|
+
await readStdin(program);
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "gaunt-sloth-assistant",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.1.1",
|
4
4
|
"description": "",
|
5
5
|
"license": "MIT",
|
6
6
|
"author": "Andrew Kondratev",
|
@@ -15,15 +15,16 @@
|
|
15
15
|
"test": "jasmine"
|
16
16
|
},
|
17
17
|
"bin": {
|
18
|
-
"gsloth": "index.js"
|
18
|
+
"gsloth": "index.js",
|
19
|
+
"gth": "index.js"
|
19
20
|
},
|
20
21
|
"dependencies": {
|
21
|
-
"@eslint/js": "^9.
|
22
|
-
"@langchain/anthropic": "^0.3.
|
23
|
-
"@langchain/core": "^0.3.
|
24
|
-
"@langchain/google-vertexai": "^0.2.
|
22
|
+
"@eslint/js": "^9.25.0",
|
23
|
+
"@langchain/anthropic": "^0.3.18",
|
24
|
+
"@langchain/core": "^0.3.45",
|
25
|
+
"@langchain/google-vertexai": "^0.2.4",
|
25
26
|
"@langchain/groq": "^0.2.2",
|
26
|
-
"@langchain/langgraph": "^0.2.
|
27
|
+
"@langchain/langgraph": "^0.2.65",
|
27
28
|
"@types/node": "^22.14.1",
|
28
29
|
"chalk": "^5.4.1",
|
29
30
|
"commander": "^13.1.0",
|
@@ -31,6 +32,6 @@
|
|
31
32
|
},
|
32
33
|
"devDependencies": {
|
33
34
|
"jasmine": "^5.6.0",
|
34
|
-
"
|
35
|
+
"testdouble": "^3.20.2"
|
35
36
|
}
|
36
37
|
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
export async function configure(importFunction, global) {
|
2
|
+
const test = await importFunction('@langchain/core/utils/testing');
|
3
|
+
return {
|
4
|
+
llm: new test.FakeListChatModel({
|
5
|
+
responses: ["First LLM message", "Second LLM message"],
|
6
|
+
}),
|
7
|
+
requirementsProviderConfig: {
|
8
|
+
'jira-legacy': {
|
9
|
+
username: 'user.name@company.com', // Your Jira username/email
|
10
|
+
token: 'YoUrToKeN', // Replace with your real Jira API token
|
11
|
+
baseUrl: 'https://company.atlassian.net/rest/api/2/issue/' // Your Jira instance base URL
|
12
|
+
}
|
13
|
+
},
|
14
|
+
requirementsProvider: "jira-legacy",
|
15
|
+
contentProvider: "somethingSpecial",
|
16
|
+
contentProviderConfig: {
|
17
|
+
somethingSpecial: {
|
18
|
+
test: 'example'
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import {Command} from 'commander';
|
2
|
+
import * as td from 'testdouble';
|
3
|
+
|
4
|
+
describe('askCommand', function (){
|
5
|
+
|
6
|
+
beforeEach(async function() {
|
7
|
+
this.askQuestion = td.function();
|
8
|
+
this.prompt = await td.replaceEsm("../src/prompt.js");
|
9
|
+
td.when(this.prompt.readInternalPreamble()).thenReturn("INTERNAL PREAMBLE");
|
10
|
+
this.questionAnsweringMock = await td.replaceEsm("../src/modules/questionAnsweringModule.js");
|
11
|
+
await td.replaceEsm("../src/config.js");
|
12
|
+
this.utils = await td.replaceEsm("../src/utils.js");
|
13
|
+
td.when(this.utils.readFileFromCurrentDir("test.file")).thenReturn("FILE CONTENT");
|
14
|
+
td.when(this.questionAnsweringMock.askQuestion(
|
15
|
+
'sloth-ASK',
|
16
|
+
td.matchers.anything(),
|
17
|
+
td.matchers.anything())
|
18
|
+
).thenDo(this.askQuestion);
|
19
|
+
});
|
20
|
+
|
21
|
+
it('Should call askQuestion with message', async function() {
|
22
|
+
const { askCommand } = await import("../src/commands/askCommand.js");
|
23
|
+
const program = new Command();
|
24
|
+
await askCommand(program, {});
|
25
|
+
await program.parseAsync(['na', 'na', 'ask', 'test message']);
|
26
|
+
td.verify(this.askQuestion('sloth-ASK', "INTERNAL PREAMBLE", "test message"));
|
27
|
+
});
|
28
|
+
|
29
|
+
it('Should call askQuestion with message and file content', async function() {
|
30
|
+
const { askCommand } = await import("../src/commands/askCommand.js");
|
31
|
+
const program = new Command();
|
32
|
+
await askCommand(program, {});
|
33
|
+
await program.parseAsync(['na', 'na', 'ask', 'test message', '-f', 'test.file']);
|
34
|
+
td.verify(this.askQuestion('sloth-ASK', "INTERNAL PREAMBLE", "test message\nFILE CONTENT"));
|
35
|
+
});
|
36
|
+
|
37
|
+
it('Should display help correctly', async function() {
|
38
|
+
const { askCommand } = await import("../src/commands/askCommand.js");
|
39
|
+
const program = new Command();
|
40
|
+
const testOutput = { text: '' };
|
41
|
+
|
42
|
+
program.configureOutput({
|
43
|
+
writeOut: (str) => testOutput.text += str,
|
44
|
+
writeErr: (str) => testOutput.text += str
|
45
|
+
});
|
46
|
+
|
47
|
+
await askCommand(program, {});
|
48
|
+
|
49
|
+
const commandUnderTest = program.commands.find(c => c.name() == 'ask');
|
50
|
+
expect(commandUnderTest).toBeDefined();
|
51
|
+
commandUnderTest.outputHelp();
|
52
|
+
|
53
|
+
// Verify help content
|
54
|
+
expect(testOutput.text).toContain('Ask a question');
|
55
|
+
expect(testOutput.text).toContain('<message>');
|
56
|
+
expect(testOutput.text).toContain('-f, --file');
|
57
|
+
});
|
58
|
+
});
|
@@ -0,0 +1,54 @@
|
|
1
|
+
import {Command} from 'commander';
|
2
|
+
import * as td from 'testdouble';
|
3
|
+
|
4
|
+
describe('initCommand', function (){
|
5
|
+
|
6
|
+
beforeEach(async function() {
|
7
|
+
// Create a mock for createProjectConfig
|
8
|
+
this.createProjectConfig = td.function();
|
9
|
+
|
10
|
+
// Replace the config module
|
11
|
+
await td.replaceEsm("../src/config.js", {
|
12
|
+
createProjectConfig: this.createProjectConfig,
|
13
|
+
availableDefaultConfigs: ['vertexai', 'anthropic', 'groq'],
|
14
|
+
SLOTH_INTERNAL_PREAMBLE: '.gsloth.preamble.internal.md',
|
15
|
+
USER_PROJECT_REVIEW_PREAMBLE: '.gsloth.preamble.review.md',
|
16
|
+
USER_PROJECT_CONFIG_FILE: '.gsloth.config.js',
|
17
|
+
slothContext: {
|
18
|
+
installDir: '/mock/install/dir',
|
19
|
+
currentDir: '/mock/current/dir',
|
20
|
+
config: {},
|
21
|
+
session: {}
|
22
|
+
}
|
23
|
+
});
|
24
|
+
});
|
25
|
+
|
26
|
+
it('Should call createProjectConfig with the provided config type', async function() {
|
27
|
+
const { initCommand } = await import("../src/commands/initCommand.js");
|
28
|
+
const program = new Command();
|
29
|
+
await initCommand(program, {});
|
30
|
+
await program.parseAsync(['na', 'na', 'init', 'vertexai']);
|
31
|
+
td.verify(this.createProjectConfig('vertexai'));
|
32
|
+
});
|
33
|
+
|
34
|
+
it('Should display available config types in help', async function() {
|
35
|
+
const { initCommand } = await import("../src/commands/initCommand.js");
|
36
|
+
const program = new Command();
|
37
|
+
const testOutput = { text: '' };
|
38
|
+
|
39
|
+
program.configureOutput({
|
40
|
+
writeOut: (str) => testOutput.text += str,
|
41
|
+
writeErr: (str) => testOutput.text += str
|
42
|
+
});
|
43
|
+
|
44
|
+
await initCommand(program, {});
|
45
|
+
|
46
|
+
const commandUnderTest = program.commands.find(c => c.name() == 'init');
|
47
|
+
expect(commandUnderTest).toBeDefined();
|
48
|
+
commandUnderTest.outputHelp();
|
49
|
+
|
50
|
+
// Verify available config types are displayed
|
51
|
+
expect(testOutput.text).toContain('<type>');
|
52
|
+
expect(testOutput.text).toContain('(choices: "vertexai", "anthropic", "groq")');
|
53
|
+
});
|
54
|
+
});
|
@@ -0,0 +1,137 @@
|
|
1
|
+
import * as td from 'testdouble';
|
2
|
+
|
3
|
+
describe('questionAnsweringModule', function (){
|
4
|
+
|
5
|
+
beforeEach(async function() {
|
6
|
+
// Reset testdouble before each test
|
7
|
+
td.reset();
|
8
|
+
|
9
|
+
// Create a mock context
|
10
|
+
this.mockLlmInvoke = td.function();
|
11
|
+
this.context = {
|
12
|
+
config: {
|
13
|
+
llm: {
|
14
|
+
invoke: this.mockLlmInvoke
|
15
|
+
}
|
16
|
+
},
|
17
|
+
session: {configurable: {thread_id: 'test-thread-id'}}
|
18
|
+
};
|
19
|
+
|
20
|
+
// Create fs mock
|
21
|
+
this.fs = {
|
22
|
+
writeFileSync: td.function()
|
23
|
+
};
|
24
|
+
|
25
|
+
// Create path mock
|
26
|
+
this.path = {
|
27
|
+
resolve: td.function()
|
28
|
+
};
|
29
|
+
|
30
|
+
// Create consoleUtils mock
|
31
|
+
this.consoleUtils = {
|
32
|
+
display: td.function(),
|
33
|
+
displaySuccess: td.function(),
|
34
|
+
displayError: td.function()
|
35
|
+
};
|
36
|
+
|
37
|
+
// Create utils mock functions
|
38
|
+
const extractLastMessageContent = td.function();
|
39
|
+
const toFileSafeString = td.function();
|
40
|
+
const fileSafeLocalDate = td.function();
|
41
|
+
const ProgressIndicator = td.constructor();
|
42
|
+
const readFileSyncWithMessages = td.function();
|
43
|
+
const spawnCommand = td.function();
|
44
|
+
|
45
|
+
// Set up utils mock stubs
|
46
|
+
td.when(extractLastMessageContent(td.matchers.anything())).thenReturn('LLM Response');
|
47
|
+
td.when(toFileSafeString(td.matchers.anything())).thenReturn('sloth-ASK');
|
48
|
+
td.when(fileSafeLocalDate()).thenReturn('2025-01-01T00-00-00');
|
49
|
+
|
50
|
+
// Create the utils mock
|
51
|
+
this.utils = {
|
52
|
+
extractLastMessageContent,
|
53
|
+
toFileSafeString,
|
54
|
+
fileSafeLocalDate,
|
55
|
+
ProgressIndicator,
|
56
|
+
readFileSyncWithMessages,
|
57
|
+
spawnCommand
|
58
|
+
};
|
59
|
+
|
60
|
+
// Set up path.resolve mock
|
61
|
+
td.when(this.path.resolve(td.matchers.anything(), td.matchers.contains('sloth-ASK'))).thenReturn('test-file-path.md');
|
62
|
+
|
63
|
+
// Mock ProgressIndicator
|
64
|
+
this.progressIndicator = {
|
65
|
+
indicate: td.function()
|
66
|
+
};
|
67
|
+
td.when(new this.utils.ProgressIndicator(td.matchers.anything())).thenReturn(this.progressIndicator);
|
68
|
+
|
69
|
+
// Replace modules with mocks - do this after setting up all mocks
|
70
|
+
await td.replaceEsm("node:fs", this.fs);
|
71
|
+
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);
|
74
|
+
|
75
|
+
// Mock slothContext and other config exports
|
76
|
+
await td.replaceEsm("../src/config.js", {
|
77
|
+
slothContext: this.context,
|
78
|
+
SLOTH_INTERNAL_PREAMBLE: '.gsloth.preamble.internal.md',
|
79
|
+
USER_PROJECT_REVIEW_PREAMBLE: '.gsloth.preamble.review.md',
|
80
|
+
USER_PROJECT_CONFIG_FILE: '.gsloth.config.js',
|
81
|
+
initConfig: td.function()
|
82
|
+
});
|
83
|
+
});
|
84
|
+
|
85
|
+
it('Should call LLM with correct messages', async function() {
|
86
|
+
// Mock the LLM response
|
87
|
+
const llmResponse = [{ role: 'assistant', content: 'LLM Response' }];
|
88
|
+
td.when(this.mockLlmInvoke(td.matchers.anything())).thenResolve(llmResponse);
|
89
|
+
|
90
|
+
// Import the module after setting up mocks
|
91
|
+
const { askQuestionInner } = await import("../src/modules/questionAnsweringModule.js");
|
92
|
+
|
93
|
+
// Call the function
|
94
|
+
const result = await askQuestionInner(this.context, () => {}, 'Test Preamble', 'Test Content');
|
95
|
+
|
96
|
+
// Verify the result
|
97
|
+
expect(result).toBe('LLM Response');
|
98
|
+
});
|
99
|
+
|
100
|
+
it('Should write output to file', async function() {
|
101
|
+
// Mock the LLM response
|
102
|
+
const llmResponse = [{ role: 'assistant', content: 'LLM Response' }];
|
103
|
+
td.when(this.mockLlmInvoke(td.matchers.anything())).thenResolve(llmResponse);
|
104
|
+
|
105
|
+
// Import the module after setting up mocks
|
106
|
+
const { askQuestion } = await import("../src/modules/questionAnsweringModule.js");
|
107
|
+
|
108
|
+
// Call the function and wait for it to complete
|
109
|
+
await askQuestion('sloth-ASK', 'Test Preamble', 'Test Content');
|
110
|
+
|
111
|
+
// Verify the file was written with the correct content
|
112
|
+
td.verify(this.fs.writeFileSync('test-file-path.md', 'LLM Response'));
|
113
|
+
|
114
|
+
// Verify success message was displayed
|
115
|
+
td.verify(this.consoleUtils.displaySuccess(td.matchers.contains('test-file-path.md')));
|
116
|
+
});
|
117
|
+
|
118
|
+
it('Should handle file write errors', async function() {
|
119
|
+
// Mock the LLM response
|
120
|
+
const llmResponse = [{ role: 'assistant', content: 'LLM Response' }];
|
121
|
+
td.when(this.mockLlmInvoke(td.matchers.anything())).thenResolve(llmResponse);
|
122
|
+
|
123
|
+
// Mock file write to throw an error
|
124
|
+
const error = new Error('File write error');
|
125
|
+
td.when(this.fs.writeFileSync('test-file-path.md', 'LLM Response')).thenThrow(error);
|
126
|
+
|
127
|
+
// Import the module after setting up mocks
|
128
|
+
const { askQuestion } = await import("../src/modules/questionAnsweringModule.js");
|
129
|
+
|
130
|
+
// Call the function and wait for it to complete
|
131
|
+
await askQuestion('sloth-ASK', 'Test Preamble', 'Test Content');
|
132
|
+
|
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'));
|
136
|
+
});
|
137
|
+
});
|