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.
- package/README.md +47 -2
- package/package.json +8 -5
- package/src/agent-client.integration.test.ts +57 -59
- package/src/agent-client.test.ts +27 -27
- package/src/agent-client.ts +109 -77
- package/src/agent.test.ts +16 -16
- package/src/agent.ts +103 -103
- package/src/agents-md-generator.test.ts +360 -0
- package/src/agents-md-generator.ts +900 -0
- package/src/config.test.ts +209 -224
- package/src/config.ts +84 -83
- package/src/errors.test.ts +211 -208
- package/src/errors.ts +107 -101
- package/src/index.integration.test.ts +327 -223
- package/src/index.ts +163 -100
- package/src/integration-test.ts +168 -137
- package/src/logger.test.ts +67 -67
- package/src/logger.ts +8 -8
- package/src/prd-generator.integration.test.ts +50 -50
- package/src/prd-generator.test.ts +66 -25
- package/src/prd-generator.ts +280 -194
- package/src/prds.test.ts +60 -65
- package/src/prds.ts +64 -62
- package/src/prompt.test.ts +154 -155
- package/src/prompt.ts +63 -65
- package/src/run.ts +147 -147
- package/src/status.test.ts +80 -80
- package/src/status.ts +40 -42
- package/src/task-generator.test.ts +93 -66
- package/src/task-generator.ts +125 -112
package/src/config.test.ts
CHANGED
|
@@ -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()
|
|
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
|
-
|
|
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')
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, '
|
|
298
|
-
expect(resolveModelForPhase(config, '
|
|
299
|
-
expect(resolveModelForPhase(config, '
|
|
300
|
-
expect(resolveModelForPhase(config, '
|
|
301
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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
|
-
|
|
349
|
-
expect(result.
|
|
350
|
-
expect(result.errors
|
|
351
|
-
expect(result.errors[0]).toContain('
|
|
352
|
-
|
|
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
|
-
|
|
367
|
-
expect(result.
|
|
368
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
|
|
400
|
-
expect(result.
|
|
401
|
-
|
|
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
|
+
})
|