hone-ai 0.2.0 → 0.9.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,196 +1,194 @@
1
- import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from 'bun:test';
2
- import { existsSync, rmSync, mkdirSync } from 'fs';
3
- import { join } from 'path';
4
- import {
5
- getPlansDir,
6
- ensurePlansDir,
7
- getConfigPath,
8
- loadConfig,
1
+ import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from 'bun:test'
2
+ import { existsSync, rmSync, mkdirSync } from 'fs'
3
+ import { join } from 'path'
4
+ import {
5
+ getPlansDir,
6
+ ensurePlansDir,
7
+ getConfigPath,
8
+ loadConfig,
9
9
  saveConfig,
10
10
  isValidAgent,
11
11
  resolveAgent,
12
12
  initProject,
13
13
  resolveModelForPhase,
14
14
  validateConfig,
15
- type HoneConfig
16
- } from './config';
15
+ type HoneConfig,
16
+ } from './config'
17
17
 
18
18
  // Set test environment
19
- const originalEnv = process.env.BUN_ENV;
19
+ const originalEnv = process.env.BUN_ENV
20
20
  beforeAll(() => {
21
- process.env.BUN_ENV = 'test';
22
- });
21
+ process.env.BUN_ENV = 'test'
22
+ })
23
23
  afterAll(() => {
24
- process.env.BUN_ENV = originalEnv;
25
- });
24
+ process.env.BUN_ENV = originalEnv
25
+ })
26
26
 
27
- const TEST_CWD = join(process.cwd(), 'test-workspace');
27
+ const TEST_CWD = join(process.cwd(), 'test-workspace')
28
28
 
29
29
  describe('Config Management', () => {
30
30
  beforeEach(() => {
31
31
  // Create test workspace
32
32
  if (existsSync(TEST_CWD)) {
33
- rmSync(TEST_CWD, { recursive: true, force: true });
33
+ rmSync(TEST_CWD, { recursive: true, force: true })
34
34
  }
35
- mkdirSync(TEST_CWD, { recursive: true });
36
- process.chdir(TEST_CWD);
37
- });
35
+ mkdirSync(TEST_CWD, { recursive: true })
36
+ process.chdir(TEST_CWD)
37
+ })
38
38
 
39
39
  afterEach(() => {
40
40
  // Cleanup
41
- process.chdir(join(TEST_CWD, '..'));
41
+ process.chdir(join(TEST_CWD, '..'))
42
42
  if (existsSync(TEST_CWD)) {
43
- rmSync(TEST_CWD, { recursive: true, force: true });
43
+ rmSync(TEST_CWD, { recursive: true, force: true })
44
44
  }
45
- });
45
+ })
46
46
 
47
47
  test('getPlansDir returns correct path', () => {
48
- const plansDir = getPlansDir();
49
- expect(plansDir).toBe(join(TEST_CWD, '.plans'));
50
- });
48
+ const plansDir = getPlansDir()
49
+ expect(plansDir).toBe(join(TEST_CWD, '.plans'))
50
+ })
51
51
 
52
52
  test('ensurePlansDir creates directory if not exists', () => {
53
- const plansDir = getPlansDir();
54
- expect(existsSync(plansDir)).toBe(false);
55
-
56
- ensurePlansDir();
57
-
58
- expect(existsSync(plansDir)).toBe(true);
59
- });
53
+ const plansDir = getPlansDir()
54
+ expect(existsSync(plansDir)).toBe(false)
55
+
56
+ ensurePlansDir()
57
+
58
+ expect(existsSync(plansDir)).toBe(true)
59
+ })
60
60
 
61
61
  test('ensurePlansDir is idempotent', () => {
62
- ensurePlansDir();
63
- ensurePlansDir(); // Should not throw
64
- expect(existsSync(getPlansDir())).toBe(true);
65
- });
62
+ ensurePlansDir()
63
+ ensurePlansDir() // Should not throw
64
+ expect(existsSync(getPlansDir())).toBe(true)
65
+ })
66
66
 
67
67
  test('loadConfig creates default config if not exists', async () => {
68
- const config = await loadConfig();
68
+ const config = await loadConfig()
69
+
70
+ expect(config.defaultAgent).toBe('claude')
71
+ expect(config.models.opencode).toBe('claude-sonnet-4-20250514')
72
+ expect(config.models.claude).toBe('claude-sonnet-4-20250514')
69
73
 
70
- expect(config.defaultAgent).toBe('claude');
71
- expect(config.models.opencode).toBe('claude-sonnet-4-20250514');
72
- expect(config.models.claude).toBe('claude-sonnet-4-20250514');
73
-
74
74
  // Verify file was created
75
- expect(existsSync(getConfigPath())).toBe(true);
76
- });
75
+ expect(existsSync(getConfigPath())).toBe(true)
76
+ })
77
77
 
78
78
  test('loadConfig reads existing config', async () => {
79
79
  const customConfig: HoneConfig = {
80
80
  defaultAgent: 'opencode',
81
81
  models: {
82
82
  opencode: 'custom-model',
83
- claude: 'another-model'
84
- }
85
- };
86
-
87
- await saveConfig(customConfig);
88
- const loaded = await loadConfig();
89
-
90
- expect(loaded.defaultAgent).toBe('opencode');
91
- expect(loaded.models.opencode).toBe('custom-model');
92
- });
83
+ claude: 'another-model',
84
+ },
85
+ }
86
+
87
+ await saveConfig(customConfig)
88
+ const loaded = await loadConfig()
89
+
90
+ expect(loaded.defaultAgent).toBe('opencode')
91
+ expect(loaded.models.opencode).toBe('custom-model')
92
+ })
93
93
 
94
94
  test('saveConfig writes config correctly', async () => {
95
95
  const config: HoneConfig = {
96
96
  defaultAgent: 'opencode',
97
97
  models: {
98
98
  opencode: 'test-opencode',
99
- claude: 'test-claude'
99
+ claude: 'test-claude',
100
100
  },
101
101
  feedbackInstructions: 'test: npm test, lint: npm run lint',
102
- lintCommand: 'npm run lint'
103
- };
104
-
105
- await saveConfig(config);
106
-
107
- expect(existsSync(getConfigPath())).toBe(true);
108
-
109
- const loaded = await loadConfig();
110
- expect(loaded).toEqual(config);
111
- });
102
+ lintCommand: 'npm run lint',
103
+ }
104
+
105
+ await saveConfig(config)
106
+
107
+ expect(existsSync(getConfigPath())).toBe(true)
108
+
109
+ const loaded = await loadConfig()
110
+ expect(loaded).toEqual(config)
111
+ })
112
112
 
113
113
  test('isValidAgent returns true for valid agents', () => {
114
- expect(isValidAgent('opencode')).toBe(true);
115
- expect(isValidAgent('claude')).toBe(true);
116
- });
114
+ expect(isValidAgent('opencode')).toBe(true)
115
+ expect(isValidAgent('claude')).toBe(true)
116
+ })
117
117
 
118
118
  test('isValidAgent returns false for invalid agents', () => {
119
- expect(isValidAgent('invalid')).toBe(false);
120
- expect(isValidAgent('gpt4')).toBe(false);
121
- expect(isValidAgent('')).toBe(false);
122
- });
119
+ expect(isValidAgent('invalid')).toBe(false)
120
+ expect(isValidAgent('gpt4')).toBe(false)
121
+ expect(isValidAgent('')).toBe(false)
122
+ })
123
123
 
124
124
  test('resolveAgent prioritizes flag over config', async () => {
125
125
  // Set config default to claude
126
126
  await saveConfig({
127
127
  defaultAgent: 'claude',
128
128
  models: { opencode: 'test', claude: 'test' },
129
+ })
129
130
 
130
- });
131
-
132
131
  // Flag should override
133
- const agent = await resolveAgent('opencode');
134
- expect(agent).toBe('opencode');
135
- });
132
+ const agent = await resolveAgent('opencode')
133
+ expect(agent).toBe('opencode')
134
+ })
136
135
 
137
136
  test('resolveAgent uses config when no flag provided', async () => {
138
137
  // Set config default to opencode
139
138
  await saveConfig({
140
139
  defaultAgent: 'opencode',
141
140
  models: { opencode: 'test', claude: 'test' },
141
+ })
142
142
 
143
- });
144
-
145
- const agent = await resolveAgent();
146
- expect(agent).toBe('opencode');
147
- });
143
+ const agent = await resolveAgent()
144
+ expect(agent).toBe('opencode')
145
+ })
148
146
 
149
147
  test('resolveAgent uses default when no flag and no config', async () => {
150
148
  // Don't create config, should use default
151
- const agent = await resolveAgent();
152
- expect(agent).toBe('claude'); // Default from DEFAULT_CONFIG
153
- });
149
+ const agent = await resolveAgent()
150
+ expect(agent).toBe('claude') // Default from DEFAULT_CONFIG
151
+ })
154
152
 
155
153
  test('initProject creates .plans directory and config file', async () => {
156
- const plansDir = getPlansDir();
157
- const configPath = getConfigPath();
158
-
159
- expect(existsSync(plansDir)).toBe(false);
160
- expect(existsSync(configPath)).toBe(false);
161
-
162
- const result = await initProject();
163
-
164
- expect(result.plansCreated).toBe(true);
165
- expect(result.configCreated).toBe(true);
166
- expect(existsSync(plansDir)).toBe(true);
167
- expect(existsSync(configPath)).toBe(true);
168
- });
154
+ const plansDir = getPlansDir()
155
+ const configPath = getConfigPath()
156
+
157
+ expect(existsSync(plansDir)).toBe(false)
158
+ expect(existsSync(configPath)).toBe(false)
159
+
160
+ const result = await initProject()
161
+
162
+ expect(result.plansCreated).toBe(true)
163
+ expect(result.configCreated).toBe(true)
164
+ expect(existsSync(plansDir)).toBe(true)
165
+ expect(existsSync(configPath)).toBe(true)
166
+ })
169
167
 
170
168
  test('initProject is idempotent when already initialized', async () => {
171
169
  // First init
172
- await initProject();
173
-
170
+ await initProject()
171
+
174
172
  // Second init
175
- const result = await initProject();
176
-
177
- expect(result.plansCreated).toBe(false);
178
- expect(result.configCreated).toBe(false);
179
- expect(existsSync(getPlansDir())).toBe(true);
180
- expect(existsSync(getConfigPath())).toBe(true);
181
- });
173
+ const result = await initProject()
174
+
175
+ expect(result.plansCreated).toBe(false)
176
+ expect(result.configCreated).toBe(false)
177
+ expect(existsSync(getPlansDir())).toBe(true)
178
+ expect(existsSync(getConfigPath())).toBe(true)
179
+ })
182
180
 
183
181
  test('initProject creates only missing parts', async () => {
184
182
  // Create .plans directory manually
185
- ensurePlansDir();
186
-
187
- const result = await initProject();
188
-
189
- expect(result.plansCreated).toBe(false);
190
- expect(result.configCreated).toBe(true);
191
- expect(existsSync(getConfigPath())).toBe(true);
192
- });
193
- });
183
+ ensurePlansDir()
184
+
185
+ const result = await initProject()
186
+
187
+ expect(result.plansCreated).toBe(false)
188
+ expect(result.configCreated).toBe(true)
189
+ expect(existsSync(getConfigPath())).toBe(true)
190
+ })
191
+ })
194
192
 
195
193
  describe('Model Resolution', () => {
196
194
  test('resolveModelForPhase returns default model when no phase specified', () => {
@@ -198,14 +196,13 @@ describe('Model Resolution', () => {
198
196
  defaultAgent: 'claude',
199
197
  models: {
200
198
  opencode: 'claude-sonnet-4-20250514',
201
- claude: 'claude-sonnet-4-20250514'
202
- }
199
+ claude: 'claude-sonnet-4-20250514',
200
+ },
201
+ }
203
202
 
204
- };
205
-
206
- const model = resolveModelForPhase(config);
207
- expect(model).toBe('claude-sonnet-4-20250514');
208
- });
203
+ const model = resolveModelForPhase(config)
204
+ expect(model).toBe('claude-sonnet-4-20250514')
205
+ })
209
206
 
210
207
  test('resolveModelForPhase returns phase-specific model when configured', () => {
211
208
  const config: HoneConfig = {
@@ -213,28 +210,26 @@ describe('Model Resolution', () => {
213
210
  models: {
214
211
  opencode: 'claude-sonnet-4-20250514',
215
212
  claude: 'claude-sonnet-4-20250514',
216
- implement: 'claude-opus-4-20250514'
217
- }
213
+ implement: 'claude-opus-4-20250514',
214
+ },
215
+ }
218
216
 
219
- };
220
-
221
- const model = resolveModelForPhase(config, 'implement');
222
- expect(model).toBe('claude-opus-4-20250514');
223
- });
217
+ const model = resolveModelForPhase(config, 'implement')
218
+ expect(model).toBe('claude-opus-4-20250514')
219
+ })
224
220
 
225
221
  test('resolveModelForPhase falls back to agent-specific model', () => {
226
222
  const config: HoneConfig = {
227
223
  defaultAgent: 'opencode',
228
224
  models: {
229
225
  opencode: 'custom-opencode-model',
230
- claude: 'claude-sonnet-4-20250514'
231
- }
226
+ claude: 'claude-sonnet-4-20250514',
227
+ },
228
+ }
232
229
 
233
- };
234
-
235
- const model = resolveModelForPhase(config, 'implement', 'opencode');
236
- expect(model).toBe('custom-opencode-model');
237
- });
230
+ const model = resolveModelForPhase(config, 'implement', 'opencode')
231
+ expect(model).toBe('custom-opencode-model')
232
+ })
238
233
 
239
234
  test('resolveModelForPhase prioritizes phase-specific over agent-specific', () => {
240
235
  const config: HoneConfig = {
@@ -242,42 +237,39 @@ describe('Model Resolution', () => {
242
237
  models: {
243
238
  opencode: 'opencode-default',
244
239
  claude: 'claude-default',
245
- review: 'review-specific-model'
246
- }
240
+ review: 'review-specific-model',
241
+ },
242
+ }
247
243
 
248
- };
249
-
250
- const model = resolveModelForPhase(config, 'review', 'opencode');
251
- expect(model).toBe('review-specific-model');
252
- });
244
+ const model = resolveModelForPhase(config, 'review', 'opencode')
245
+ expect(model).toBe('review-specific-model')
246
+ })
253
247
 
254
248
  test('resolveModelForPhase uses defaultAgent when agent not specified', () => {
255
249
  const config: HoneConfig = {
256
250
  defaultAgent: 'opencode',
257
251
  models: {
258
252
  opencode: 'opencode-model',
259
- claude: 'claude-model'
260
- }
253
+ claude: 'claude-model',
254
+ },
255
+ }
261
256
 
262
- };
263
-
264
- const model = resolveModelForPhase(config, 'prd');
265
- expect(model).toBe('opencode-model');
266
- });
257
+ const model = resolveModelForPhase(config, 'prd')
258
+ expect(model).toBe('opencode-model')
259
+ })
267
260
 
268
261
  test('resolveModelForPhase returns default when phase and agent models missing', () => {
269
262
  const config: HoneConfig = {
270
263
  defaultAgent: 'claude',
271
264
  models: {
272
265
  opencode: '',
273
- claude: ''
274
- }
266
+ claude: '',
267
+ },
268
+ }
275
269
 
276
- };
277
-
278
- const model = resolveModelForPhase(config, 'finalize');
279
- expect(model).toBe('claude-sonnet-4-20250514');
280
- });
270
+ const model = resolveModelForPhase(config, 'finalize')
271
+ expect(model).toBe('claude-sonnet-4-20250514')
272
+ })
281
273
 
282
274
  test('resolveModelForPhase handles all phase types', () => {
283
275
  const config: HoneConfig = {
@@ -289,18 +281,17 @@ describe('Model Resolution', () => {
289
281
  prdToTasks: 'tasks-model',
290
282
  implement: 'impl-model',
291
283
  review: 'review-model',
292
- finalize: 'final-model'
293
- }
294
-
295
- };
296
-
297
- expect(resolveModelForPhase(config, 'prd')).toBe('prd-model');
298
- expect(resolveModelForPhase(config, 'prdToTasks')).toBe('tasks-model');
299
- expect(resolveModelForPhase(config, 'implement')).toBe('impl-model');
300
- expect(resolveModelForPhase(config, 'review')).toBe('review-model');
301
- expect(resolveModelForPhase(config, 'finalize')).toBe('final-model');
302
- });
303
- });
284
+ finalize: 'final-model',
285
+ },
286
+ }
287
+
288
+ expect(resolveModelForPhase(config, 'prd')).toBe('prd-model')
289
+ expect(resolveModelForPhase(config, 'prdToTasks')).toBe('tasks-model')
290
+ expect(resolveModelForPhase(config, 'implement')).toBe('impl-model')
291
+ expect(resolveModelForPhase(config, 'review')).toBe('review-model')
292
+ expect(resolveModelForPhase(config, 'finalize')).toBe('final-model')
293
+ })
294
+ })
304
295
 
305
296
  describe('Config Validation', () => {
306
297
  test('validateConfig accepts valid model formats', () => {
@@ -308,15 +299,14 @@ describe('Config Validation', () => {
308
299
  defaultAgent: 'claude',
309
300
  models: {
310
301
  opencode: 'claude-sonnet-4-20250514',
311
- claude: 'claude-opus-4-20251231'
312
- }
302
+ claude: 'claude-opus-4-20251231',
303
+ },
304
+ }
313
305
 
314
- };
315
-
316
- const result = validateConfig(config);
317
- expect(result.valid).toBe(true);
318
- expect(result.errors.length).toBe(0);
319
- });
306
+ const result = validateConfig(config)
307
+ expect(result.valid).toBe(true)
308
+ expect(result.errors.length).toBe(0)
309
+ })
320
310
 
321
311
  test('validateConfig accepts valid phase-specific models', () => {
322
312
  const config: HoneConfig = {
@@ -325,32 +315,30 @@ describe('Config Validation', () => {
325
315
  opencode: 'claude-sonnet-4-20250514',
326
316
  claude: 'claude-sonnet-4-20250514',
327
317
  implement: 'claude-opus-4-20250601',
328
- review: 'claude-sonnet-4-20250701'
329
- }
318
+ review: 'claude-sonnet-4-20250701',
319
+ },
320
+ }
330
321
 
331
- };
332
-
333
- const result = validateConfig(config);
334
- expect(result.valid).toBe(true);
335
- expect(result.errors.length).toBe(0);
336
- });
322
+ const result = validateConfig(config)
323
+ expect(result.valid).toBe(true)
324
+ expect(result.errors.length).toBe(0)
325
+ })
337
326
 
338
327
  test('validateConfig rejects invalid agent model format', () => {
339
328
  const config: HoneConfig = {
340
329
  defaultAgent: 'claude',
341
330
  models: {
342
331
  opencode: 'invalid-model',
343
- claude: 'claude-sonnet-4-20250514'
344
- }
345
-
346
- };
347
-
348
- const result = validateConfig(config);
349
- expect(result.valid).toBe(false);
350
- expect(result.errors.length).toBe(1);
351
- expect(result.errors[0]).toContain('opencode');
352
- expect(result.errors[0]).toContain('invalid-model');
353
- });
332
+ claude: 'claude-sonnet-4-20250514',
333
+ },
334
+ }
335
+
336
+ const result = validateConfig(config)
337
+ expect(result.valid).toBe(false)
338
+ expect(result.errors.length).toBe(1)
339
+ expect(result.errors[0]).toContain('opencode')
340
+ expect(result.errors[0]).toContain('invalid-model')
341
+ })
354
342
 
355
343
  test('validateConfig rejects invalid phase model format', () => {
356
344
  const config: HoneConfig = {
@@ -358,16 +346,15 @@ describe('Config Validation', () => {
358
346
  models: {
359
347
  opencode: 'claude-sonnet-4-20250514',
360
348
  claude: 'claude-sonnet-4-20250514',
361
- implement: 'wrong-format'
362
- }
349
+ implement: 'wrong-format',
350
+ },
351
+ }
363
352
 
364
- };
365
-
366
- const result = validateConfig(config);
367
- expect(result.valid).toBe(false);
368
- expect(result.errors.length).toBe(1);
369
- expect(result.errors[0]).toContain('implement');
370
- });
353
+ const result = validateConfig(config)
354
+ expect(result.valid).toBe(false)
355
+ expect(result.errors.length).toBe(1)
356
+ expect(result.errors[0]).toContain('implement')
357
+ })
371
358
 
372
359
  test('validateConfig handles multiple invalid models', () => {
373
360
  const config: HoneConfig = {
@@ -375,29 +362,27 @@ describe('Config Validation', () => {
375
362
  models: {
376
363
  opencode: 'bad-opencode',
377
364
  claude: 'bad-claude',
378
- implement: 'bad-implement'
379
- }
365
+ implement: 'bad-implement',
366
+ },
367
+ }
380
368
 
381
- };
382
-
383
- const result = validateConfig(config);
384
- expect(result.valid).toBe(false);
385
- expect(result.errors.length).toBe(3);
386
- });
369
+ const result = validateConfig(config)
370
+ expect(result.valid).toBe(false)
371
+ expect(result.errors.length).toBe(3)
372
+ })
387
373
 
388
374
  test('validateConfig allows empty phase-specific models', () => {
389
375
  const config: HoneConfig = {
390
376
  defaultAgent: 'claude',
391
377
  models: {
392
378
  opencode: 'claude-sonnet-4-20250514',
393
- claude: 'claude-sonnet-4-20250514'
379
+ claude: 'claude-sonnet-4-20250514',
394
380
  // No phase-specific models
395
- }
396
-
397
- };
398
-
399
- const result = validateConfig(config);
400
- expect(result.valid).toBe(true);
401
- expect(result.errors.length).toBe(0);
402
- });
403
- });
381
+ },
382
+ }
383
+
384
+ const result = validateConfig(config)
385
+ expect(result.valid).toBe(true)
386
+ expect(result.errors.length).toBe(0)
387
+ })
388
+ })