@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,264 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { SecureTempFile, createSecureTempFile, cleanupTempFile } from '../src/interactive';
|
|
3
|
-
import fs from 'fs/promises';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
|
|
6
|
-
// Mock fs/promises
|
|
7
|
-
vi.mock('fs/promises');
|
|
8
|
-
vi.mock('os');
|
|
9
|
-
vi.mock('../src/logger', () => ({
|
|
10
|
-
getLogger: vi.fn(() => ({
|
|
11
|
-
info: vi.fn(),
|
|
12
|
-
error: vi.fn(),
|
|
13
|
-
warn: vi.fn(),
|
|
14
|
-
debug: vi.fn()
|
|
15
|
-
}))
|
|
16
|
-
}));
|
|
17
|
-
|
|
18
|
-
describe('SecureTempFile', () => {
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
vi.clearAllMocks();
|
|
21
|
-
|
|
22
|
-
// Mock os.tmpdir
|
|
23
|
-
vi.mocked(os.tmpdir).mockReturnValue('/tmp');
|
|
24
|
-
|
|
25
|
-
// Mock fs methods
|
|
26
|
-
vi.mocked(fs.access).mockResolvedValue(undefined);
|
|
27
|
-
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
28
|
-
vi.mocked(fs.unlink).mockResolvedValue(undefined);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
afterEach(() => {
|
|
32
|
-
vi.clearAllMocks();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe('create', () => {
|
|
36
|
-
it('should create a secure temp file', async () => {
|
|
37
|
-
const mockFd = {
|
|
38
|
-
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
39
|
-
readFile: vi.fn().mockResolvedValue('content'),
|
|
40
|
-
close: vi.fn().mockResolvedValue(undefined),
|
|
41
|
-
} as any;
|
|
42
|
-
|
|
43
|
-
vi.mocked(fs.open).mockResolvedValue(mockFd);
|
|
44
|
-
|
|
45
|
-
const tempFile = await SecureTempFile.create('test', '.txt');
|
|
46
|
-
|
|
47
|
-
expect(tempFile).toBeDefined();
|
|
48
|
-
expect(tempFile.path).toContain('/tmp/test_');
|
|
49
|
-
expect(tempFile.path).toContain('.txt');
|
|
50
|
-
expect(fs.open).toHaveBeenCalledWith(
|
|
51
|
-
expect.stringContaining('/tmp/test_'),
|
|
52
|
-
'wx',
|
|
53
|
-
0o600
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should use custom prefix and extension', async () => {
|
|
58
|
-
const mockFd = {
|
|
59
|
-
writeFile: vi.fn(),
|
|
60
|
-
readFile: vi.fn(),
|
|
61
|
-
close: vi.fn(),
|
|
62
|
-
} as any;
|
|
63
|
-
|
|
64
|
-
vi.mocked(fs.open).mockResolvedValue(mockFd);
|
|
65
|
-
|
|
66
|
-
const tempFile = await SecureTempFile.create('custom-prefix', '.md');
|
|
67
|
-
|
|
68
|
-
expect(tempFile.path).toContain('custom-prefix_');
|
|
69
|
-
expect(tempFile.path).toContain('.md');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('should handle file creation errors', async () => {
|
|
73
|
-
const error: any = new Error('Generic error');
|
|
74
|
-
vi.mocked(fs.open).mockRejectedValue(error);
|
|
75
|
-
|
|
76
|
-
await expect(SecureTempFile.create('test', '.txt'))
|
|
77
|
-
.rejects.toThrow('Failed to create temporary file');
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should throw error if file exists', async () => {
|
|
81
|
-
const error: any = new Error('File exists');
|
|
82
|
-
error.code = 'EEXIST';
|
|
83
|
-
vi.mocked(fs.open).mockRejectedValue(error);
|
|
84
|
-
|
|
85
|
-
await expect(SecureTempFile.create('test', '.txt'))
|
|
86
|
-
.rejects.toThrow('Temporary file already exists');
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
describe('writeContent', () => {
|
|
91
|
-
it('should write content to temp file', async () => {
|
|
92
|
-
const mockFd = {
|
|
93
|
-
writeFile: vi.fn().mockResolvedValue(undefined),
|
|
94
|
-
readFile: vi.fn(),
|
|
95
|
-
close: vi.fn(),
|
|
96
|
-
} as any;
|
|
97
|
-
|
|
98
|
-
vi.mocked(fs.open).mockResolvedValue(mockFd);
|
|
99
|
-
|
|
100
|
-
const tempFile = await SecureTempFile.create('test', '.txt');
|
|
101
|
-
await tempFile.writeContent('test content');
|
|
102
|
-
|
|
103
|
-
expect(mockFd.writeFile).toHaveBeenCalledWith('test content', 'utf8');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should throw if file is closed', async () => {
|
|
107
|
-
const mockFd = {
|
|
108
|
-
writeFile: vi.fn(),
|
|
109
|
-
readFile: vi.fn(),
|
|
110
|
-
close: vi.fn().mockResolvedValue(undefined),
|
|
111
|
-
} as any;
|
|
112
|
-
|
|
113
|
-
vi.mocked(fs.open).mockResolvedValue(mockFd);
|
|
114
|
-
|
|
115
|
-
const tempFile = await SecureTempFile.create('test', '.txt');
|
|
116
|
-
await tempFile.close();
|
|
117
|
-
|
|
118
|
-
await expect(tempFile.writeContent('content'))
|
|
119
|
-
.rejects.toThrow('Temp file is not available for writing');
|
|
120
|
-
});
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
describe('readContent', () => {
|
|
124
|
-
it('should read content from temp file', async () => {
|
|
125
|
-
const mockFd = {
|
|
126
|
-
writeFile: vi.fn(),
|
|
127
|
-
readFile: vi.fn().mockResolvedValue('test content'),
|
|
128
|
-
close: vi.fn(),
|
|
129
|
-
} as any;
|
|
130
|
-
|
|
131
|
-
vi.mocked(fs.open).mockResolvedValue(mockFd);
|
|
132
|
-
|
|
133
|
-
const tempFile = await SecureTempFile.create('test', '.txt');
|
|
134
|
-
const content = await tempFile.readContent();
|
|
135
|
-
|
|
136
|
-
expect(content).toBe('test content');
|
|
137
|
-
expect(mockFd.readFile).toHaveBeenCalledWith('utf8');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('should throw if file is closed', async () => {
|
|
141
|
-
const mockFd = {
|
|
142
|
-
writeFile: vi.fn(),
|
|
143
|
-
readFile: vi.fn(),
|
|
144
|
-
close: vi.fn().mockResolvedValue(undefined),
|
|
145
|
-
} as any;
|
|
146
|
-
|
|
147
|
-
vi.mocked(fs.open).mockResolvedValue(mockFd);
|
|
148
|
-
|
|
149
|
-
const tempFile = await SecureTempFile.create('test', '.txt');
|
|
150
|
-
await tempFile.close();
|
|
151
|
-
|
|
152
|
-
await expect(tempFile.readContent())
|
|
153
|
-
.rejects.toThrow('Temp file is not available for reading');
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
describe('cleanup', () => {
|
|
158
|
-
it('should close and unlink temp file', async () => {
|
|
159
|
-
const mockFd = {
|
|
160
|
-
writeFile: vi.fn(),
|
|
161
|
-
readFile: vi.fn(),
|
|
162
|
-
close: vi.fn().mockResolvedValue(undefined),
|
|
163
|
-
} as any;
|
|
164
|
-
|
|
165
|
-
vi.mocked(fs.open).mockResolvedValue(mockFd);
|
|
166
|
-
|
|
167
|
-
const tempFile = await SecureTempFile.create('test', '.txt');
|
|
168
|
-
const filePath = tempFile.path;
|
|
169
|
-
|
|
170
|
-
await tempFile.cleanup();
|
|
171
|
-
|
|
172
|
-
expect(mockFd.close).toHaveBeenCalled();
|
|
173
|
-
expect(fs.unlink).toHaveBeenCalledWith(filePath);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('should handle cleanup errors gracefully', async () => {
|
|
177
|
-
const mockFd = {
|
|
178
|
-
writeFile: vi.fn(),
|
|
179
|
-
readFile: vi.fn(),
|
|
180
|
-
close: vi.fn().mockResolvedValue(undefined),
|
|
181
|
-
} as any;
|
|
182
|
-
|
|
183
|
-
vi.mocked(fs.open).mockResolvedValue(mockFd);
|
|
184
|
-
vi.mocked(fs.unlink).mockRejectedValue(new Error('Unlink failed'));
|
|
185
|
-
|
|
186
|
-
const tempFile = await SecureTempFile.create('test', '.txt');
|
|
187
|
-
|
|
188
|
-
// Should not throw
|
|
189
|
-
await expect(tempFile.cleanup()).resolves.not.toThrow();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('should skip cleanup if already cleaned', async () => {
|
|
193
|
-
const mockFd = {
|
|
194
|
-
writeFile: vi.fn(),
|
|
195
|
-
readFile: vi.fn(),
|
|
196
|
-
close: vi.fn().mockResolvedValue(undefined),
|
|
197
|
-
} as any;
|
|
198
|
-
|
|
199
|
-
vi.mocked(fs.open).mockResolvedValue(mockFd);
|
|
200
|
-
|
|
201
|
-
const tempFile = await SecureTempFile.create('test', '.txt');
|
|
202
|
-
|
|
203
|
-
await tempFile.cleanup();
|
|
204
|
-
vi.mocked(fs.unlink).mockClear();
|
|
205
|
-
|
|
206
|
-
await tempFile.cleanup();
|
|
207
|
-
|
|
208
|
-
expect(fs.unlink).not.toHaveBeenCalled();
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it('should throw error when accessing path after cleanup', async () => {
|
|
212
|
-
const mockFd = {
|
|
213
|
-
writeFile: vi.fn(),
|
|
214
|
-
readFile: vi.fn(),
|
|
215
|
-
close: vi.fn().mockResolvedValue(undefined),
|
|
216
|
-
} as any;
|
|
217
|
-
|
|
218
|
-
vi.mocked(fs.open).mockResolvedValue(mockFd);
|
|
219
|
-
|
|
220
|
-
const tempFile = await SecureTempFile.create('test', '.txt');
|
|
221
|
-
await tempFile.cleanup();
|
|
222
|
-
|
|
223
|
-
expect(() => tempFile.path).toThrow('Temp file has been cleaned up');
|
|
224
|
-
});
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
describe('createSecureTempFile', () => {
|
|
229
|
-
it('should create and close temp file', async () => {
|
|
230
|
-
const mockFd = {
|
|
231
|
-
writeFile: vi.fn(),
|
|
232
|
-
readFile: vi.fn(),
|
|
233
|
-
close: vi.fn().mockResolvedValue(undefined),
|
|
234
|
-
} as any;
|
|
235
|
-
|
|
236
|
-
vi.mocked(os.tmpdir).mockReturnValue('/tmp');
|
|
237
|
-
vi.mocked(fs.access).mockResolvedValue(undefined);
|
|
238
|
-
vi.mocked(fs.open).mockResolvedValue(mockFd);
|
|
239
|
-
|
|
240
|
-
const path = await createSecureTempFile('test', '.txt');
|
|
241
|
-
|
|
242
|
-
expect(path).toContain('/tmp/test_');
|
|
243
|
-
expect(mockFd.close).toHaveBeenCalled();
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
describe('cleanupTempFile', () => {
|
|
248
|
-
it('should unlink the file', async () => {
|
|
249
|
-
vi.mocked(fs.unlink).mockResolvedValue(undefined);
|
|
250
|
-
|
|
251
|
-
await cleanupTempFile('/tmp/test.txt');
|
|
252
|
-
|
|
253
|
-
expect(fs.unlink).toHaveBeenCalledWith('/tmp/test.txt');
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('should handle ENOENT errors gracefully', async () => {
|
|
257
|
-
const error: any = new Error('ENOENT');
|
|
258
|
-
error.code = 'ENOENT';
|
|
259
|
-
vi.mocked(fs.unlink).mockRejectedValue(error);
|
|
260
|
-
|
|
261
|
-
await expect(cleanupTempFile('/tmp/test.txt')).resolves.not.toThrow();
|
|
262
|
-
});
|
|
263
|
-
});
|
|
264
|
-
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { getUserChoice, STANDARD_CHOICES } from '../src/interactive';
|
|
3
|
-
|
|
4
|
-
// Mock logger
|
|
5
|
-
const mockLoggerInstance = {
|
|
6
|
-
info: vi.fn(),
|
|
7
|
-
error: vi.fn(),
|
|
8
|
-
warn: vi.fn(),
|
|
9
|
-
debug: vi.fn(),
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
vi.mock('../src/logger', () => ({
|
|
13
|
-
getLogger: vi.fn(() => mockLoggerInstance),
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
describe('getUserChoice', () => {
|
|
17
|
-
let originalIsTTY: boolean | undefined;
|
|
18
|
-
let originalSetRawMode: any;
|
|
19
|
-
let originalResume: any;
|
|
20
|
-
let originalPause: any;
|
|
21
|
-
let originalRef: any;
|
|
22
|
-
let originalUnref: any;
|
|
23
|
-
let originalOn: any;
|
|
24
|
-
let originalRemoveListener: any;
|
|
25
|
-
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
vi.clearAllMocks();
|
|
28
|
-
|
|
29
|
-
// Save original stdin methods
|
|
30
|
-
originalIsTTY = process.stdin.isTTY;
|
|
31
|
-
originalSetRawMode = process.stdin.setRawMode;
|
|
32
|
-
originalResume = process.stdin.resume;
|
|
33
|
-
originalPause = process.stdin.pause;
|
|
34
|
-
originalRef = process.stdin.ref;
|
|
35
|
-
originalUnref = process.stdin.unref;
|
|
36
|
-
originalOn = process.stdin.on;
|
|
37
|
-
originalRemoveListener = process.stdin.removeListener;
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
afterEach(() => {
|
|
41
|
-
// Restore stdin
|
|
42
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true, writable: true });
|
|
43
|
-
process.stdin.setRawMode = originalSetRawMode;
|
|
44
|
-
process.stdin.resume = originalResume;
|
|
45
|
-
process.stdin.pause = originalPause;
|
|
46
|
-
process.stdin.ref = originalRef;
|
|
47
|
-
process.stdin.unref = originalUnref;
|
|
48
|
-
process.stdin.on = originalOn;
|
|
49
|
-
process.stdin.removeListener = originalRemoveListener;
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should return default when stdin is not TTY', async () => {
|
|
53
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true, writable: true });
|
|
54
|
-
|
|
55
|
-
const result = await getUserChoice('Test prompt', [
|
|
56
|
-
{ key: 'a', label: 'Option A' },
|
|
57
|
-
{ key: 'b', label: 'Option B' },
|
|
58
|
-
]);
|
|
59
|
-
|
|
60
|
-
expect(result).toBe('s');
|
|
61
|
-
expect(mockLoggerInstance.error).toHaveBeenCalledWith(expect.stringContaining('STDIN is piped'));
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should show error suggestions when not TTY', async () => {
|
|
65
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true, writable: true });
|
|
66
|
-
|
|
67
|
-
await getUserChoice(
|
|
68
|
-
'Test prompt',
|
|
69
|
-
[{ key: 'a', label: 'Option A' }],
|
|
70
|
-
{ nonTtyErrorSuggestions: ['Use --dry-run', 'Run in terminal'] }
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
expect(mockLoggerInstance.error).toHaveBeenCalledWith(' • Use --dry-run');
|
|
74
|
-
expect(mockLoggerInstance.error).toHaveBeenCalledWith(' • Run in terminal');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('should display prompt and choices', async () => {
|
|
78
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true, writable: true });
|
|
79
|
-
|
|
80
|
-
// Mock stdin methods
|
|
81
|
-
process.stdin.setRawMode = vi.fn();
|
|
82
|
-
process.stdin.resume = vi.fn();
|
|
83
|
-
process.stdin.pause = vi.fn();
|
|
84
|
-
process.stdin.ref = vi.fn();
|
|
85
|
-
process.stdin.unref = vi.fn();
|
|
86
|
-
process.stdin.removeListener = vi.fn();
|
|
87
|
-
|
|
88
|
-
let dataCallback: any;
|
|
89
|
-
process.stdin.on = vi.fn((event, callback) => {
|
|
90
|
-
if (event === 'data') {
|
|
91
|
-
dataCallback = callback;
|
|
92
|
-
// Simulate user pressing 'a'
|
|
93
|
-
setTimeout(() => dataCallback(Buffer.from('a')), 10);
|
|
94
|
-
}
|
|
95
|
-
return process.stdin;
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
const result = await getUserChoice('What to do?', [
|
|
99
|
-
{ key: 'a', label: 'Action A' },
|
|
100
|
-
{ key: 'b', label: 'Action B' },
|
|
101
|
-
]);
|
|
102
|
-
|
|
103
|
-
expect(result).toBe('a');
|
|
104
|
-
expect(mockLoggerInstance.info).toHaveBeenCalledWith('What to do?');
|
|
105
|
-
expect(mockLoggerInstance.info).toHaveBeenCalledWith(' [a] Action A');
|
|
106
|
-
expect(mockLoggerInstance.info).toHaveBeenCalledWith(' [b] Action B');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('should handle user selecting first choice', async () => {
|
|
110
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true, writable: true });
|
|
111
|
-
|
|
112
|
-
process.stdin.setRawMode = vi.fn();
|
|
113
|
-
process.stdin.resume = vi.fn();
|
|
114
|
-
process.stdin.pause = vi.fn();
|
|
115
|
-
process.stdin.ref = vi.fn();
|
|
116
|
-
process.stdin.unref = vi.fn();
|
|
117
|
-
process.stdin.removeListener = vi.fn();
|
|
118
|
-
|
|
119
|
-
process.stdin.on = vi.fn((event, callback) => {
|
|
120
|
-
if (event === 'data') {
|
|
121
|
-
setTimeout(() => callback(Buffer.from('c')), 10);
|
|
122
|
-
}
|
|
123
|
-
return process.stdin;
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const result = await getUserChoice('Confirm?', [STANDARD_CHOICES.CONFIRM, STANDARD_CHOICES.SKIP]);
|
|
127
|
-
|
|
128
|
-
expect(result).toBe('c');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should cleanup stdin on completion', async () => {
|
|
132
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true, writable: true });
|
|
133
|
-
|
|
134
|
-
process.stdin.setRawMode = vi.fn();
|
|
135
|
-
process.stdin.resume = vi.fn();
|
|
136
|
-
process.stdin.pause = vi.fn();
|
|
137
|
-
process.stdin.ref = vi.fn();
|
|
138
|
-
process.stdin.unref = vi.fn();
|
|
139
|
-
const mockRemoveListener = vi.fn();
|
|
140
|
-
process.stdin.removeListener = mockRemoveListener;
|
|
141
|
-
|
|
142
|
-
process.stdin.on = vi.fn((event, callback) => {
|
|
143
|
-
if (event === 'data') {
|
|
144
|
-
setTimeout(() => callback(Buffer.from('s')), 10);
|
|
145
|
-
}
|
|
146
|
-
return process.stdin;
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
await getUserChoice('Test?', [STANDARD_CHOICES.SKIP]);
|
|
150
|
-
|
|
151
|
-
expect(mockRemoveListener).toHaveBeenCalled();
|
|
152
|
-
expect(process.stdin.pause).toHaveBeenCalled();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('should handle errors during input setup', async () => {
|
|
156
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true, writable: true });
|
|
157
|
-
|
|
158
|
-
process.stdin.setRawMode = vi.fn(() => {
|
|
159
|
-
throw new Error('Setup failed');
|
|
160
|
-
});
|
|
161
|
-
process.stdin.resume = vi.fn();
|
|
162
|
-
process.stdin.pause = vi.fn();
|
|
163
|
-
process.stdin.ref = vi.fn();
|
|
164
|
-
process.stdin.unref = vi.fn();
|
|
165
|
-
process.stdin.removeListener = vi.fn();
|
|
166
|
-
process.stdin.on = vi.fn();
|
|
167
|
-
|
|
168
|
-
await expect(
|
|
169
|
-
getUserChoice('Test?', [{ key: 'a', label: 'Option A' }])
|
|
170
|
-
).rejects.toThrow('Setup failed');
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { getUserTextInput } from '../src/interactive';
|
|
3
|
-
|
|
4
|
-
// Mock logger
|
|
5
|
-
const mockLoggerInstance = {
|
|
6
|
-
info: vi.fn(),
|
|
7
|
-
error: vi.fn(),
|
|
8
|
-
warn: vi.fn(),
|
|
9
|
-
debug: vi.fn(),
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
vi.mock('../src/logger', () => ({
|
|
13
|
-
getLogger: vi.fn(() => mockLoggerInstance),
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
describe('getUserTextInput', () => {
|
|
17
|
-
let originalIsTTY: boolean | undefined;
|
|
18
|
-
let originalSetEncoding: any;
|
|
19
|
-
let originalResume: any;
|
|
20
|
-
let originalPause: any;
|
|
21
|
-
let originalRef: any;
|
|
22
|
-
let originalUnref: any;
|
|
23
|
-
let originalOn: any;
|
|
24
|
-
let originalRemoveListener: any;
|
|
25
|
-
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
vi.clearAllMocks();
|
|
28
|
-
|
|
29
|
-
// Save original stdin methods
|
|
30
|
-
originalIsTTY = process.stdin.isTTY;
|
|
31
|
-
originalSetEncoding = process.stdin.setEncoding;
|
|
32
|
-
originalResume = process.stdin.resume;
|
|
33
|
-
originalPause = process.stdin.pause;
|
|
34
|
-
originalRef = process.stdin.ref;
|
|
35
|
-
originalUnref = process.stdin.unref;
|
|
36
|
-
originalOn = process.stdin.on;
|
|
37
|
-
originalRemoveListener = process.stdin.removeListener;
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
afterEach(() => {
|
|
41
|
-
// Restore stdin
|
|
42
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: originalIsTTY, configurable: true, writable: true });
|
|
43
|
-
process.stdin.setEncoding = originalSetEncoding;
|
|
44
|
-
process.stdin.resume = originalResume;
|
|
45
|
-
process.stdin.pause = originalPause;
|
|
46
|
-
process.stdin.ref = originalRef;
|
|
47
|
-
process.stdin.unref = originalUnref;
|
|
48
|
-
process.stdin.on = originalOn;
|
|
49
|
-
process.stdin.removeListener = originalRemoveListener;
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should throw error when stdin is not TTY', async () => {
|
|
53
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true, writable: true });
|
|
54
|
-
|
|
55
|
-
await expect(
|
|
56
|
-
getUserTextInput('Enter text:')
|
|
57
|
-
).rejects.toThrow('Interactive text input requires a terminal');
|
|
58
|
-
|
|
59
|
-
expect(mockLoggerInstance.error).toHaveBeenCalledWith(expect.stringContaining('STDIN is piped'));
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should show error suggestions when not TTY', async () => {
|
|
63
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true, writable: true });
|
|
64
|
-
|
|
65
|
-
await expect(
|
|
66
|
-
getUserTextInput('Enter text:', {
|
|
67
|
-
nonTtyErrorSuggestions: ['Use file input', 'Run interactively'],
|
|
68
|
-
})
|
|
69
|
-
).rejects.toThrow();
|
|
70
|
-
|
|
71
|
-
expect(mockLoggerInstance.error).toHaveBeenCalledWith(' • Use file input');
|
|
72
|
-
expect(mockLoggerInstance.error).toHaveBeenCalledWith(' • Run interactively');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should accept text input from user', async () => {
|
|
76
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true, writable: true });
|
|
77
|
-
|
|
78
|
-
process.stdin.setEncoding = vi.fn();
|
|
79
|
-
process.stdin.resume = vi.fn();
|
|
80
|
-
process.stdin.pause = vi.fn();
|
|
81
|
-
process.stdin.ref = vi.fn();
|
|
82
|
-
process.stdin.unref = vi.fn();
|
|
83
|
-
process.stdin.removeListener = vi.fn();
|
|
84
|
-
|
|
85
|
-
process.stdin.on = vi.fn((event, callback) => {
|
|
86
|
-
if (event === 'data') {
|
|
87
|
-
setTimeout(() => callback('user input text\n'), 10);
|
|
88
|
-
}
|
|
89
|
-
return process.stdin;
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
const result = await getUserTextInput('Enter feedback:');
|
|
93
|
-
|
|
94
|
-
expect(result).toBe('user input text');
|
|
95
|
-
expect(mockLoggerInstance.info).toHaveBeenCalledWith('Enter feedback:');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should reject empty input', async () => {
|
|
99
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true, writable: true });
|
|
100
|
-
|
|
101
|
-
process.stdin.setEncoding = vi.fn();
|
|
102
|
-
process.stdin.resume = vi.fn();
|
|
103
|
-
process.stdin.pause = vi.fn();
|
|
104
|
-
process.stdin.ref = vi.fn();
|
|
105
|
-
process.stdin.unref = vi.fn();
|
|
106
|
-
process.stdin.removeListener = vi.fn();
|
|
107
|
-
|
|
108
|
-
process.stdin.on = vi.fn((event, callback) => {
|
|
109
|
-
if (event === 'data') {
|
|
110
|
-
setTimeout(() => callback('\n'), 10);
|
|
111
|
-
}
|
|
112
|
-
return process.stdin;
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
await expect(
|
|
116
|
-
getUserTextInput('Enter text:')
|
|
117
|
-
).rejects.toThrow('Empty input received');
|
|
118
|
-
|
|
119
|
-
expect(mockLoggerInstance.warn).toHaveBeenCalledWith(expect.stringContaining('Empty input'));
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('should cleanup stdin on completion', async () => {
|
|
123
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true, writable: true });
|
|
124
|
-
|
|
125
|
-
process.stdin.setEncoding = vi.fn();
|
|
126
|
-
process.stdin.resume = vi.fn();
|
|
127
|
-
const mockPause = vi.fn();
|
|
128
|
-
process.stdin.pause = mockPause;
|
|
129
|
-
process.stdin.ref = vi.fn();
|
|
130
|
-
process.stdin.unref = vi.fn();
|
|
131
|
-
const mockRemoveListener = vi.fn();
|
|
132
|
-
process.stdin.removeListener = mockRemoveListener;
|
|
133
|
-
|
|
134
|
-
process.stdin.on = vi.fn((event, callback) => {
|
|
135
|
-
if (event === 'data') {
|
|
136
|
-
setTimeout(() => callback('text\n'), 10);
|
|
137
|
-
}
|
|
138
|
-
return process.stdin;
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
await getUserTextInput('Enter text:');
|
|
142
|
-
|
|
143
|
-
expect(mockRemoveListener).toHaveBeenCalled();
|
|
144
|
-
expect(mockPause).toHaveBeenCalled();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('should handle input processing errors', async () => {
|
|
148
|
-
Object.defineProperty(process.stdin, 'isTTY', { value: true, configurable: true, writable: true });
|
|
149
|
-
|
|
150
|
-
process.stdin.setEncoding = vi.fn();
|
|
151
|
-
process.stdin.resume = vi.fn();
|
|
152
|
-
process.stdin.pause = vi.fn();
|
|
153
|
-
process.stdin.ref = vi.fn();
|
|
154
|
-
process.stdin.unref = vi.fn();
|
|
155
|
-
process.stdin.removeListener = vi.fn();
|
|
156
|
-
|
|
157
|
-
process.stdin.on = vi.fn((event, callback) => {
|
|
158
|
-
if (event === 'data') {
|
|
159
|
-
setTimeout(() => {
|
|
160
|
-
try {
|
|
161
|
-
callback(null); // Invalid input
|
|
162
|
-
} catch (e) {
|
|
163
|
-
// Expected
|
|
164
|
-
}
|
|
165
|
-
}, 10);
|
|
166
|
-
}
|
|
167
|
-
return process.stdin;
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
// This test verifies error handling exists
|
|
171
|
-
expect(process.stdin.on).toBeDefined();
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
|
|
2
|
-
import { requireTTY } from '../src/interactive';
|
|
3
|
-
import type { Logger } from '../src/types';
|
|
4
|
-
|
|
5
|
-
// Mock logger for tests
|
|
6
|
-
vi.mock('../src/logger', () => ({
|
|
7
|
-
getLogger: vi.fn(() => ({
|
|
8
|
-
info: vi.fn(),
|
|
9
|
-
error: vi.fn(),
|
|
10
|
-
warn: vi.fn(),
|
|
11
|
-
debug: vi.fn()
|
|
12
|
-
}))
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
describe('Interactive Utility Module', () => {
|
|
16
|
-
let originalIsTTY: boolean | undefined;
|
|
17
|
-
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
vi.clearAllMocks();
|
|
20
|
-
originalIsTTY = process.stdin.isTTY;
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(() => {
|
|
24
|
-
if (originalIsTTY !== undefined) {
|
|
25
|
-
Object.defineProperty(process.stdin, 'isTTY', {
|
|
26
|
-
value: originalIsTTY,
|
|
27
|
-
configurable: true,
|
|
28
|
-
writable: true
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe('requireTTY', () => {
|
|
34
|
-
it('should not throw when stdin is a TTY', () => {
|
|
35
|
-
Object.defineProperty(process.stdin, 'isTTY', {
|
|
36
|
-
value: true,
|
|
37
|
-
configurable: true,
|
|
38
|
-
writable: true
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
expect(() => requireTTY()).not.toThrow();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('should throw when stdin is not a TTY', () => {
|
|
45
|
-
Object.defineProperty(process.stdin, 'isTTY', {
|
|
46
|
-
value: false,
|
|
47
|
-
configurable: true,
|
|
48
|
-
writable: true
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
expect(() => requireTTY()).toThrow('Interactive mode requires a terminal');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should throw custom error message', () => {
|
|
55
|
-
Object.defineProperty(process.stdin, 'isTTY', {
|
|
56
|
-
value: false,
|
|
57
|
-
configurable: true,
|
|
58
|
-
writable: true
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
expect(() => requireTTY('Custom error message')).toThrow('Custom error message');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('should accept optional logger', () => {
|
|
65
|
-
const mockLogger: Logger = {
|
|
66
|
-
info: vi.fn(),
|
|
67
|
-
error: vi.fn(),
|
|
68
|
-
warn: vi.fn(),
|
|
69
|
-
debug: vi.fn()
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
Object.defineProperty(process.stdin, 'isTTY', {
|
|
73
|
-
value: false,
|
|
74
|
-
configurable: true,
|
|
75
|
-
writable: true
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
expect(() => requireTTY('Test error', mockLogger)).toThrow();
|
|
79
|
-
expect(mockLogger.error).toHaveBeenCalled();
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe('STANDARD_CHOICES', () => {
|
|
84
|
-
it('should export standard choice constants', async () => {
|
|
85
|
-
const { STANDARD_CHOICES } = await import('../src/interactive');
|
|
86
|
-
|
|
87
|
-
expect(STANDARD_CHOICES.CONFIRM).toEqual({ key: 'c', label: 'Confirm and proceed' });
|
|
88
|
-
expect(STANDARD_CHOICES.EDIT).toEqual({ key: 'e', label: 'Edit in editor' });
|
|
89
|
-
expect(STANDARD_CHOICES.SKIP).toEqual({ key: 's', label: 'Skip and abort' });
|
|
90
|
-
expect(STANDARD_CHOICES.IMPROVE).toEqual({ key: 'i', label: 'Improve with LLM feedback' });
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|