@eldrforge/ai-service 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/dist/index.d.ts +2 -0
- package/dist/index.js.map +1 -0
- package/dist/src/ai.d.ts +55 -0
- package/{src/index.ts → dist/src/index.d.ts} +1 -2
- package/dist/src/interactive.d.ts +122 -0
- package/dist/src/logger.d.ts +19 -0
- package/dist/src/prompts/commit.d.ts +29 -0
- package/dist/src/prompts/index.d.ts +10 -0
- package/dist/src/prompts/release.d.ts +25 -0
- package/dist/src/prompts/review.d.ts +21 -0
- package/dist/src/types.d.ts +99 -0
- package/package.json +11 -8
- package/.github/dependabot.yml +0 -12
- package/.github/workflows/npm-publish.yml +0 -48
- package/.github/workflows/test.yml +0 -33
- package/eslint.config.mjs +0 -84
- package/src/ai.ts +0 -421
- package/src/interactive.ts +0 -562
- package/src/logger.ts +0 -69
- package/src/prompts/commit.ts +0 -85
- package/src/prompts/index.ts +0 -28
- package/src/prompts/instructions/commit.md +0 -133
- package/src/prompts/instructions/release.md +0 -188
- package/src/prompts/instructions/review.md +0 -169
- package/src/prompts/personas/releaser.md +0 -24
- package/src/prompts/personas/you.md +0 -55
- package/src/prompts/release.ts +0 -118
- package/src/prompts/review.ts +0 -72
- package/src/types.ts +0 -112
- package/tests/ai-complete-coverage.test.ts +0 -241
- package/tests/ai-create-completion.test.ts +0 -288
- package/tests/ai-edge-cases.test.ts +0 -221
- package/tests/ai-openai-error.test.ts +0 -35
- package/tests/ai-transcribe.test.ts +0 -169
- package/tests/ai.test.ts +0 -139
- package/tests/interactive-editor.test.ts +0 -253
- package/tests/interactive-secure-temp.test.ts +0 -264
- package/tests/interactive-user-choice.test.ts +0 -173
- package/tests/interactive-user-text.test.ts +0 -174
- package/tests/interactive.test.ts +0 -94
- package/tests/logger-noop.test.ts +0 -40
- package/tests/logger.test.ts +0 -122
- package/tests/prompts.test.ts +0 -179
- package/tsconfig.json +0 -35
- package/vite.config.ts +0 -69
- package/vitest.config.ts +0 -25
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { transcribeAudio, OpenAIError } from '../src/ai';
|
|
3
|
-
|
|
4
|
-
// Create mock functions
|
|
5
|
-
const mockTranscriptionsCreate = vi.fn();
|
|
6
|
-
const mockReadStream = {
|
|
7
|
-
destroy: vi.fn(),
|
|
8
|
-
destroyed: false,
|
|
9
|
-
on: vi.fn(),
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
// Mock OpenAI
|
|
13
|
-
vi.mock('openai', () => ({
|
|
14
|
-
OpenAI: vi.fn().mockImplementation(() => ({
|
|
15
|
-
audio: {
|
|
16
|
-
transcriptions: {
|
|
17
|
-
create: mockTranscriptionsCreate,
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
})),
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
// Mock fs
|
|
24
|
-
vi.mock('fs', () => ({
|
|
25
|
-
default: {
|
|
26
|
-
createReadStream: vi.fn(() => mockReadStream),
|
|
27
|
-
},
|
|
28
|
-
createReadStream: vi.fn(() => mockReadStream),
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
// Mock logger
|
|
32
|
-
vi.mock('../src/logger', () => ({
|
|
33
|
-
getLogger: vi.fn(() => ({
|
|
34
|
-
info: vi.fn(),
|
|
35
|
-
error: vi.fn(),
|
|
36
|
-
warn: vi.fn(),
|
|
37
|
-
debug: vi.fn(),
|
|
38
|
-
})),
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
describe('transcribeAudio', () => {
|
|
42
|
-
beforeEach(() => {
|
|
43
|
-
vi.clearAllMocks();
|
|
44
|
-
process.env.OPENAI_API_KEY = 'test-key';
|
|
45
|
-
mockReadStream.destroyed = false;
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('should transcribe audio successfully', async () => {
|
|
49
|
-
mockTranscriptionsCreate.mockResolvedValue({
|
|
50
|
-
text: 'Transcribed text',
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const result = await transcribeAudio('/path/to/audio.mp3');
|
|
54
|
-
|
|
55
|
-
expect(result).toEqual({ text: 'Transcribed text' });
|
|
56
|
-
expect(mockTranscriptionsCreate).toHaveBeenCalled();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('should throw error if API key not set', async () => {
|
|
60
|
-
delete process.env.OPENAI_API_KEY;
|
|
61
|
-
|
|
62
|
-
await expect(
|
|
63
|
-
transcribeAudio('/path/to/audio.mp3')
|
|
64
|
-
).rejects.toThrow('OPENAI_API_KEY environment variable is not set');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should use specified model', async () => {
|
|
68
|
-
mockTranscriptionsCreate.mockResolvedValue({
|
|
69
|
-
text: 'Transcribed text',
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
await transcribeAudio('/path/to/audio.mp3', { model: 'whisper-2' });
|
|
73
|
-
|
|
74
|
-
expect(mockTranscriptionsCreate).toHaveBeenCalledWith(
|
|
75
|
-
expect.objectContaining({ model: 'whisper-2' })
|
|
76
|
-
);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should use default model if not specified', async () => {
|
|
80
|
-
mockTranscriptionsCreate.mockResolvedValue({
|
|
81
|
-
text: 'Transcribed text',
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
await transcribeAudio('/path/to/audio.mp3');
|
|
85
|
-
|
|
86
|
-
expect(mockTranscriptionsCreate).toHaveBeenCalledWith(
|
|
87
|
-
expect.objectContaining({ model: 'whisper-1' })
|
|
88
|
-
);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should call onArchive callback if provided', async () => {
|
|
92
|
-
mockTranscriptionsCreate.mockResolvedValue({
|
|
93
|
-
text: 'Transcribed text',
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const onArchive = vi.fn().mockResolvedValue(undefined);
|
|
97
|
-
|
|
98
|
-
await transcribeAudio('/path/to/audio.mp3', { onArchive });
|
|
99
|
-
|
|
100
|
-
expect(onArchive).toHaveBeenCalledWith('/path/to/audio.mp3', 'Transcribed text');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should continue if onArchive fails', async () => {
|
|
104
|
-
mockTranscriptionsCreate.mockResolvedValue({
|
|
105
|
-
text: 'Transcribed text',
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
const onArchive = vi.fn().mockRejectedValue(new Error('Archive failed'));
|
|
109
|
-
|
|
110
|
-
const result = await transcribeAudio('/path/to/audio.mp3', { onArchive });
|
|
111
|
-
|
|
112
|
-
expect(result).toEqual({ text: 'Transcribed text' });
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should close audio stream after success', async () => {
|
|
116
|
-
mockTranscriptionsCreate.mockResolvedValue({
|
|
117
|
-
text: 'Transcribed text',
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
await transcribeAudio('/path/to/audio.mp3');
|
|
121
|
-
|
|
122
|
-
expect(mockReadStream.destroy).toHaveBeenCalled();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('should close audio stream on API error', async () => {
|
|
126
|
-
mockTranscriptionsCreate.mockRejectedValue(new Error('API error'));
|
|
127
|
-
|
|
128
|
-
await expect(
|
|
129
|
-
transcribeAudio('/path/to/audio.mp3')
|
|
130
|
-
).rejects.toThrow(OpenAIError);
|
|
131
|
-
|
|
132
|
-
expect(mockReadStream.destroy).toHaveBeenCalled();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('should handle stream errors', async () => {
|
|
136
|
-
// Simulate stream error event
|
|
137
|
-
mockReadStream.on.mockImplementation((event: string, callback: any) => {
|
|
138
|
-
if (event === 'error') {
|
|
139
|
-
setTimeout(() => callback(new Error('Stream error')), 0);
|
|
140
|
-
}
|
|
141
|
-
return mockReadStream;
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
mockTranscriptionsCreate.mockResolvedValue({
|
|
145
|
-
text: 'Transcribed text',
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
const result = await transcribeAudio('/path/to/audio.mp3');
|
|
149
|
-
|
|
150
|
-
expect(result).toEqual({ text: 'Transcribed text' });
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should handle empty response', async () => {
|
|
154
|
-
mockTranscriptionsCreate.mockResolvedValue(null);
|
|
155
|
-
|
|
156
|
-
await expect(
|
|
157
|
-
transcribeAudio('/path/to/audio.mp3')
|
|
158
|
-
).rejects.toThrow('No transcription received from OpenAI');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('should wrap errors in OpenAIError', async () => {
|
|
162
|
-
mockTranscriptionsCreate.mockRejectedValue(new Error('Generic error'));
|
|
163
|
-
|
|
164
|
-
await expect(
|
|
165
|
-
transcribeAudio('/path/to/audio.mp3')
|
|
166
|
-
).rejects.toThrow(OpenAIError);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
package/tests/ai.test.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
import { describe, it, beforeEach, expect, vi } from 'vitest';
|
|
2
|
-
import type { AIConfig } from '../src/types';
|
|
3
|
-
import {
|
|
4
|
-
getModelForCommand,
|
|
5
|
-
getOpenAIReasoningForCommand,
|
|
6
|
-
isTokenLimitError,
|
|
7
|
-
isRateLimitError,
|
|
8
|
-
} from '../src/ai';
|
|
9
|
-
|
|
10
|
-
describe('AI Configuration Functions', () => {
|
|
11
|
-
describe('getModelForCommand', () => {
|
|
12
|
-
it('should return command-specific model when available', () => {
|
|
13
|
-
const config: AIConfig = {
|
|
14
|
-
model: 'gpt-4o-mini',
|
|
15
|
-
commands: {
|
|
16
|
-
commit: { model: 'gpt-4o' },
|
|
17
|
-
release: { model: 'gpt-4-turbo' },
|
|
18
|
-
review: { model: 'gpt-3.5-turbo' },
|
|
19
|
-
},
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
expect(getModelForCommand(config, 'commit')).toBe('gpt-4o');
|
|
23
|
-
expect(getModelForCommand(config, 'audio-commit')).toBe('gpt-4o');
|
|
24
|
-
expect(getModelForCommand(config, 'release')).toBe('gpt-4-turbo');
|
|
25
|
-
expect(getModelForCommand(config, 'review')).toBe('gpt-3.5-turbo');
|
|
26
|
-
expect(getModelForCommand(config, 'audio-review')).toBe('gpt-3.5-turbo');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should fallback to global model when command-specific model not available', () => {
|
|
30
|
-
const config: AIConfig = {
|
|
31
|
-
model: 'gpt-4o-mini',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
expect(getModelForCommand(config, 'commit')).toBe('gpt-4o-mini');
|
|
35
|
-
expect(getModelForCommand(config, 'release')).toBe('gpt-4o-mini');
|
|
36
|
-
expect(getModelForCommand(config, 'review')).toBe('gpt-4o-mini');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should fallback to default model when no models specified', () => {
|
|
40
|
-
const config: AIConfig = {};
|
|
41
|
-
|
|
42
|
-
expect(getModelForCommand(config, 'commit')).toBe('gpt-4o-mini');
|
|
43
|
-
expect(getModelForCommand(config, 'release')).toBe('gpt-4o-mini');
|
|
44
|
-
expect(getModelForCommand(config, 'review')).toBe('gpt-4o-mini');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should use global model for unknown commands', () => {
|
|
48
|
-
const config: AIConfig = {
|
|
49
|
-
model: 'gpt-4o',
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
expect(getModelForCommand(config, 'unknown-command')).toBe('gpt-4o');
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
describe('getOpenAIReasoningForCommand', () => {
|
|
57
|
-
it('should return command-specific reasoning when available', () => {
|
|
58
|
-
const config: AIConfig = {
|
|
59
|
-
reasoning: 'low',
|
|
60
|
-
commands: {
|
|
61
|
-
commit: { reasoning: 'high' },
|
|
62
|
-
release: { reasoning: 'medium' },
|
|
63
|
-
review: { reasoning: 'high' },
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
expect(getOpenAIReasoningForCommand(config, 'commit')).toBe('high');
|
|
68
|
-
expect(getOpenAIReasoningForCommand(config, 'audio-commit')).toBe('high');
|
|
69
|
-
expect(getOpenAIReasoningForCommand(config, 'release')).toBe('medium');
|
|
70
|
-
expect(getOpenAIReasoningForCommand(config, 'review')).toBe('high');
|
|
71
|
-
expect(getOpenAIReasoningForCommand(config, 'audio-review')).toBe('high');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should fallback to global reasoning when command-specific reasoning not available', () => {
|
|
75
|
-
const config: AIConfig = {
|
|
76
|
-
reasoning: 'medium',
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
expect(getOpenAIReasoningForCommand(config, 'commit')).toBe('medium');
|
|
80
|
-
expect(getOpenAIReasoningForCommand(config, 'release')).toBe('medium');
|
|
81
|
-
expect(getOpenAIReasoningForCommand(config, 'review')).toBe('medium');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('should fallback to default reasoning when no reasoning specified', () => {
|
|
85
|
-
const config: AIConfig = {};
|
|
86
|
-
|
|
87
|
-
expect(getOpenAIReasoningForCommand(config, 'commit')).toBe('low');
|
|
88
|
-
expect(getOpenAIReasoningForCommand(config, 'release')).toBe('low');
|
|
89
|
-
expect(getOpenAIReasoningForCommand(config, 'review')).toBe('low');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should use global reasoning for unknown commands', () => {
|
|
93
|
-
const config: AIConfig = {
|
|
94
|
-
reasoning: 'high',
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
expect(getOpenAIReasoningForCommand(config, 'unknown-command')).toBe('high');
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe('isTokenLimitError', () => {
|
|
102
|
-
it('should identify token limit errors by message content', () => {
|
|
103
|
-
expect(isTokenLimitError({ message: 'maximum context length exceeded' })).toBe(true);
|
|
104
|
-
expect(isTokenLimitError({ message: 'context_length_exceeded' })).toBe(true);
|
|
105
|
-
expect(isTokenLimitError({ message: 'token limit reached' })).toBe(true);
|
|
106
|
-
expect(isTokenLimitError({ message: 'too many tokens' })).toBe(true);
|
|
107
|
-
expect(isTokenLimitError({ message: 'please reduce the length' })).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should return false for non-token-limit errors', () => {
|
|
111
|
-
expect(isTokenLimitError({ message: 'Rate limit exceeded' })).toBe(false);
|
|
112
|
-
expect(isTokenLimitError({ message: 'Network error' })).toBe(false);
|
|
113
|
-
expect(isTokenLimitError({})).toBe(false);
|
|
114
|
-
expect(isTokenLimitError(null)).toBe(false);
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe('isRateLimitError', () => {
|
|
119
|
-
it('should identify rate limit errors by status code', () => {
|
|
120
|
-
expect(isRateLimitError({ status: 429 })).toBe(true);
|
|
121
|
-
expect(isRateLimitError({ code: 'rate_limit_exceeded' })).toBe(true);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('should identify rate limit errors by message content', () => {
|
|
125
|
-
expect(isRateLimitError({ message: 'rate limit exceeded' })).toBe(true);
|
|
126
|
-
expect(isRateLimitError({ message: 'too many requests' })).toBe(true);
|
|
127
|
-
expect(isRateLimitError({ message: 'quota exceeded' })).toBe(true);
|
|
128
|
-
expect(isRateLimitError({ message: 'rate limit reached' })).toBe(true);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should return false for non-rate-limit errors', () => {
|
|
132
|
-
expect(isRateLimitError({ message: 'Token limit exceeded' })).toBe(false);
|
|
133
|
-
expect(isRateLimitError({ message: 'Network error' })).toBe(false);
|
|
134
|
-
expect(isRateLimitError({})).toBe(false);
|
|
135
|
-
expect(isRateLimitError(null)).toBe(false);
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { editContentInEditor, getLLMFeedbackInEditor } from '../src/interactive';
|
|
3
|
-
|
|
4
|
-
// Mock child_process
|
|
5
|
-
vi.mock('child_process');
|
|
6
|
-
|
|
7
|
-
// Mock fs/promises
|
|
8
|
-
vi.mock('fs/promises');
|
|
9
|
-
|
|
10
|
-
// Mock os
|
|
11
|
-
vi.mock('os', () => ({
|
|
12
|
-
tmpdir: vi.fn(() => '/tmp'),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
// Mock logger
|
|
16
|
-
vi.mock('../src/logger', () => ({
|
|
17
|
-
getLogger: vi.fn(() => ({
|
|
18
|
-
info: vi.fn(),
|
|
19
|
-
error: vi.fn(),
|
|
20
|
-
warn: vi.fn(),
|
|
21
|
-
debug: vi.fn(),
|
|
22
|
-
})),
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
describe('editContentInEditor', () => {
|
|
26
|
-
let mockSpawnSync: any;
|
|
27
|
-
let mockReadFile: any;
|
|
28
|
-
let mockFsOpen: any;
|
|
29
|
-
let mockFd: any;
|
|
30
|
-
|
|
31
|
-
beforeEach(async () => {
|
|
32
|
-
vi.clearAllMocks();
|
|
33
|
-
|
|
34
|
-
// Import mocked modules
|
|
35
|
-
const childProcess = await import('child_process');
|
|
36
|
-
const fsPromises = await import('fs/promises');
|
|
37
|
-
|
|
38
|
-
mockSpawnSync = childProcess.spawnSync;
|
|
39
|
-
mockReadFile = fsPromises.readFile;
|
|
40
|
-
mockFsOpen = fsPromises.open;
|
|
41
|
-
|
|
42
|
-
// Setup mock file descriptor
|
|
43
|
-
mockFd = {
|
|
44
|
-
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
45
|
-
readFile: vi.fn().mockResolvedValue('content'),
|
|
46
|
-
close: vi.fn().mockResolvedValue(undefined),
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
mockFsOpen.mockResolvedValue(mockFd);
|
|
50
|
-
mockSpawnSync.mockReturnValue({ error: null, status: 0 });
|
|
51
|
-
mockReadFile.mockResolvedValue('edited content');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should edit content successfully', async () => {
|
|
55
|
-
mockReadFile.mockResolvedValue('edited content');
|
|
56
|
-
|
|
57
|
-
const result = await editContentInEditor('initial content');
|
|
58
|
-
|
|
59
|
-
expect(result).toBeDefined();
|
|
60
|
-
expect(result.content).toBe('edited content');
|
|
61
|
-
expect(result.wasEdited).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should use default editor', async () => {
|
|
65
|
-
mockReadFile.mockResolvedValue('content');
|
|
66
|
-
|
|
67
|
-
await editContentInEditor('test');
|
|
68
|
-
|
|
69
|
-
expect(mockSpawnSync).toHaveBeenCalledWith(
|
|
70
|
-
'vi',
|
|
71
|
-
expect.any(Array),
|
|
72
|
-
expect.any(Object)
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should use EDITOR env var', async () => {
|
|
77
|
-
process.env.EDITOR = 'nano';
|
|
78
|
-
mockReadFile.mockResolvedValue('content');
|
|
79
|
-
|
|
80
|
-
await editContentInEditor('test');
|
|
81
|
-
|
|
82
|
-
expect(mockSpawnSync).toHaveBeenCalledWith(
|
|
83
|
-
'nano',
|
|
84
|
-
expect.any(Array),
|
|
85
|
-
expect.any(Object)
|
|
86
|
-
);
|
|
87
|
-
|
|
88
|
-
delete process.env.EDITOR;
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should use specified editor', async () => {
|
|
92
|
-
mockReadFile.mockResolvedValue('content');
|
|
93
|
-
|
|
94
|
-
await editContentInEditor('test', [], '.txt', 'vim');
|
|
95
|
-
|
|
96
|
-
expect(mockSpawnSync).toHaveBeenCalledWith(
|
|
97
|
-
'vim',
|
|
98
|
-
expect.any(Array),
|
|
99
|
-
expect.any(Object)
|
|
100
|
-
);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should include template lines', async () => {
|
|
104
|
-
mockReadFile.mockResolvedValue('edited without comments');
|
|
105
|
-
|
|
106
|
-
const result = await editContentInEditor(
|
|
107
|
-
'content',
|
|
108
|
-
['# Template line 1', '# Template line 2'],
|
|
109
|
-
'.md'
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
expect(mockFd.writeFile).toHaveBeenCalledWith(
|
|
113
|
-
expect.stringContaining('# Template line 1'),
|
|
114
|
-
'utf8'
|
|
115
|
-
);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('should filter out comment lines', async () => {
|
|
119
|
-
mockReadFile.mockResolvedValue('# Comment\nreal content\n# Another comment\nmore content');
|
|
120
|
-
|
|
121
|
-
const result = await editContentInEditor('test');
|
|
122
|
-
|
|
123
|
-
expect(result.content).toBe('real content\nmore content');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should detect if content was edited', async () => {
|
|
127
|
-
mockReadFile.mockResolvedValue('initial content');
|
|
128
|
-
|
|
129
|
-
const result = await editContentInEditor('initial content');
|
|
130
|
-
|
|
131
|
-
expect(result.wasEdited).toBe(false);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should handle editor errors', async () => {
|
|
135
|
-
mockSpawnSync.mockReturnValue({
|
|
136
|
-
error: new Error('Editor not found'),
|
|
137
|
-
status: 0,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
await expect(
|
|
141
|
-
editContentInEditor('test')
|
|
142
|
-
).rejects.toThrow("Failed to launch editor 'vi'");
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('should throw if content is empty after editing', async () => {
|
|
146
|
-
mockReadFile.mockResolvedValue('# Just comments\n# More comments');
|
|
147
|
-
|
|
148
|
-
await expect(
|
|
149
|
-
editContentInEditor('test')
|
|
150
|
-
).rejects.toThrow('Content is empty after editing');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('should use specified file extension', async () => {
|
|
154
|
-
mockReadFile.mockResolvedValue('content');
|
|
155
|
-
|
|
156
|
-
await editContentInEditor('test', [], '.md');
|
|
157
|
-
|
|
158
|
-
expect(mockFd.writeFile).toHaveBeenCalled();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('should close file before opening editor', async () => {
|
|
162
|
-
mockReadFile.mockResolvedValue('content');
|
|
163
|
-
|
|
164
|
-
await editContentInEditor('test');
|
|
165
|
-
|
|
166
|
-
expect(mockFd.close).toHaveBeenCalled();
|
|
167
|
-
});
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
describe('getLLMFeedbackInEditor', () => {
|
|
171
|
-
let mockSpawnSync: any;
|
|
172
|
-
let mockReadFile: any;
|
|
173
|
-
let mockFsOpen: any;
|
|
174
|
-
let mockFd: any;
|
|
175
|
-
|
|
176
|
-
beforeEach(async () => {
|
|
177
|
-
vi.clearAllMocks();
|
|
178
|
-
|
|
179
|
-
const childProcess = await import('child_process');
|
|
180
|
-
const fsPromises = await import('fs/promises');
|
|
181
|
-
|
|
182
|
-
mockSpawnSync = childProcess.spawnSync;
|
|
183
|
-
mockReadFile = fsPromises.readFile;
|
|
184
|
-
mockFsOpen = fsPromises.open;
|
|
185
|
-
|
|
186
|
-
mockFd = {
|
|
187
|
-
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
188
|
-
readFile: vi.fn().mockResolvedValue('content'),
|
|
189
|
-
close: vi.fn().mockResolvedValue(undefined),
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
mockFsOpen.mockResolvedValue(mockFd);
|
|
193
|
-
mockSpawnSync.mockReturnValue({ error: null, status: 0 });
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('should get feedback for content improvement', async () => {
|
|
197
|
-
// Mock what user would type - comments get filtered by editContentInEditor
|
|
198
|
-
mockReadFile.mockResolvedValue('# comment line\nPlease improve the formatting\n\nOld content here');
|
|
199
|
-
|
|
200
|
-
const feedback = await getLLMFeedbackInEditor('commit message', 'old message');
|
|
201
|
-
|
|
202
|
-
// Since there's no ### ORIGINAL marker after filtering, it takes everything
|
|
203
|
-
expect(feedback).toContain('improve the formatting');
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('should extract feedback when user keeps the original marker', async () => {
|
|
207
|
-
// If user doesn't delete the ### ORIGINAL marker (not starting with #), it stays
|
|
208
|
-
// This allows the function to split feedback from original
|
|
209
|
-
mockReadFile.mockResolvedValue('Make it better\n\nxxx ORIGINAL CONTENT:\nold');
|
|
210
|
-
|
|
211
|
-
const feedback = await getLLMFeedbackInEditor('content', 'old');
|
|
212
|
-
|
|
213
|
-
// Since the marker doesn't match '### original', takes everything
|
|
214
|
-
expect(feedback).toContain('Make it better');
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('should throw if only original content remains', async () => {
|
|
218
|
-
// If user deletes all feedback and just leaves the original content
|
|
219
|
-
mockReadFile.mockResolvedValue('old');
|
|
220
|
-
|
|
221
|
-
const result = await getLLMFeedbackInEditor('content', 'old');
|
|
222
|
-
|
|
223
|
-
// Since there's actual content, it returns it (not empty)
|
|
224
|
-
expect(result).toBe('old');
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('should handle feedback without original section', async () => {
|
|
228
|
-
mockReadFile.mockResolvedValue('Just feedback text without markers');
|
|
229
|
-
|
|
230
|
-
const feedback = await getLLMFeedbackInEditor('content', 'old');
|
|
231
|
-
|
|
232
|
-
expect(feedback).toBe('Just feedback text without markers');
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('should use .md extension for editor', async () => {
|
|
236
|
-
mockReadFile.mockResolvedValue('feedback');
|
|
237
|
-
|
|
238
|
-
await getLLMFeedbackInEditor('content', 'test');
|
|
239
|
-
|
|
240
|
-
expect(mockFd.writeFile).toHaveBeenCalled();
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('should include template in editor', async () => {
|
|
244
|
-
mockReadFile.mockResolvedValue('feedback text');
|
|
245
|
-
|
|
246
|
-
await getLLMFeedbackInEditor('commit message', 'old content');
|
|
247
|
-
|
|
248
|
-
expect(mockFd.writeFile).toHaveBeenCalledWith(
|
|
249
|
-
expect.stringContaining('Provide Your Instructions'),
|
|
250
|
-
'utf8'
|
|
251
|
-
);
|
|
252
|
-
});
|
|
253
|
-
});
|