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.
Files changed (45) hide show
  1. package/README.md +364 -0
  2. package/dist/cli.js +87 -0
  3. package/dist/commands/about.js +83 -0
  4. package/dist/commands/add-comment.js +109 -0
  5. package/dist/commands/me.js +23 -0
  6. package/dist/commands/project-statuses.js +23 -0
  7. package/dist/commands/projects.js +35 -0
  8. package/dist/commands/run-jql.js +44 -0
  9. package/dist/commands/task-with-details.js +23 -0
  10. package/dist/commands/update-description.js +109 -0
  11. package/dist/lib/formatters.js +193 -0
  12. package/dist/lib/jira-client.js +216 -0
  13. package/dist/lib/settings.js +68 -0
  14. package/dist/lib/utils.js +79 -0
  15. package/jest.config.js +21 -0
  16. package/package.json +47 -0
  17. package/settings.yaml +24 -0
  18. package/src/cli.ts +97 -0
  19. package/src/commands/about.ts +98 -0
  20. package/src/commands/add-comment.ts +94 -0
  21. package/src/commands/me.ts +18 -0
  22. package/src/commands/project-statuses.ts +18 -0
  23. package/src/commands/projects.ts +32 -0
  24. package/src/commands/run-jql.ts +40 -0
  25. package/src/commands/task-with-details.ts +18 -0
  26. package/src/commands/update-description.ts +94 -0
  27. package/src/lib/formatters.ts +224 -0
  28. package/src/lib/jira-client.ts +319 -0
  29. package/src/lib/settings.ts +77 -0
  30. package/src/lib/utils.ts +76 -0
  31. package/src/types/md-to-adf.d.ts +14 -0
  32. package/tests/README.md +97 -0
  33. package/tests/__mocks__/jira.js.ts +4 -0
  34. package/tests/__mocks__/md-to-adf.ts +7 -0
  35. package/tests/__mocks__/mdast-util-from-adf.ts +4 -0
  36. package/tests/__mocks__/mdast-util-to-markdown.ts +1 -0
  37. package/tests/add-comment.test.ts +226 -0
  38. package/tests/cli-permissions.test.ts +156 -0
  39. package/tests/jira-client.test.ts +123 -0
  40. package/tests/projects.test.ts +205 -0
  41. package/tests/settings.test.ts +288 -0
  42. package/tests/task-with-details.test.ts +83 -0
  43. package/tests/update-description.test.ts +262 -0
  44. package/to-do.md +9 -0
  45. package/tsconfig.json +18 -0
@@ -0,0 +1,76 @@
1
+ import chalk from 'chalk';
2
+ import { fromADF } from 'mdast-util-from-adf';
3
+ import { toMarkdown } from 'mdast-util-to-markdown';
4
+
5
+ /**
6
+ * Validate required environment variables
7
+ */
8
+ export function validateEnvVars(): void {
9
+ const required = [
10
+ 'JIRA_HOST',
11
+ 'JIRA_USER_EMAIL',
12
+ 'JIRA_API_TOKEN',
13
+ ];
14
+
15
+ const missing = required.filter((key) => !process.env[key]);
16
+
17
+ if (missing.length > 0) {
18
+ console.error(chalk.red('✗ Missing required environment variables:\n'));
19
+ missing.forEach((key) => console.error(chalk.red(` - ${key}`)));
20
+ console.log('\nPlease create a .env file with the following variables:');
21
+ console.log(' JIRA_HOST=https://your-domain.atlassian.net');
22
+ console.log(' JIRA_USER_EMAIL=your-email@example.com');
23
+ console.log(' JIRA_API_TOKEN=your-api-token');
24
+ console.log('\nGet your API token from: https://id.atlassian.com/manage-profile/security/api-tokens');
25
+ process.exit(1);
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Format timestamp for display
31
+ */
32
+ export function formatTimestamp(date: string | Date): string {
33
+ const d = typeof date === 'string' ? new Date(date) : date;
34
+ return d.toLocaleString('en-US', {
35
+ year: 'numeric',
36
+ month: 'short',
37
+ day: 'numeric',
38
+ hour: '2-digit',
39
+ minute: '2-digit',
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Truncate text to max length with ellipsis
45
+ */
46
+ export function truncate(text: string, maxLength: number): string {
47
+ if (text.length <= maxLength) return text;
48
+ return text.substring(0, maxLength - 3) + '...';
49
+ }
50
+
51
+ /**
52
+ * Convert Atlassian Document Format (ADF) to Markdown
53
+ * Handles both string content and ADF JSON objects
54
+ */
55
+ export function convertADFToMarkdown(content: any): string {
56
+ // If content is already a string, return it as-is
57
+ if (typeof content === 'string') {
58
+ return content;
59
+ }
60
+
61
+ // If content is null or undefined, return empty string
62
+ if (!content) {
63
+ return '';
64
+ }
65
+
66
+ try {
67
+ // Convert ADF to mdast, then mdast to markdown
68
+ const mdastTree = fromADF(content);
69
+ const markdown = toMarkdown(mdastTree);
70
+ return markdown.trim();
71
+ } catch (error) {
72
+ // If conversion fails, fall back to JSON string representation
73
+ console.error('Failed to convert ADF to Markdown:', error);
74
+ return JSON.stringify(content, null, 2);
75
+ }
76
+ }
@@ -0,0 +1,14 @@
1
+ declare module 'md-to-adf' {
2
+ /**
3
+ * Convert Markdown to Atlassian Document Format (ADF)
4
+ * @param markdown - GitHub Flavored Markdown text
5
+ * @returns ADF document object
6
+ */
7
+ function mdToAdf(markdown: string): {
8
+ version: number;
9
+ type: string;
10
+ content: any[];
11
+ };
12
+
13
+ export = mdToAdf;
14
+ }
@@ -0,0 +1,97 @@
1
+ # Test Suite Documentation
2
+
3
+ ## Overview
4
+
5
+ This test suite provides comprehensive coverage for the Jira AI settings and permissions system.
6
+
7
+ ## Test Files
8
+
9
+ ### 1. `settings.test.ts` (16 tests)
10
+ Tests the core settings module functionality:
11
+ - **Loading settings**: Reading and parsing settings.yaml
12
+ - **Project filtering**: Testing project allow/deny lists
13
+ - **Command filtering**: Testing command permissions
14
+ - **Caching**: Ensuring settings are cached properly
15
+ - **Error handling**: Invalid YAML, missing files
16
+
17
+ **Coverage**: 100% of settings.ts module
18
+
19
+ ### 2. `projects.test.ts` (6 tests)
20
+ Tests the projects command with filtering:
21
+ - Displaying all projects when "all" is allowed
22
+ - Filtering projects based on settings.yaml
23
+ - Handling empty results
24
+ - API error handling
25
+
26
+ **Coverage**: 100% of projects.ts command
27
+
28
+ ### 3. `cli-permissions.test.ts` (9 tests)
29
+ Tests the CLI permission wrapper:
30
+ - Command allow/deny logic
31
+ - Permission enforcement at CLI level
32
+ - Error messages for blocked commands
33
+ - Integration scenarios
34
+
35
+ **Total: 31 tests passing**
36
+
37
+ ## Running Tests
38
+
39
+ ```bash
40
+ # Run all tests
41
+ npm test
42
+
43
+ # Run tests in watch mode
44
+ npm run test:watch
45
+
46
+ # Run tests with coverage report
47
+ npm run test:coverage
48
+
49
+ # Run tests with verbose output
50
+ npm run test:verbose
51
+ ```
52
+
53
+ ## Test Coverage
54
+
55
+ Current coverage for tested modules:
56
+ - `src/lib/settings.ts`: 100%
57
+ - `src/commands/projects.ts`: 100%
58
+
59
+ ## Key Test Scenarios Covered
60
+
61
+ ### Settings Module
62
+ ✓ Load settings from YAML file
63
+ ✓ Default to "all" when file doesn't exist
64
+ ✓ Handle null/undefined values
65
+ ✓ Exit on invalid YAML
66
+ ✓ Project allow/deny filtering
67
+ ✓ Command allow/deny filtering
68
+ ✓ Case-sensitive project matching
69
+ ✓ Settings caching
70
+
71
+ ### Projects Command
72
+ ✓ Show all projects when "all" allowed
73
+ ✓ Filter to specific projects (BP, PM, PS)
74
+ ✓ Show warning for no matches
75
+ ✓ Handle API errors gracefully
76
+
77
+ ### CLI Permissions
78
+ ✓ Allow/deny command execution
79
+ ✓ Display helpful error messages
80
+ ✓ Support "all" wildcard
81
+ ✓ Pass arguments to allowed commands
82
+
83
+ ## Mock Strategy
84
+
85
+ The test suite uses Jest mocks for:
86
+ - **fs module**: Mocked to test file I/O without actual files
87
+ - **jira.js**: Mocked to avoid ES module issues
88
+ - **ora, chalk**: Mocked to test CLI output
89
+ - **console**: Spied to verify error/warning messages
90
+
91
+ ## Future Test Coverage
92
+
93
+ Potential areas to expand testing:
94
+ - Other command modules (me, task-with-details, project-statuses)
95
+ - Formatters
96
+ - Jira client integration tests
97
+ - End-to-end CLI tests
@@ -0,0 +1,4 @@
1
+ // Mock for jira.js library to avoid ES module issues in tests
2
+ export class Version3Client {
3
+ constructor() {}
4
+ }
@@ -0,0 +1,7 @@
1
+ const mdToAdf = jest.fn((markdown: string) => ({
2
+ version: 1,
3
+ type: 'doc',
4
+ content: []
5
+ }));
6
+
7
+ export default mdToAdf;
@@ -0,0 +1,4 @@
1
+ export const fromADF = jest.fn((adf: any) => ({
2
+ type: 'root',
3
+ children: []
4
+ }));
@@ -0,0 +1 @@
1
+ export const toMarkdown = jest.fn((tree: any) => 'Mocked markdown content');
@@ -0,0 +1,226 @@
1
+ import { addCommentCommand } from '../src/commands/add-comment';
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('Add Comment Command', () => {
25
+ const mockIssueKey = 'TEST-123';
26
+ const mockFilePath = '/path/to/comment.md';
27
+ const mockMarkdownContent = '# Test Comment\n\nThis is a test comment.';
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 Comment' }]
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.addIssueComment = jest.fn().mockResolvedValue(undefined);
50
+ });
51
+
52
+ it('should successfully add a comment to a Jira issue', async () => {
53
+ await addCommentCommand({ filePath: mockFilePath, issueKey: mockIssueKey });
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.addIssueComment).toHaveBeenCalledWith(
59
+ mockIssueKey,
60
+ mockAdfContent
61
+ );
62
+ expect(console.log).toHaveBeenCalled();
63
+ });
64
+
65
+ it('should exit with error when issue key is empty', async () => {
66
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
67
+ throw new Error('Process exit');
68
+ });
69
+
70
+ await expect(addCommentCommand({ filePath: mockFilePath, issueKey: '' })).rejects.toThrow(
71
+ 'Process exit'
72
+ );
73
+
74
+ expect(console.error).toHaveBeenCalledWith(
75
+ expect.stringContaining('Issue key is required')
76
+ );
77
+ expect(processExitSpy).toHaveBeenCalledWith(1);
78
+
79
+ processExitSpy.mockRestore();
80
+ });
81
+
82
+ it('should exit with error when file path is empty', async () => {
83
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
84
+ throw new Error('Process exit');
85
+ });
86
+
87
+ await expect(addCommentCommand({ filePath: '', issueKey: mockIssueKey })).rejects.toThrow(
88
+ 'Process exit'
89
+ );
90
+
91
+ expect(console.error).toHaveBeenCalledWith(
92
+ expect.stringContaining('File path is required')
93
+ );
94
+ expect(processExitSpy).toHaveBeenCalledWith(1);
95
+
96
+ processExitSpy.mockRestore();
97
+ });
98
+
99
+ it('should exit with error when file does not exist', async () => {
100
+ jest.spyOn(mockFs, 'existsSync').mockReturnValue(false);
101
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
102
+ throw new Error('Process exit');
103
+ });
104
+
105
+ await expect(
106
+ addCommentCommand({ filePath: mockFilePath, issueKey: mockIssueKey })
107
+ ).rejects.toThrow('Process exit');
108
+
109
+ expect(console.error).toHaveBeenCalledWith(
110
+ expect.stringContaining('File not found')
111
+ );
112
+ expect(processExitSpy).toHaveBeenCalledWith(1);
113
+
114
+ processExitSpy.mockRestore();
115
+ });
116
+
117
+ it('should exit with error when file read fails', async () => {
118
+ const readError = new Error('Permission denied');
119
+ jest.spyOn(mockFs, 'readFileSync').mockImplementation(() => {
120
+ throw readError;
121
+ });
122
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
123
+ throw new Error('Process exit');
124
+ });
125
+
126
+ await expect(
127
+ addCommentCommand({ filePath: mockFilePath, issueKey: mockIssueKey })
128
+ ).rejects.toThrow('Process exit');
129
+
130
+ expect(console.error).toHaveBeenCalledWith(
131
+ expect.stringContaining('Error reading file')
132
+ );
133
+ expect(console.error).toHaveBeenCalledWith(
134
+ expect.stringContaining('Permission denied')
135
+ );
136
+ expect(processExitSpy).toHaveBeenCalledWith(1);
137
+
138
+ processExitSpy.mockRestore();
139
+ });
140
+
141
+ it('should exit with error when file is empty', async () => {
142
+ jest.spyOn(mockFs, 'readFileSync').mockReturnValue(' \n \t ');
143
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
144
+ throw new Error('Process exit');
145
+ });
146
+
147
+ await expect(
148
+ addCommentCommand({ filePath: mockFilePath, issueKey: mockIssueKey })
149
+ ).rejects.toThrow('Process exit');
150
+
151
+ expect(console.error).toHaveBeenCalledWith(
152
+ expect.stringContaining('File is empty')
153
+ );
154
+ expect(processExitSpy).toHaveBeenCalledWith(1);
155
+
156
+ processExitSpy.mockRestore();
157
+ });
158
+
159
+ it('should exit with error when markdown conversion fails', async () => {
160
+ const conversionError = new Error('Invalid markdown syntax');
161
+ mockMdToAdf.mockImplementation(() => {
162
+ throw conversionError;
163
+ });
164
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
165
+ throw new Error('Process exit');
166
+ });
167
+
168
+ await expect(
169
+ addCommentCommand({ filePath: mockFilePath, issueKey: mockIssueKey })
170
+ ).rejects.toThrow('Process exit');
171
+
172
+ expect(console.error).toHaveBeenCalledWith(
173
+ expect.stringContaining('Error converting Markdown to ADF')
174
+ );
175
+ expect(console.error).toHaveBeenCalledWith(
176
+ expect.stringContaining('Invalid markdown syntax')
177
+ );
178
+ expect(processExitSpy).toHaveBeenCalledWith(1);
179
+
180
+ processExitSpy.mockRestore();
181
+ });
182
+
183
+ it('should exit with error and hint when issue not found (404)', async () => {
184
+ const apiError = new Error('Issue not found (404)');
185
+ mockJiraClient.addIssueComment = jest.fn().mockRejectedValue(apiError);
186
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
187
+ throw new Error('Process exit');
188
+ });
189
+
190
+ await expect(
191
+ addCommentCommand({ filePath: mockFilePath, issueKey: mockIssueKey })
192
+ ).rejects.toThrow('Process exit');
193
+
194
+ expect(console.error).toHaveBeenCalledWith(
195
+ expect.stringContaining('Issue not found (404)')
196
+ );
197
+ expect(console.log).toHaveBeenCalledWith(
198
+ expect.stringContaining('Check that the issue key is correct')
199
+ );
200
+ expect(processExitSpy).toHaveBeenCalledWith(1);
201
+
202
+ processExitSpy.mockRestore();
203
+ });
204
+
205
+ it('should exit with error and hint when permission denied (403)', async () => {
206
+ const apiError = new Error('Permission denied (403)');
207
+ mockJiraClient.addIssueComment = jest.fn().mockRejectedValue(apiError);
208
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
209
+ throw new Error('Process exit');
210
+ });
211
+
212
+ await expect(
213
+ addCommentCommand({ filePath: mockFilePath, issueKey: mockIssueKey })
214
+ ).rejects.toThrow('Process exit');
215
+
216
+ expect(console.error).toHaveBeenCalledWith(
217
+ expect.stringContaining('Permission denied (403)')
218
+ );
219
+ expect(console.log).toHaveBeenCalledWith(
220
+ expect.stringContaining('You may not have permission to comment on this issue')
221
+ );
222
+ expect(processExitSpy).toHaveBeenCalledWith(1);
223
+
224
+ processExitSpy.mockRestore();
225
+ });
226
+ });
@@ -0,0 +1,156 @@
1
+ import * as settings from '../src/lib/settings';
2
+
3
+ // Mock all dependencies
4
+ jest.mock('../src/lib/settings');
5
+ jest.mock('../src/commands/me');
6
+ jest.mock('../src/commands/projects');
7
+ jest.mock('../src/commands/task-with-details');
8
+ jest.mock('../src/commands/project-statuses');
9
+ jest.mock('../src/commands/about');
10
+ jest.mock('../src/lib/utils', () => ({
11
+ validateEnvVars: jest.fn()
12
+ }));
13
+ jest.mock('dotenv', () => ({
14
+ config: jest.fn()
15
+ }));
16
+
17
+ const mockSettings = settings as jest.Mocked<typeof settings>;
18
+
19
+ describe('CLI Command Permissions', () => {
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+ console.log = jest.fn();
23
+ console.error = jest.fn();
24
+ });
25
+
26
+ describe('Command permission checks', () => {
27
+ it('should allow execution when command is in allowed list', () => {
28
+ mockSettings.isCommandAllowed.mockReturnValue(true);
29
+ mockSettings.getAllowedCommands.mockReturnValue(['me', 'projects']);
30
+
31
+ expect(mockSettings.isCommandAllowed('me')).toBe(true);
32
+ expect(mockSettings.isCommandAllowed('projects')).toBe(true);
33
+ });
34
+
35
+ it('should deny execution when command is not in allowed list', () => {
36
+ mockSettings.isCommandAllowed.mockImplementation((cmd: string) =>
37
+ ['me', 'projects'].includes(cmd)
38
+ );
39
+ mockSettings.getAllowedCommands.mockReturnValue(['me', 'projects']);
40
+
41
+ expect(mockSettings.isCommandAllowed('task-with-details')).toBe(false);
42
+ expect(mockSettings.isCommandAllowed('project-statuses')).toBe(false);
43
+ });
44
+
45
+ it('should allow all commands when "all" is specified', () => {
46
+ mockSettings.isCommandAllowed.mockReturnValue(true);
47
+ mockSettings.getAllowedCommands.mockReturnValue(['all']);
48
+
49
+ expect(mockSettings.isCommandAllowed('me')).toBe(true);
50
+ expect(mockSettings.isCommandAllowed('projects')).toBe(true);
51
+ expect(mockSettings.isCommandAllowed('task-with-details')).toBe(true);
52
+ expect(mockSettings.isCommandAllowed('project-statuses')).toBe(true);
53
+ });
54
+ });
55
+
56
+ describe('Permission wrapper behavior', () => {
57
+ it('should execute command when permission is granted', async () => {
58
+ mockSettings.isCommandAllowed.mockReturnValue(true);
59
+
60
+ const mockCommand = jest.fn().mockResolvedValue(undefined);
61
+ const wrappedCommand = async (...args: any[]) => {
62
+ if (!mockSettings.isCommandAllowed('me')) {
63
+ console.error('Command not allowed');
64
+ process.exit(1);
65
+ }
66
+ return mockCommand(...args);
67
+ };
68
+
69
+ await wrappedCommand('arg1', 'arg2');
70
+
71
+ expect(mockCommand).toHaveBeenCalledWith('arg1', 'arg2');
72
+ expect(console.error).not.toHaveBeenCalled();
73
+ });
74
+
75
+ it('should block command execution when permission is denied', async () => {
76
+ mockSettings.isCommandAllowed.mockReturnValue(false);
77
+ mockSettings.getAllowedCommands.mockReturnValue(['me', 'projects']);
78
+
79
+ const mockCommand = jest.fn().mockResolvedValue(undefined);
80
+ const processExitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {
81
+ throw new Error('Process exit');
82
+ });
83
+
84
+ const wrappedCommand = async (...args: any[]) => {
85
+ if (!mockSettings.isCommandAllowed('task-with-details')) {
86
+ console.error("Command 'task-with-details' is not allowed.");
87
+ console.log('Allowed commands: ' + mockSettings.getAllowedCommands().join(', '));
88
+ process.exit(1);
89
+ }
90
+ return mockCommand(...args);
91
+ };
92
+
93
+ await expect(wrappedCommand()).rejects.toThrow('Process exit');
94
+
95
+ expect(mockCommand).not.toHaveBeenCalled();
96
+ expect(console.error).toHaveBeenCalledWith(expect.stringContaining('not allowed'));
97
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('me, projects'));
98
+ expect(processExitSpy).toHaveBeenCalledWith(1);
99
+
100
+ processExitSpy.mockRestore();
101
+ });
102
+
103
+ it('should pass arguments correctly to allowed commands', async () => {
104
+ mockSettings.isCommandAllowed.mockReturnValue(true);
105
+
106
+ const mockCommand = jest.fn().mockResolvedValue(undefined);
107
+ const wrappedCommand = async (...args: any[]) => {
108
+ if (!mockSettings.isCommandAllowed('task-with-details')) {
109
+ process.exit(1);
110
+ }
111
+ return mockCommand(...args);
112
+ };
113
+
114
+ await wrappedCommand('BP-123');
115
+
116
+ expect(mockCommand).toHaveBeenCalledWith('BP-123');
117
+ expect(mockCommand).toHaveBeenCalledTimes(1);
118
+ });
119
+ });
120
+
121
+ describe('Integration scenarios', () => {
122
+ it('should handle scenario: only me and projects allowed', () => {
123
+ mockSettings.isCommandAllowed.mockImplementation((cmd: string) =>
124
+ ['me', 'projects'].includes(cmd)
125
+ );
126
+ mockSettings.getAllowedCommands.mockReturnValue(['me', 'projects']);
127
+
128
+ expect(mockSettings.isCommandAllowed('me')).toBe(true);
129
+ expect(mockSettings.isCommandAllowed('projects')).toBe(true);
130
+ expect(mockSettings.isCommandAllowed('task-with-details')).toBe(false);
131
+ expect(mockSettings.isCommandAllowed('project-statuses')).toBe(false);
132
+ expect(mockSettings.isCommandAllowed('about')).toBe(false);
133
+ });
134
+
135
+ it('should handle scenario: all commands allowed', () => {
136
+ mockSettings.isCommandAllowed.mockReturnValue(true);
137
+ mockSettings.getAllowedCommands.mockReturnValue(['all']);
138
+
139
+ const commands = ['me', 'projects', 'task-with-details', 'project-statuses', 'about'];
140
+ commands.forEach(cmd => {
141
+ expect(mockSettings.isCommandAllowed(cmd)).toBe(true);
142
+ });
143
+ });
144
+
145
+ it('should handle scenario: no commands allowed except about', () => {
146
+ mockSettings.isCommandAllowed.mockImplementation((cmd: string) =>
147
+ cmd === 'about'
148
+ );
149
+ mockSettings.getAllowedCommands.mockReturnValue([]);
150
+
151
+ expect(mockSettings.isCommandAllowed('me')).toBe(false);
152
+ expect(mockSettings.isCommandAllowed('projects')).toBe(false);
153
+ expect(mockSettings.isCommandAllowed('about')).toBe(true);
154
+ });
155
+ });
156
+ });