@ksw8954/git-ai-commit 1.0.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/AGENTS.md +38 -0
- package/CRUSH.md +28 -0
- package/Makefile +32 -0
- package/README.md +145 -0
- package/dist/commands/ai.d.ts +35 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +206 -0
- package/dist/commands/ai.js.map +1 -0
- package/dist/commands/commit.d.ts +17 -0
- package/dist/commands/commit.d.ts.map +1 -0
- package/dist/commands/commit.js +126 -0
- package/dist/commands/commit.js.map +1 -0
- package/dist/commands/config.d.ts +33 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +141 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/configCommand.d.ts +20 -0
- package/dist/commands/configCommand.d.ts.map +1 -0
- package/dist/commands/configCommand.js +108 -0
- package/dist/commands/configCommand.js.map +1 -0
- package/dist/commands/git.d.ts +26 -0
- package/dist/commands/git.d.ts.map +1 -0
- package/dist/commands/git.js +150 -0
- package/dist/commands/git.js.map +1 -0
- package/dist/commands/loadEnv.d.ts +2 -0
- package/dist/commands/loadEnv.d.ts.map +1 -0
- package/dist/commands/loadEnv.js +11 -0
- package/dist/commands/loadEnv.js.map +1 -0
- package/dist/commands/prCommand.d.ts +16 -0
- package/dist/commands/prCommand.d.ts.map +1 -0
- package/dist/commands/prCommand.js +61 -0
- package/dist/commands/prCommand.js.map +1 -0
- package/dist/commands/tag.d.ts +17 -0
- package/dist/commands/tag.d.ts.map +1 -0
- package/dist/commands/tag.js +127 -0
- package/dist/commands/tag.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/commit.d.ts +3 -0
- package/dist/prompts/commit.d.ts.map +1 -0
- package/dist/prompts/commit.js +101 -0
- package/dist/prompts/commit.js.map +1 -0
- package/dist/prompts/pr.d.ts +3 -0
- package/dist/prompts/pr.d.ts.map +1 -0
- package/dist/prompts/pr.js +58 -0
- package/dist/prompts/pr.js.map +1 -0
- package/dist/prompts/tag.d.ts +3 -0
- package/dist/prompts/tag.d.ts.map +1 -0
- package/dist/prompts/tag.js +42 -0
- package/dist/prompts/tag.js.map +1 -0
- package/eslint.config.js +35 -0
- package/jest.config.js +16 -0
- package/package.json +51 -0
- package/src/__tests__/ai.test.ts +185 -0
- package/src/__tests__/commitCommand.test.ts +155 -0
- package/src/__tests__/config.test.ts +238 -0
- package/src/__tests__/git.test.ts +88 -0
- package/src/__tests__/integration.test.ts +138 -0
- package/src/__tests__/prCommand.test.ts +121 -0
- package/src/__tests__/tagCommand.test.ts +197 -0
- package/src/commands/ai.ts +266 -0
- package/src/commands/commit.ts +215 -0
- package/src/commands/config.ts +182 -0
- package/src/commands/configCommand.ts +139 -0
- package/src/commands/git.ts +174 -0
- package/src/commands/history.ts +82 -0
- package/src/commands/loadEnv.ts +5 -0
- package/src/commands/log.ts +71 -0
- package/src/commands/prCommand.ts +108 -0
- package/src/commands/tag.ts +230 -0
- package/src/index.ts +29 -0
- package/src/prompts/commit.ts +105 -0
- package/src/prompts/pr.ts +64 -0
- package/src/prompts/tag.ts +48 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { loadEnv } from '../commands/loadEnv';
|
|
2
|
+
|
|
3
|
+
// Load environment variables from .env file before tests
|
|
4
|
+
loadEnv();
|
|
5
|
+
|
|
6
|
+
import { AIService } from '../commands/ai';
|
|
7
|
+
import { GitService } from '../commands/git';
|
|
8
|
+
import { ConfigService } from '../commands/config';
|
|
9
|
+
|
|
10
|
+
const hasApiKey = Boolean(
|
|
11
|
+
process.env.OPENAI_API_KEY ||
|
|
12
|
+
process.env.AI_API_KEY
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
describe('Integration Tests with Real API', () => {
|
|
16
|
+
let aiService: AIService;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
if (!hasApiKey) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const config = ConfigService.getConfig();
|
|
24
|
+
ConfigService.validateConfig(config);
|
|
25
|
+
aiService = new AIService({
|
|
26
|
+
apiKey: config.apiKey!,
|
|
27
|
+
baseURL: config.baseURL,
|
|
28
|
+
model: config.model,
|
|
29
|
+
language: config.language
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const describeIfApiKey = hasApiKey ? describe : describe.skip;
|
|
34
|
+
|
|
35
|
+
describeIfApiKey('Real API Integration', () => {
|
|
36
|
+
it('should generate commit message using real API', async () => {
|
|
37
|
+
// Sample git diff for testing
|
|
38
|
+
const sampleDiff = `diff --git a/package.json b/package.json
|
|
39
|
+
index 1a2b3c4..5d6e7f8 100644
|
|
40
|
+
--- a/package.json
|
|
41
|
+
+++ b/package.json
|
|
42
|
+
@@ -1,6 +1,6 @@
|
|
43
|
+
{
|
|
44
|
+
"name": "test-project",
|
|
45
|
+
- "version": "1.0.0",
|
|
46
|
+
+ "version": "1.1.0",
|
|
47
|
+
"description": "Test project",
|
|
48
|
+
"main": "index.js"
|
|
49
|
+
}
|
|
50
|
+
diff --git a/src/index.ts b/src/index.ts
|
|
51
|
+
index abc123..def456 100644
|
|
52
|
+
--- a/src/index.ts
|
|
53
|
+
+++ b/src/index.ts
|
|
54
|
+
@@ -1,5 +1,8 @@
|
|
55
|
+
console.log('Hello World');
|
|
56
|
+
+
|
|
57
|
+
+// Add new feature
|
|
58
|
+
+console.log('New feature added');
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
console.log('Testing with real API...');
|
|
62
|
+
console.log('Sample diff:', sampleDiff);
|
|
63
|
+
|
|
64
|
+
const result = await aiService.generateCommitMessage(sampleDiff);
|
|
65
|
+
|
|
66
|
+
console.log('API Response:', result);
|
|
67
|
+
|
|
68
|
+
expect(result.success).toBe(true);
|
|
69
|
+
expect(result.message).toBeDefined();
|
|
70
|
+
expect(result.message!.length).toBeGreaterThan(0);
|
|
71
|
+
|
|
72
|
+
// Check if the message follows conventional commit format
|
|
73
|
+
expect(result.message).toMatch(/^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?!?: .+/);
|
|
74
|
+
}, 30000); // 30 second timeout for real API call
|
|
75
|
+
|
|
76
|
+
it('should handle API errors gracefully', async () => {
|
|
77
|
+
// Test with empty diff to potentially trigger API error
|
|
78
|
+
const emptyDiff = '';
|
|
79
|
+
|
|
80
|
+
const result = await aiService.generateCommitMessage(emptyDiff);
|
|
81
|
+
|
|
82
|
+
console.log('Empty diff test result:', result);
|
|
83
|
+
|
|
84
|
+
// The API might still return a message, but if it fails, it should handle gracefully
|
|
85
|
+
if (!result.success) {
|
|
86
|
+
expect(result.error).toBeDefined();
|
|
87
|
+
expect(result.error!.length).toBeGreaterThan(0);
|
|
88
|
+
}
|
|
89
|
+
}, 60000);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('Git Service Integration', () => {
|
|
93
|
+
it('should get actual git diff if available', async () => {
|
|
94
|
+
const result = await GitService.getStagedDiff();
|
|
95
|
+
|
|
96
|
+
console.log('Git diff result:', result);
|
|
97
|
+
|
|
98
|
+
// If there are staged changes, success should be true
|
|
99
|
+
// If no staged changes, success should be false with appropriate error
|
|
100
|
+
if (result.success) {
|
|
101
|
+
expect(result.diff).toBeDefined();
|
|
102
|
+
expect(result.diff!.length).toBeGreaterThan(0);
|
|
103
|
+
} else {
|
|
104
|
+
expect(result.error).toBeDefined();
|
|
105
|
+
expect(result.error).toContain('No staged changes');
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('Configuration Integration', () => {
|
|
111
|
+
const itIfApiKey = hasApiKey ? it : it.skip;
|
|
112
|
+
|
|
113
|
+
itIfApiKey('should load configuration from environment', () => {
|
|
114
|
+
const config = ConfigService.getConfig();
|
|
115
|
+
ConfigService.validateConfig(config);
|
|
116
|
+
|
|
117
|
+
console.log('Loaded config:', {
|
|
118
|
+
apiKey: config.apiKey ? `${config.apiKey.slice(0, 8)}...${config.apiKey.slice(-4)}` : 'undefined',
|
|
119
|
+
baseURL: config.baseURL || 'undefined',
|
|
120
|
+
model: config.model || 'undefined',
|
|
121
|
+
language: config.language,
|
|
122
|
+
autoPush: config.autoPush
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(config.apiKey).toBeDefined();
|
|
126
|
+
expect(config.apiKey!.length).toBeGreaterThan(0);
|
|
127
|
+
|
|
128
|
+
// Should have model from .env or default
|
|
129
|
+
expect(config.model).toBeDefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
itIfApiKey('should validate configuration successfully', () => {
|
|
133
|
+
const config = ConfigService.getConfig();
|
|
134
|
+
|
|
135
|
+
expect(() => ConfigService.validateConfig(config)).not.toThrow();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { PullRequestCommand } from '../commands/prCommand';
|
|
2
|
+
import { GitService } from '../commands/git';
|
|
3
|
+
import { AIService } from '../commands/ai';
|
|
4
|
+
import { ConfigService } from '../commands/config';
|
|
5
|
+
|
|
6
|
+
const mockGeneratePullRequestMessage = jest.fn();
|
|
7
|
+
|
|
8
|
+
jest.mock('../commands/git', () => ({
|
|
9
|
+
GitService: {
|
|
10
|
+
getBranchDiff: jest.fn()
|
|
11
|
+
}
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
jest.mock('../commands/ai', () => ({
|
|
15
|
+
AIService: jest.fn().mockImplementation(() => ({
|
|
16
|
+
generatePullRequestMessage: mockGeneratePullRequestMessage
|
|
17
|
+
}))
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.mock('../commands/config', () => ({
|
|
21
|
+
ConfigService: {
|
|
22
|
+
getConfig: jest.fn(),
|
|
23
|
+
validateConfig: jest.fn()
|
|
24
|
+
}
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
describe('PullRequestCommand', () => {
|
|
28
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => undefined) as any);
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
jest.clearAllMocks();
|
|
32
|
+
|
|
33
|
+
(ConfigService.getConfig as jest.Mock).mockReturnValue({
|
|
34
|
+
apiKey: 'config-key',
|
|
35
|
+
baseURL: 'https://api.test',
|
|
36
|
+
model: 'test-model',
|
|
37
|
+
language: 'ko',
|
|
38
|
+
autoPush: false
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
(ConfigService.validateConfig as jest.Mock).mockReturnValue(undefined);
|
|
42
|
+
|
|
43
|
+
(GitService.getBranchDiff as jest.Mock).mockResolvedValue({
|
|
44
|
+
success: true,
|
|
45
|
+
diff: 'diff --git a/file b/file'
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
mockGeneratePullRequestMessage.mockResolvedValue({
|
|
49
|
+
success: true,
|
|
50
|
+
message: 'Add caching layer\n\n## Summary\n- ...\n\n## Testing\n- ...'
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterAll(() => {
|
|
55
|
+
exitSpy.mockRestore();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const createCommand = () => new PullRequestCommand();
|
|
59
|
+
|
|
60
|
+
it('prints generated pull request message on success', async () => {
|
|
61
|
+
const command = createCommand();
|
|
62
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await (command as any).handlePullRequest({ base: 'main', compare: 'feature/cache' });
|
|
66
|
+
|
|
67
|
+
expect(ConfigService.validateConfig).toHaveBeenCalledWith({
|
|
68
|
+
apiKey: 'config-key',
|
|
69
|
+
language: 'ko'
|
|
70
|
+
});
|
|
71
|
+
expect(GitService.getBranchDiff).toHaveBeenCalledWith('main', 'feature/cache');
|
|
72
|
+
expect(mockGeneratePullRequestMessage).toHaveBeenCalledWith(
|
|
73
|
+
'main',
|
|
74
|
+
'feature/cache',
|
|
75
|
+
'diff --git a/file b/file'
|
|
76
|
+
);
|
|
77
|
+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Add caching layer'));
|
|
78
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
79
|
+
} finally {
|
|
80
|
+
logSpy.mockRestore();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('exits when diff retrieval fails', async () => {
|
|
85
|
+
(GitService.getBranchDiff as jest.Mock).mockResolvedValueOnce({
|
|
86
|
+
success: false,
|
|
87
|
+
error: 'diff error'
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const command = createCommand();
|
|
91
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await (command as any).handlePullRequest({ base: 'main', compare: 'feature/cache' });
|
|
95
|
+
|
|
96
|
+
expect(errorSpy).toHaveBeenCalledWith('Error:', 'diff error');
|
|
97
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
98
|
+
} finally {
|
|
99
|
+
errorSpy.mockRestore();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('exits when AI generation fails', async () => {
|
|
104
|
+
mockGeneratePullRequestMessage.mockResolvedValueOnce({
|
|
105
|
+
success: false,
|
|
106
|
+
error: 'ai error'
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const command = createCommand();
|
|
110
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await (command as any).handlePullRequest({ base: 'main', compare: 'feature/cache' });
|
|
114
|
+
|
|
115
|
+
expect(errorSpy).toHaveBeenCalledWith('Error:', 'ai error');
|
|
116
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
117
|
+
} finally {
|
|
118
|
+
errorSpy.mockRestore();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { TagCommand } from '../commands/tag';
|
|
2
|
+
import { GitService } from '../commands/git';
|
|
3
|
+
import { AIService } from '../commands/ai';
|
|
4
|
+
import { ConfigService } from '../commands/config';
|
|
5
|
+
|
|
6
|
+
jest.mock('../commands/git', () => ({
|
|
7
|
+
GitService: {
|
|
8
|
+
getLatestTag: jest.fn(),
|
|
9
|
+
getCommitSummariesSince: jest.fn(),
|
|
10
|
+
createAnnotatedTag: jest.fn(),
|
|
11
|
+
pushTag: jest.fn()
|
|
12
|
+
}
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const mockGenerateTagNotes = jest.fn();
|
|
16
|
+
|
|
17
|
+
jest.mock('../commands/ai', () => ({
|
|
18
|
+
AIService: jest.fn().mockImplementation(() => ({
|
|
19
|
+
generateTagNotes: mockGenerateTagNotes
|
|
20
|
+
}))
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
jest.mock('../commands/config', () => ({
|
|
24
|
+
ConfigService: {
|
|
25
|
+
getConfig: jest.fn(),
|
|
26
|
+
validateConfig: jest.fn()
|
|
27
|
+
}
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
describe('TagCommand', () => {
|
|
31
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => undefined) as any);
|
|
32
|
+
let confirmSpy: jest.SpyInstance;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.clearAllMocks();
|
|
36
|
+
|
|
37
|
+
(ConfigService.getConfig as jest.Mock).mockReturnValue({
|
|
38
|
+
apiKey: 'env-key',
|
|
39
|
+
baseURL: 'https://env.test',
|
|
40
|
+
model: 'env-model',
|
|
41
|
+
language: 'ko',
|
|
42
|
+
autoPush: false
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
(ConfigService.validateConfig as jest.Mock).mockReturnValue(undefined);
|
|
46
|
+
|
|
47
|
+
// Confirm creation by default
|
|
48
|
+
jest
|
|
49
|
+
.spyOn(TagCommand.prototype as any, 'confirmTagCreate')
|
|
50
|
+
.mockResolvedValue(true);
|
|
51
|
+
|
|
52
|
+
// Do not push by default
|
|
53
|
+
confirmSpy = jest
|
|
54
|
+
.spyOn(TagCommand.prototype as any, 'confirmTagPush')
|
|
55
|
+
.mockResolvedValue(false);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
confirmSpy.mockRestore();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
afterAll(() => {
|
|
63
|
+
exitSpy.mockRestore();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const getTagCommand = () => new TagCommand();
|
|
67
|
+
|
|
68
|
+
it('should create tag with provided message without invoking AI', async () => {
|
|
69
|
+
(GitService.createAnnotatedTag as jest.Mock).mockResolvedValue(true);
|
|
70
|
+
|
|
71
|
+
const command = getTagCommand();
|
|
72
|
+
|
|
73
|
+
await (command as any).handleTag('v1.2.3', { message: 'Manual release notes' });
|
|
74
|
+
|
|
75
|
+
expect(GitService.createAnnotatedTag).toHaveBeenCalledWith('v1.2.3', 'Manual release notes');
|
|
76
|
+
expect(AIService).not.toHaveBeenCalled();
|
|
77
|
+
expect(GitService.pushTag).not.toHaveBeenCalled();
|
|
78
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should generate tag notes using AI when message is not provided', async () => {
|
|
82
|
+
(GitService.getCommitSummariesSince as jest.Mock).mockResolvedValue({
|
|
83
|
+
success: true,
|
|
84
|
+
log: '- feat: add feature\n- fix: bug fix'
|
|
85
|
+
});
|
|
86
|
+
(GitService.createAnnotatedTag as jest.Mock).mockResolvedValue(true);
|
|
87
|
+
|
|
88
|
+
mockGenerateTagNotes.mockResolvedValue({
|
|
89
|
+
success: true,
|
|
90
|
+
notes: '- Added feature\n- Fixed bug'
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const command = getTagCommand();
|
|
94
|
+
|
|
95
|
+
await (command as any).handleTag('v1.3.0', {
|
|
96
|
+
apiKey: 'test-key',
|
|
97
|
+
baseUrl: 'https://api.test',
|
|
98
|
+
model: 'gpt-test',
|
|
99
|
+
baseTag: 'v1.2.0'
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(GitService.getCommitSummariesSince).toHaveBeenCalledWith('v1.2.0');
|
|
103
|
+
expect(AIService).toHaveBeenCalledWith({
|
|
104
|
+
apiKey: 'test-key',
|
|
105
|
+
baseURL: 'https://api.test',
|
|
106
|
+
model: 'gpt-test',
|
|
107
|
+
language: 'ko'
|
|
108
|
+
});
|
|
109
|
+
expect(mockGenerateTagNotes).toHaveBeenCalledWith('v1.3.0', '- feat: add feature\n- fix: bug fix', undefined);
|
|
110
|
+
expect(GitService.createAnnotatedTag).toHaveBeenCalledWith('v1.3.0', '- Added feature\n- Fixed bug');
|
|
111
|
+
expect(GitService.pushTag).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should exit when commit history cannot be read', async () => {
|
|
115
|
+
(GitService.getCommitSummariesSince as jest.Mock).mockResolvedValue({
|
|
116
|
+
success: false,
|
|
117
|
+
error: 'No commits found'
|
|
118
|
+
});
|
|
119
|
+
(GitService.getLatestTag as jest.Mock).mockResolvedValue({
|
|
120
|
+
success: false,
|
|
121
|
+
error: 'No tags'
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const command = getTagCommand();
|
|
125
|
+
|
|
126
|
+
await (command as any).handleTag('v2.0.0', { apiKey: 'test-key' });
|
|
127
|
+
|
|
128
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
129
|
+
expect(GitService.createAnnotatedTag).not.toHaveBeenCalled();
|
|
130
|
+
expect(GitService.pushTag).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should use environment config when API key is not provided', async () => {
|
|
134
|
+
(GitService.getCommitSummariesSince as jest.Mock).mockResolvedValue({
|
|
135
|
+
success: true,
|
|
136
|
+
log: '- chore: update deps'
|
|
137
|
+
});
|
|
138
|
+
(GitService.createAnnotatedTag as jest.Mock).mockResolvedValue(true);
|
|
139
|
+
mockGenerateTagNotes.mockResolvedValue({
|
|
140
|
+
success: true,
|
|
141
|
+
notes: '- Updated dependencies'
|
|
142
|
+
});
|
|
143
|
+
(ConfigService.getConfig as jest.Mock).mockReturnValue({
|
|
144
|
+
apiKey: 'env-key',
|
|
145
|
+
baseURL: 'https://env.test',
|
|
146
|
+
model: 'env-model',
|
|
147
|
+
language: 'ko',
|
|
148
|
+
autoPush: false
|
|
149
|
+
});
|
|
150
|
+
(GitService.getLatestTag as jest.Mock).mockResolvedValue({
|
|
151
|
+
success: true,
|
|
152
|
+
tag: 'v1.0.0'
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const command = getTagCommand();
|
|
156
|
+
|
|
157
|
+
await (command as any).handleTag('v1.1.0', {});
|
|
158
|
+
|
|
159
|
+
expect(GitService.getLatestTag).toHaveBeenCalled();
|
|
160
|
+
expect(AIService).toHaveBeenCalledWith({
|
|
161
|
+
apiKey: 'env-key',
|
|
162
|
+
baseURL: 'https://env.test',
|
|
163
|
+
model: 'env-model',
|
|
164
|
+
language: 'ko'
|
|
165
|
+
});
|
|
166
|
+
expect(GitService.createAnnotatedTag).toHaveBeenCalledWith('v1.1.0', '- Updated dependencies');
|
|
167
|
+
expect(GitService.pushTag).not.toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should push tag when user confirms', async () => {
|
|
171
|
+
(GitService.createAnnotatedTag as jest.Mock).mockResolvedValue(true);
|
|
172
|
+
(GitService.pushTag as jest.Mock).mockResolvedValue(true);
|
|
173
|
+
confirmSpy.mockResolvedValueOnce(true);
|
|
174
|
+
|
|
175
|
+
const command = getTagCommand();
|
|
176
|
+
|
|
177
|
+
await (command as any).handleTag('v2.0.0', { message: 'Release notes' });
|
|
178
|
+
|
|
179
|
+
expect(GitService.createAnnotatedTag).toHaveBeenCalledWith('v2.0.0', 'Release notes');
|
|
180
|
+
expect(GitService.pushTag).toHaveBeenCalledWith('v2.0.0');
|
|
181
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should exit when tag push fails after confirmation', async () => {
|
|
185
|
+
(GitService.createAnnotatedTag as jest.Mock).mockResolvedValue(true);
|
|
186
|
+
(GitService.pushTag as jest.Mock).mockResolvedValue(false);
|
|
187
|
+
confirmSpy.mockResolvedValueOnce(true);
|
|
188
|
+
|
|
189
|
+
const command = getTagCommand();
|
|
190
|
+
|
|
191
|
+
await (command as any).handleTag('v3.0.0', { message: 'Release notes' });
|
|
192
|
+
|
|
193
|
+
expect(GitService.createAnnotatedTag).toHaveBeenCalledWith('v3.0.0', 'Release notes');
|
|
194
|
+
expect(GitService.pushTag).toHaveBeenCalledWith('v3.0.0');
|
|
195
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
196
|
+
});
|
|
197
|
+
});
|