jira-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +364 -0
- package/dist/cli.js +87 -0
- package/dist/commands/about.js +83 -0
- package/dist/commands/add-comment.js +109 -0
- package/dist/commands/me.js +23 -0
- package/dist/commands/project-statuses.js +23 -0
- package/dist/commands/projects.js +35 -0
- package/dist/commands/run-jql.js +44 -0
- package/dist/commands/task-with-details.js +23 -0
- package/dist/commands/update-description.js +109 -0
- package/dist/lib/formatters.js +193 -0
- package/dist/lib/jira-client.js +216 -0
- package/dist/lib/settings.js +68 -0
- package/dist/lib/utils.js +79 -0
- package/jest.config.js +21 -0
- package/package.json +47 -0
- package/settings.yaml +24 -0
- package/src/cli.ts +97 -0
- package/src/commands/about.ts +98 -0
- package/src/commands/add-comment.ts +94 -0
- package/src/commands/me.ts +18 -0
- package/src/commands/project-statuses.ts +18 -0
- package/src/commands/projects.ts +32 -0
- package/src/commands/run-jql.ts +40 -0
- package/src/commands/task-with-details.ts +18 -0
- package/src/commands/update-description.ts +94 -0
- package/src/lib/formatters.ts +224 -0
- package/src/lib/jira-client.ts +319 -0
- package/src/lib/settings.ts +77 -0
- package/src/lib/utils.ts +76 -0
- package/src/types/md-to-adf.d.ts +14 -0
- package/tests/README.md +97 -0
- package/tests/__mocks__/jira.js.ts +4 -0
- package/tests/__mocks__/md-to-adf.ts +7 -0
- package/tests/__mocks__/mdast-util-from-adf.ts +4 -0
- package/tests/__mocks__/mdast-util-to-markdown.ts +1 -0
- package/tests/add-comment.test.ts +226 -0
- package/tests/cli-permissions.test.ts +156 -0
- package/tests/jira-client.test.ts +123 -0
- package/tests/projects.test.ts +205 -0
- package/tests/settings.test.ts +288 -0
- package/tests/task-with-details.test.ts +83 -0
- package/tests/update-description.test.ts +262 -0
- package/to-do.md +9 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { updateDescriptionCommand } from '../src/commands/update-description';
|
|
2
|
+
import * as jiraClient from '../src/lib/jira-client';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import mdToAdf from 'md-to-adf';
|
|
6
|
+
|
|
7
|
+
// Mock dependencies
|
|
8
|
+
jest.mock('fs');
|
|
9
|
+
jest.mock('md-to-adf');
|
|
10
|
+
jest.mock('../src/lib/jira-client');
|
|
11
|
+
jest.mock('../src/lib/utils');
|
|
12
|
+
jest.mock('ora', () => {
|
|
13
|
+
return jest.fn(() => ({
|
|
14
|
+
start: jest.fn().mockReturnThis(),
|
|
15
|
+
succeed: jest.fn().mockReturnThis(),
|
|
16
|
+
fail: jest.fn().mockReturnThis()
|
|
17
|
+
}));
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const mockJiraClient = jiraClient as jest.Mocked<typeof jiraClient>;
|
|
21
|
+
const mockFs = fs as jest.Mocked<typeof fs>;
|
|
22
|
+
const mockMdToAdf = mdToAdf as jest.MockedFunction<typeof mdToAdf>;
|
|
23
|
+
|
|
24
|
+
describe('Update Description Command', () => {
|
|
25
|
+
const mockTaskId = 'TEST-123';
|
|
26
|
+
const mockFilePath = '/path/to/description.md';
|
|
27
|
+
const mockMarkdownContent = '# Test Description\n\nThis is a test description.';
|
|
28
|
+
const mockAdfContent = {
|
|
29
|
+
version: 1,
|
|
30
|
+
type: 'doc',
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: 'heading',
|
|
34
|
+
attrs: { level: 1 },
|
|
35
|
+
content: [{ type: 'text', text: 'Test Description' }]
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
jest.clearAllMocks();
|
|
42
|
+
console.log = jest.fn();
|
|
43
|
+
console.error = jest.fn();
|
|
44
|
+
|
|
45
|
+
// Setup default mocks
|
|
46
|
+
jest.spyOn(mockFs, 'existsSync').mockReturnValue(true);
|
|
47
|
+
jest.spyOn(mockFs, 'readFileSync').mockReturnValue(mockMarkdownContent);
|
|
48
|
+
mockMdToAdf.mockReturnValue(mockAdfContent);
|
|
49
|
+
mockJiraClient.updateIssueDescription = jest.fn().mockResolvedValue(undefined);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should successfully update issue description', async () => {
|
|
53
|
+
await updateDescriptionCommand(mockTaskId, { fromFile: mockFilePath });
|
|
54
|
+
|
|
55
|
+
expect(mockFs.existsSync).toHaveBeenCalledWith(path.resolve(mockFilePath));
|
|
56
|
+
expect(mockFs.readFileSync).toHaveBeenCalledWith(path.resolve(mockFilePath), 'utf-8');
|
|
57
|
+
expect(mockMdToAdf).toHaveBeenCalledWith(mockMarkdownContent);
|
|
58
|
+
expect(mockJiraClient.updateIssueDescription).toHaveBeenCalledWith(
|
|
59
|
+
mockTaskId,
|
|
60
|
+
mockAdfContent
|
|
61
|
+
);
|
|
62
|
+
expect(console.log).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should exit with error when task ID is empty', async () => {
|
|
66
|
+
const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
67
|
+
throw new Error('Process exit');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await expect(updateDescriptionCommand('', { fromFile: mockFilePath })).rejects.toThrow(
|
|
71
|
+
'Process exit'
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
75
|
+
expect.stringContaining('Task ID cannot be empty')
|
|
76
|
+
);
|
|
77
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
78
|
+
|
|
79
|
+
processExitSpy.mockRestore();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should exit with error when file does not exist', async () => {
|
|
83
|
+
jest.spyOn(mockFs, 'existsSync').mockReturnValue(false);
|
|
84
|
+
const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
85
|
+
throw new Error('Process exit');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await expect(
|
|
89
|
+
updateDescriptionCommand(mockTaskId, { fromFile: mockFilePath })
|
|
90
|
+
).rejects.toThrow('Process exit');
|
|
91
|
+
|
|
92
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
93
|
+
expect.stringContaining('File not found')
|
|
94
|
+
);
|
|
95
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
96
|
+
|
|
97
|
+
processExitSpy.mockRestore();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should exit with error when file read fails', async () => {
|
|
101
|
+
const readError = new Error('Permission denied');
|
|
102
|
+
jest.spyOn(mockFs, 'readFileSync').mockImplementation(() => {
|
|
103
|
+
throw readError;
|
|
104
|
+
});
|
|
105
|
+
const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
106
|
+
throw new Error('Process exit');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await expect(
|
|
110
|
+
updateDescriptionCommand(mockTaskId, { fromFile: mockFilePath })
|
|
111
|
+
).rejects.toThrow('Process exit');
|
|
112
|
+
|
|
113
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
114
|
+
expect.stringContaining('Error reading file')
|
|
115
|
+
);
|
|
116
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
117
|
+
expect.stringContaining('Permission denied')
|
|
118
|
+
);
|
|
119
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
120
|
+
|
|
121
|
+
processExitSpy.mockRestore();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should exit with error when file is empty', async () => {
|
|
125
|
+
jest.spyOn(mockFs, 'readFileSync').mockReturnValue(' \n \t ');
|
|
126
|
+
const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
127
|
+
throw new Error('Process exit');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await expect(
|
|
131
|
+
updateDescriptionCommand(mockTaskId, { fromFile: mockFilePath })
|
|
132
|
+
).rejects.toThrow('Process exit');
|
|
133
|
+
|
|
134
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
135
|
+
expect.stringContaining('File is empty')
|
|
136
|
+
);
|
|
137
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
138
|
+
|
|
139
|
+
processExitSpy.mockRestore();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should exit with error when markdown conversion fails', async () => {
|
|
143
|
+
const conversionError = new Error('Invalid markdown syntax');
|
|
144
|
+
mockMdToAdf.mockImplementation(() => {
|
|
145
|
+
throw conversionError;
|
|
146
|
+
});
|
|
147
|
+
const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
148
|
+
throw new Error('Process exit');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await expect(
|
|
152
|
+
updateDescriptionCommand(mockTaskId, { fromFile: mockFilePath })
|
|
153
|
+
).rejects.toThrow('Process exit');
|
|
154
|
+
|
|
155
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
156
|
+
expect.stringContaining('Error converting Markdown to ADF')
|
|
157
|
+
);
|
|
158
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
159
|
+
expect.stringContaining('Invalid markdown syntax')
|
|
160
|
+
);
|
|
161
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
162
|
+
|
|
163
|
+
processExitSpy.mockRestore();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should exit with error and hint when issue not found (404)', async () => {
|
|
167
|
+
const apiError = new Error('Issue not found (404)');
|
|
168
|
+
mockJiraClient.updateIssueDescription = jest.fn().mockRejectedValue(apiError);
|
|
169
|
+
const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
170
|
+
throw new Error('Process exit');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await expect(
|
|
174
|
+
updateDescriptionCommand(mockTaskId, { fromFile: mockFilePath })
|
|
175
|
+
).rejects.toThrow('Process exit');
|
|
176
|
+
|
|
177
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
178
|
+
expect.stringContaining('Issue not found (404)')
|
|
179
|
+
);
|
|
180
|
+
expect(console.log).toHaveBeenCalledWith(
|
|
181
|
+
expect.stringContaining('Check that the task ID is correct')
|
|
182
|
+
);
|
|
183
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
184
|
+
|
|
185
|
+
processExitSpy.mockRestore();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should exit with error and hint when permission denied (403)', async () => {
|
|
189
|
+
const apiError = new Error('Permission denied (403)');
|
|
190
|
+
mockJiraClient.updateIssueDescription = jest.fn().mockRejectedValue(apiError);
|
|
191
|
+
const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
192
|
+
throw new Error('Process exit');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
await expect(
|
|
196
|
+
updateDescriptionCommand(mockTaskId, { fromFile: mockFilePath })
|
|
197
|
+
).rejects.toThrow('Process exit');
|
|
198
|
+
|
|
199
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
200
|
+
expect.stringContaining('Permission denied (403)')
|
|
201
|
+
);
|
|
202
|
+
expect(console.log).toHaveBeenCalledWith(
|
|
203
|
+
expect.stringContaining('You may not have permission to edit this issue')
|
|
204
|
+
);
|
|
205
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
206
|
+
|
|
207
|
+
processExitSpy.mockRestore();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should exit with error for other API errors', async () => {
|
|
211
|
+
const apiError = new Error('Network connection failed');
|
|
212
|
+
mockJiraClient.updateIssueDescription = jest.fn().mockRejectedValue(apiError);
|
|
213
|
+
const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
|
|
214
|
+
throw new Error('Process exit');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await expect(
|
|
218
|
+
updateDescriptionCommand(mockTaskId, { fromFile: mockFilePath })
|
|
219
|
+
).rejects.toThrow('Process exit');
|
|
220
|
+
|
|
221
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
222
|
+
expect.stringContaining('Network connection failed')
|
|
223
|
+
);
|
|
224
|
+
expect(processExitSpy).toHaveBeenCalledWith(1);
|
|
225
|
+
|
|
226
|
+
processExitSpy.mockRestore();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should resolve relative file paths to absolute paths', async () => {
|
|
230
|
+
const relativeFilePath = './description.md';
|
|
231
|
+
const absolutePath = path.resolve(relativeFilePath);
|
|
232
|
+
|
|
233
|
+
await updateDescriptionCommand(mockTaskId, { fromFile: relativeFilePath });
|
|
234
|
+
|
|
235
|
+
expect(mockFs.existsSync).toHaveBeenCalledWith(absolutePath);
|
|
236
|
+
expect(mockFs.readFileSync).toHaveBeenCalledWith(absolutePath, 'utf-8');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle complex markdown content', async () => {
|
|
240
|
+
const complexMarkdown = `# Heading 1
|
|
241
|
+
|
|
242
|
+
## Heading 2
|
|
243
|
+
|
|
244
|
+
This is **bold** and *italic* text.
|
|
245
|
+
|
|
246
|
+
- List item 1
|
|
247
|
+
- List item 2
|
|
248
|
+
|
|
249
|
+
\`\`\`javascript
|
|
250
|
+
const example = "code block";
|
|
251
|
+
\`\`\`
|
|
252
|
+
|
|
253
|
+
[Link](https://example.com)`;
|
|
254
|
+
|
|
255
|
+
jest.spyOn(mockFs, 'readFileSync').mockReturnValue(complexMarkdown);
|
|
256
|
+
|
|
257
|
+
await updateDescriptionCommand(mockTaskId, { fromFile: mockFilePath });
|
|
258
|
+
|
|
259
|
+
expect(mockMdToAdf).toHaveBeenCalledWith(complexMarkdown);
|
|
260
|
+
expect(mockJiraClient.updateIssueDescription).toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
});
|
package/to-do.md
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"moduleResolution": "node",
|
|
14
|
+
"typeRoots": ["./node_modules/@types", "./src/types"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules"]
|
|
18
|
+
}
|