hone-ai 0.5.0 → 0.10.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.
@@ -1,281 +1,284 @@
1
- import { describe, expect, test, beforeEach, mock } from 'bun:test';
2
- import {
3
- formatError,
1
+ import { describe, expect, test, beforeEach, mock } from 'bun:test'
2
+ import {
3
+ formatError,
4
4
  isNetworkError,
5
5
  isRateLimitError,
6
6
  isModelUnavailableError,
7
7
  parseAgentError,
8
8
  retryWithBackoff,
9
9
  HoneError,
10
- ErrorMessages
11
- } from './errors';
10
+ ErrorMessages,
11
+ } from './errors'
12
12
 
13
13
  describe('formatError', () => {
14
14
  test('formats error with message only', () => {
15
- const result = formatError('Something went wrong');
16
- expect(result).toBe('✗ Something went wrong');
17
- });
18
-
15
+ const result = formatError('Something went wrong')
16
+ expect(result).toBe('✗ Something went wrong')
17
+ })
18
+
19
19
  test('formats error with message and details', () => {
20
- const result = formatError('Something went wrong', 'More details here');
21
- expect(result).toBe('✗ Something went wrong\n\nMore details here');
22
- });
23
- });
20
+ const result = formatError('Something went wrong', 'More details here')
21
+ expect(result).toBe('✗ Something went wrong\n\nMore details here')
22
+ })
23
+ })
24
24
 
25
25
  describe('isNetworkError', () => {
26
26
  test('returns false for non-errors', () => {
27
- expect(isNetworkError(null)).toBe(false);
28
- expect(isNetworkError(undefined)).toBe(false);
29
- expect(isNetworkError('string')).toBe(false);
30
- expect(isNetworkError(123)).toBe(false);
31
- });
32
-
27
+ expect(isNetworkError(null)).toBe(false)
28
+ expect(isNetworkError(undefined)).toBe(false)
29
+ expect(isNetworkError('string')).toBe(false)
30
+ expect(isNetworkError(123)).toBe(false)
31
+ })
32
+
33
33
  test('detects ECONNREFUSED', () => {
34
- const error = new Error('connect ECONNREFUSED');
35
- expect(isNetworkError(error)).toBe(true);
36
- });
37
-
34
+ const error = new Error('connect ECONNREFUSED')
35
+ expect(isNetworkError(error)).toBe(true)
36
+ })
37
+
38
38
  test('detects ETIMEDOUT', () => {
39
- const error = { code: 'ETIMEDOUT', message: 'timeout' };
40
- expect(isNetworkError(error)).toBe(true);
41
- });
42
-
39
+ const error = { code: 'ETIMEDOUT', message: 'timeout' }
40
+ expect(isNetworkError(error)).toBe(true)
41
+ })
42
+
43
43
  test('detects fetch failed', () => {
44
- const error = new Error('fetch failed');
45
- expect(isNetworkError(error)).toBe(true);
46
- });
47
-
44
+ const error = new Error('fetch failed')
45
+ expect(isNetworkError(error)).toBe(true)
46
+ })
47
+
48
48
  test('detects network in message', () => {
49
- const error = new Error('Network request failed');
50
- expect(isNetworkError(error)).toBe(true);
51
- });
52
-
49
+ const error = new Error('Network request failed')
50
+ expect(isNetworkError(error)).toBe(true)
51
+ })
52
+
53
53
  test('returns false for non-network errors', () => {
54
- const error = new Error('Validation failed');
55
- expect(isNetworkError(error)).toBe(false);
56
- });
57
- });
54
+ const error = new Error('Validation failed')
55
+ expect(isNetworkError(error)).toBe(false)
56
+ })
57
+ })
58
58
 
59
59
  describe('retryWithBackoff', () => {
60
60
  beforeEach(() => {
61
61
  // Clear any timers
62
- mock.restore();
63
- });
64
-
62
+ mock.restore()
63
+ })
64
+
65
65
  test('succeeds on first try', async () => {
66
- const fn = mock(() => Promise.resolve('success'));
67
- const result = await retryWithBackoff(fn);
68
-
69
- expect(result).toBe('success');
70
- expect(fn).toHaveBeenCalledTimes(1);
71
- });
72
-
66
+ const fn = mock(() => Promise.resolve('success'))
67
+ const result = await retryWithBackoff(fn)
68
+
69
+ expect(result).toBe('success')
70
+ expect(fn).toHaveBeenCalledTimes(1)
71
+ })
72
+
73
73
  test('retries on network error and succeeds', async () => {
74
- let attempts = 0;
74
+ let attempts = 0
75
75
  const fn = mock(() => {
76
- attempts++;
76
+ attempts++
77
77
  if (attempts < 2) {
78
- return Promise.reject(new Error('Network timeout'));
78
+ return Promise.reject(new Error('Network timeout'))
79
79
  }
80
- return Promise.resolve('success');
81
- });
82
-
83
- const result = await retryWithBackoff(fn, {
80
+ return Promise.resolve('success')
81
+ })
82
+
83
+ const result = await retryWithBackoff(fn, {
84
84
  initialDelay: 10,
85
- maxDelay: 50
86
- });
87
-
88
- expect(result).toBe('success');
89
- expect(fn).toHaveBeenCalledTimes(2);
90
- });
91
-
85
+ maxDelay: 50,
86
+ })
87
+
88
+ expect(result).toBe('success')
89
+ expect(fn).toHaveBeenCalledTimes(2)
90
+ })
91
+
92
92
  test('throws after max retries', async () => {
93
- const fn = mock(() => Promise.reject(new Error('Network timeout')));
94
-
93
+ const fn = mock(() => Promise.reject(new Error('Network timeout')))
94
+
95
95
  await expect(
96
- retryWithBackoff(fn, {
96
+ retryWithBackoff(fn, {
97
97
  maxRetries: 2,
98
98
  initialDelay: 10,
99
- maxDelay: 50
99
+ maxDelay: 50,
100
100
  })
101
- ).rejects.toThrow('Network timeout');
102
-
103
- expect(fn).toHaveBeenCalledTimes(3); // Initial + 2 retries
104
- });
105
-
101
+ ).rejects.toThrow('Network timeout')
102
+
103
+ expect(fn).toHaveBeenCalledTimes(3) // Initial + 2 retries
104
+ })
105
+
106
106
  test('does not retry non-network errors', async () => {
107
- const fn = mock(() => Promise.reject(new Error('Validation failed')));
108
-
107
+ const fn = mock(() => Promise.reject(new Error('Validation failed')))
108
+
109
109
  await expect(
110
- retryWithBackoff(fn, {
110
+ retryWithBackoff(fn, {
111
111
  initialDelay: 10,
112
- maxDelay: 50
112
+ maxDelay: 50,
113
113
  })
114
- ).rejects.toThrow('Validation failed');
115
-
116
- expect(fn).toHaveBeenCalledTimes(1); // Only initial attempt
117
- });
118
-
114
+ ).rejects.toThrow('Validation failed')
115
+
116
+ expect(fn).toHaveBeenCalledTimes(1) // Only initial attempt
117
+ })
118
+
119
119
  test('respects custom shouldRetry predicate', async () => {
120
- const fn = mock(() => Promise.reject(new Error('Custom error')));
121
-
122
- const result = await retryWithBackoff(fn, {
120
+ const fn = mock(() => Promise.reject(new Error('Custom error')))
121
+
122
+ const result = await retryWithBackoff(fn, {
123
123
  maxRetries: 1,
124
124
  initialDelay: 10,
125
125
  shouldRetry: (error: unknown) => {
126
- return error instanceof Error && error.message === 'Custom error';
127
- }
128
- }).catch(() => 'caught');
129
-
130
- expect(result).toBe('caught');
131
- expect(fn).toHaveBeenCalledTimes(2); // Initial + 1 retry
132
- });
133
- });
126
+ return error instanceof Error && error.message === 'Custom error'
127
+ },
128
+ }).catch(() => 'caught')
129
+
130
+ expect(result).toBe('caught')
131
+ expect(fn).toHaveBeenCalledTimes(2) // Initial + 1 retry
132
+ })
133
+ })
134
134
 
135
135
  describe('HoneError', () => {
136
136
  test('creates error with message', () => {
137
- const error = new HoneError('Test error');
138
- expect(error.message).toBe('Test error');
139
- expect(error.exitCode).toBe(1);
140
- expect(error.name).toBe('HoneError');
141
- });
142
-
137
+ const error = new HoneError('Test error')
138
+ expect(error.message).toBe('Test error')
139
+ expect(error.exitCode).toBe(1)
140
+ expect(error.name).toBe('HoneError')
141
+ })
142
+
143
143
  test('creates error with custom exit code', () => {
144
- const error = new HoneError('Test error', 2);
145
- expect(error.exitCode).toBe(2);
146
- });
147
- });
144
+ const error = new HoneError('Test error', 2)
145
+ expect(error.exitCode).toBe(2)
146
+ })
147
+ })
148
148
 
149
149
  describe('isRateLimitError', () => {
150
150
  test('detects rate limit variations', () => {
151
- expect(isRateLimitError('Rate limit exceeded')).toBe(true);
152
- expect(isRateLimitError('rate_limit error')).toBe(true);
153
- expect(isRateLimitError('429 Too Many Requests')).toBe(true);
154
- expect(isRateLimitError('Quota exceeded')).toBe(true);
155
- });
156
-
151
+ expect(isRateLimitError('Rate limit exceeded')).toBe(true)
152
+ expect(isRateLimitError('rate_limit error')).toBe(true)
153
+ expect(isRateLimitError('429 Too Many Requests')).toBe(true)
154
+ expect(isRateLimitError('Quota exceeded')).toBe(true)
155
+ })
156
+
157
157
  test('returns false for non-rate-limit errors', () => {
158
- expect(isRateLimitError('Model not found')).toBe(false);
159
- expect(isRateLimitError('Network error')).toBe(false);
160
- });
161
- });
158
+ expect(isRateLimitError('Model not found')).toBe(false)
159
+ expect(isRateLimitError('Network error')).toBe(false)
160
+ })
161
+ })
162
162
 
163
163
  describe('isModelUnavailableError', () => {
164
164
  test('detects model unavailability', () => {
165
- expect(isModelUnavailableError('Model not found')).toBe(true);
166
- expect(isModelUnavailableError('Invalid model name')).toBe(true);
167
- expect(isModelUnavailableError('404 Not Found')).toBe(true);
168
- expect(isModelUnavailableError('Unknown model')).toBe(true);
169
- });
170
-
165
+ expect(isModelUnavailableError('Model not found')).toBe(true)
166
+ expect(isModelUnavailableError('Invalid model name')).toBe(true)
167
+ expect(isModelUnavailableError('404 Not Found')).toBe(true)
168
+ expect(isModelUnavailableError('Unknown model')).toBe(true)
169
+ })
170
+
171
171
  test('returns false for non-model errors', () => {
172
- expect(isModelUnavailableError('Rate limit exceeded')).toBe(false);
173
- expect(isModelUnavailableError('Network timeout')).toBe(false);
174
- });
175
- });
172
+ expect(isModelUnavailableError('Rate limit exceeded')).toBe(false)
173
+ expect(isModelUnavailableError('Network timeout')).toBe(false)
174
+ })
175
+ })
176
176
 
177
177
  describe('parseAgentError', () => {
178
178
  test('identifies network errors', () => {
179
- const result = parseAgentError('ECONNREFUSED', 1);
180
- expect(result.type).toBe('network');
181
- expect(result.retryable).toBe(true);
182
- });
183
-
179
+ const result = parseAgentError('ECONNREFUSED', 1)
180
+ expect(result.type).toBe('network')
181
+ expect(result.retryable).toBe(true)
182
+ })
183
+
184
184
  test('identifies rate limit errors', () => {
185
- const result = parseAgentError('Rate limit exceeded', 1);
186
- expect(result.type).toBe('rate_limit');
187
- expect(result.retryable).toBe(false);
188
- });
189
-
185
+ const result = parseAgentError('Rate limit exceeded', 1)
186
+ expect(result.type).toBe('rate_limit')
187
+ expect(result.retryable).toBe(false)
188
+ })
189
+
190
190
  test('extracts retry-after from rate limit errors', () => {
191
- const result = parseAgentError('Rate limit exceeded. Retry after 60 seconds', 1);
192
- expect(result.type).toBe('rate_limit');
193
- expect(result.retryAfter).toBe(60);
194
- });
195
-
191
+ const result = parseAgentError('Rate limit exceeded. Retry after 60 seconds', 1)
192
+ expect(result.type).toBe('rate_limit')
193
+ expect(result.retryAfter).toBe(60)
194
+ })
195
+
196
196
  test('identifies model unavailable errors', () => {
197
- const result = parseAgentError('Model not found', 1);
198
- expect(result.type).toBe('model_unavailable');
199
- expect(result.retryable).toBe(false);
200
- });
201
-
197
+ const result = parseAgentError('Model not found', 1)
198
+ expect(result.type).toBe('model_unavailable')
199
+ expect(result.retryable).toBe(false)
200
+ })
201
+
202
202
  test('identifies spawn failures', () => {
203
- const result = parseAgentError('ENOENT', 1);
204
- expect(result.type).toBe('spawn_failed');
205
- expect(result.retryable).toBe(false);
206
- });
207
-
203
+ const result = parseAgentError('ENOENT', 1)
204
+ expect(result.type).toBe('spawn_failed')
205
+ expect(result.retryable).toBe(false)
206
+ })
207
+
208
208
  test('returns unknown for other errors', () => {
209
- const result = parseAgentError('Some random error', 1);
210
- expect(result.type).toBe('unknown');
211
- expect(result.retryable).toBe(false);
212
- });
213
- });
209
+ const result = parseAgentError('Some random error', 1)
210
+ expect(result.type).toBe('unknown')
211
+ expect(result.retryable).toBe(false)
212
+ })
213
+ })
214
214
 
215
215
  describe('ErrorMessages', () => {
216
216
  test('MISSING_API_KEY has correct format', () => {
217
- const { message, details } = ErrorMessages.MISSING_API_KEY;
218
- expect(message).toContain('ANTHROPIC_API_KEY');
219
- expect(details).toContain('.env');
220
- expect(details).toContain('https://console.anthropic.com/');
221
- });
222
-
217
+ const { message, details } = ErrorMessages.MISSING_API_KEY
218
+ expect(message).toContain('ANTHROPIC_API_KEY')
219
+ expect(details).toContain('.env')
220
+ expect(details).toContain('https://console.anthropic.com/')
221
+ })
222
+
223
223
  test('FILE_NOT_FOUND includes path', () => {
224
- const { message, details } = ErrorMessages.FILE_NOT_FOUND('/path/to/file.yml');
225
- expect(message).toContain('File not found');
226
- expect(details).toContain('/path/to/file.yml');
227
- });
228
-
224
+ const { message, details } = ErrorMessages.FILE_NOT_FOUND('/path/to/file.yml')
225
+ expect(message).toContain('File not found')
226
+ expect(details).toContain('/path/to/file.yml')
227
+ })
228
+
229
229
  test('AGENT_NOT_FOUND provides install instructions for claude', () => {
230
- const { message, details } = ErrorMessages.AGENT_NOT_FOUND('claude');
231
- expect(message).toContain('claude');
232
- expect(details).toContain('npm install');
233
- });
234
-
230
+ const { message, details } = ErrorMessages.AGENT_NOT_FOUND('claude')
231
+ expect(message).toContain('claude')
232
+ expect(details).toContain('npm install')
233
+ })
234
+
235
235
  test('AGENT_NOT_FOUND provides install instructions for opencode', () => {
236
- const { message, details } = ErrorMessages.AGENT_NOT_FOUND('opencode');
237
- expect(message).toContain('opencode');
238
- expect(details).toContain('npm install');
239
- });
240
-
236
+ const { message, details } = ErrorMessages.AGENT_NOT_FOUND('opencode')
237
+ expect(message).toContain('opencode')
238
+ expect(details).toContain('npm install')
239
+ })
240
+
241
241
  test('GIT_NOT_INITIALIZED has init instructions', () => {
242
- const { message, details } = ErrorMessages.GIT_NOT_INITIALIZED;
243
- expect(message).toContain('Git');
244
- expect(details).toContain('git init');
245
- });
246
-
242
+ const { message, details } = ErrorMessages.GIT_NOT_INITIALIZED
243
+ expect(message).toContain('Git')
244
+ expect(details).toContain('git init')
245
+ })
246
+
247
247
  test('AGENT_SPAWN_FAILED includes agent and error', () => {
248
- const { message, details } = ErrorMessages.AGENT_SPAWN_FAILED('opencode', 'command not found');
249
- expect(message).toContain('opencode');
250
- expect(details).toContain('command not found');
251
- expect(details).toContain('PATH');
252
- });
253
-
248
+ const { message, details } = ErrorMessages.AGENT_SPAWN_FAILED('opencode', 'command not found')
249
+ expect(message).toContain('opencode')
250
+ expect(details).toContain('command not found')
251
+ expect(details).toContain('PATH')
252
+ })
253
+
254
254
  test('MODEL_UNAVAILABLE includes model and agent', () => {
255
- const { message, details } = ErrorMessages.MODEL_UNAVAILABLE('claude-sonnet-4-invalid', 'opencode');
256
- expect(message).toContain('Model not available');
257
- expect(details).toContain('claude-sonnet-4-invalid');
258
- expect(details).toContain('opencode');
259
- expect(details).toContain('--help');
260
- });
261
-
255
+ const { message, details } = ErrorMessages.MODEL_UNAVAILABLE(
256
+ 'claude-sonnet-4-invalid',
257
+ 'opencode'
258
+ )
259
+ expect(message).toContain('Model not available')
260
+ expect(details).toContain('claude-sonnet-4-invalid')
261
+ expect(details).toContain('opencode')
262
+ expect(details).toContain('--help')
263
+ })
264
+
262
265
  test('RATE_LIMIT_ERROR without retry-after', () => {
263
- const { message, details } = ErrorMessages.RATE_LIMIT_ERROR('opencode');
264
- expect(message).toContain('Rate limit');
265
- expect(details).toContain('opencode');
266
- expect(details).toContain('wait');
267
- });
268
-
266
+ const { message, details } = ErrorMessages.RATE_LIMIT_ERROR('opencode')
267
+ expect(message).toContain('Rate limit')
268
+ expect(details).toContain('opencode')
269
+ expect(details).toContain('wait')
270
+ })
271
+
269
272
  test('RATE_LIMIT_ERROR with retry-after', () => {
270
- const { message, details } = ErrorMessages.RATE_LIMIT_ERROR('claude', 120);
271
- expect(message).toContain('Rate limit');
272
- expect(details).toContain('120 seconds');
273
- });
274
-
273
+ const { message, details } = ErrorMessages.RATE_LIMIT_ERROR('claude', 120)
274
+ expect(message).toContain('Rate limit')
275
+ expect(details).toContain('120 seconds')
276
+ })
277
+
275
278
  test('AGENT_ERROR includes details', () => {
276
- const { message, details } = ErrorMessages.AGENT_ERROR('opencode', 2, 'Invalid input');
277
- expect(message).toContain('opencode');
278
- expect(details).toContain('code 2');
279
- expect(details).toContain('Invalid input');
280
- });
281
- });
279
+ const { message, details } = ErrorMessages.AGENT_ERROR('opencode', 2, 'Invalid input')
280
+ expect(message).toContain('opencode')
281
+ expect(details).toContain('code 2')
282
+ expect(details).toContain('Invalid input')
283
+ })
284
+ })