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.
- package/README.md +47 -2
- package/package.json +5 -2
- 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
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from 'bun:test'
|
|
2
|
-
import { existsSync, rmSync, mkdirSync, writeFileSync } from 'fs'
|
|
3
|
-
import { join } from 'path'
|
|
4
|
-
import { spawnSync } from 'child_process'
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach, beforeAll, afterAll } from 'bun:test'
|
|
2
|
+
import { existsSync, rmSync, mkdirSync, writeFileSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { spawnSync } from 'child_process'
|
|
5
|
+
import packageJson from '../package.json'
|
|
5
6
|
|
|
6
7
|
// Set test environment
|
|
7
|
-
const originalEnv = process.env.BUN_ENV
|
|
8
|
+
const originalEnv = process.env.BUN_ENV
|
|
8
9
|
beforeAll(() => {
|
|
9
|
-
process.env.BUN_ENV = 'test'
|
|
10
|
-
process.env.ANTHROPIC_API_KEY = 'test-api-key'
|
|
11
|
-
})
|
|
10
|
+
process.env.BUN_ENV = 'test'
|
|
11
|
+
process.env.ANTHROPIC_API_KEY = 'test-api-key'
|
|
12
|
+
})
|
|
12
13
|
afterAll(() => {
|
|
13
|
-
process.env.BUN_ENV = originalEnv
|
|
14
|
-
delete process.env.ANTHROPIC_API_KEY
|
|
15
|
-
})
|
|
14
|
+
process.env.BUN_ENV = originalEnv
|
|
15
|
+
delete process.env.ANTHROPIC_API_KEY
|
|
16
|
+
})
|
|
16
17
|
|
|
17
|
-
const TEST_CWD = join(process.cwd(), 'test-cli-integration')
|
|
18
|
-
const CLI_PATH = join(process.cwd(), 'src', 'index.ts')
|
|
18
|
+
const TEST_CWD = join(process.cwd(), 'test-cli-integration')
|
|
19
|
+
const CLI_PATH = join(process.cwd(), 'src', 'index.ts')
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Helper to run CLI command
|
|
@@ -24,322 +25,425 @@ function runCli(args: string[]): { stdout: string; stderr: string; exitCode: num
|
|
|
24
25
|
const result = spawnSync('bun', [CLI_PATH, ...args], {
|
|
25
26
|
cwd: TEST_CWD,
|
|
26
27
|
encoding: 'utf-8',
|
|
27
|
-
env: { ...process.env, BUN_ENV: 'test', ANTHROPIC_API_KEY: 'test-api-key' }
|
|
28
|
-
})
|
|
29
|
-
|
|
28
|
+
env: { ...process.env, BUN_ENV: 'test', ANTHROPIC_API_KEY: 'test-api-key' },
|
|
29
|
+
})
|
|
30
|
+
|
|
30
31
|
return {
|
|
31
32
|
stdout: result.stdout || '',
|
|
32
33
|
stderr: result.stderr || '',
|
|
33
|
-
exitCode: result.status || 0
|
|
34
|
-
}
|
|
34
|
+
exitCode: result.status || 0,
|
|
35
|
+
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* Helper to create mock PRD file
|
|
39
40
|
*/
|
|
40
41
|
function createMockPrd(feature: string, content: string = '# Feature\n\nTest PRD content'): void {
|
|
41
|
-
const plansDir = join(TEST_CWD, '.plans')
|
|
42
|
+
const plansDir = join(TEST_CWD, '.plans')
|
|
42
43
|
if (!existsSync(plansDir)) {
|
|
43
|
-
mkdirSync(plansDir, { recursive: true })
|
|
44
|
+
mkdirSync(plansDir, { recursive: true })
|
|
44
45
|
}
|
|
45
|
-
writeFileSync(join(plansDir, `prd-${feature}.md`), content)
|
|
46
|
+
writeFileSync(join(plansDir, `prd-${feature}.md`), content)
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* Helper to create mock task file
|
|
50
51
|
*/
|
|
51
52
|
function createMockTaskFile(feature: string, tasks: any[]): void {
|
|
52
|
-
const plansDir = join(TEST_CWD, '.plans')
|
|
53
|
+
const plansDir = join(TEST_CWD, '.plans')
|
|
53
54
|
if (!existsSync(plansDir)) {
|
|
54
|
-
mkdirSync(plansDir, { recursive: true })
|
|
55
|
+
mkdirSync(plansDir, { recursive: true })
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
+
|
|
57
58
|
const yaml = `feature: ${feature}
|
|
58
59
|
prd: ./prd-${feature}.md
|
|
59
60
|
created_at: 2026-01-28T12:00:00Z
|
|
60
61
|
updated_at: 2026-01-28T12:00:00Z
|
|
61
62
|
|
|
62
63
|
tasks:
|
|
63
|
-
${tasks
|
|
64
|
+
${tasks
|
|
65
|
+
.map(
|
|
66
|
+
t => ` - id: ${t.id}
|
|
64
67
|
title: "${t.title}"
|
|
65
68
|
description: "${t.description}"
|
|
66
69
|
status: ${t.status}
|
|
67
70
|
dependencies: ${t.dependencies ? JSON.stringify(t.dependencies) : '[]'}
|
|
68
71
|
acceptance_criteria: ${t.acceptance_criteria ? JSON.stringify(t.acceptance_criteria) : '[]'}
|
|
69
|
-
completed_at: ${t.completed_at || 'null'}`
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
completed_at: ${t.completed_at || 'null'}`
|
|
73
|
+
)
|
|
74
|
+
.join('\n')}
|
|
75
|
+
`
|
|
76
|
+
|
|
77
|
+
writeFileSync(join(plansDir, `tasks-${feature}.yml`), yaml)
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
describe('CLI Integration Tests', () => {
|
|
76
81
|
beforeEach(() => {
|
|
77
82
|
// Create test workspace
|
|
78
83
|
if (existsSync(TEST_CWD)) {
|
|
79
|
-
rmSync(TEST_CWD, { recursive: true, force: true })
|
|
84
|
+
rmSync(TEST_CWD, { recursive: true, force: true })
|
|
80
85
|
}
|
|
81
|
-
mkdirSync(TEST_CWD, { recursive: true })
|
|
82
|
-
})
|
|
86
|
+
mkdirSync(TEST_CWD, { recursive: true })
|
|
87
|
+
})
|
|
83
88
|
|
|
84
89
|
afterEach(() => {
|
|
85
90
|
// Cleanup
|
|
86
91
|
if (existsSync(TEST_CWD)) {
|
|
87
|
-
rmSync(TEST_CWD, { recursive: true, force: true })
|
|
92
|
+
rmSync(TEST_CWD, { recursive: true, force: true })
|
|
88
93
|
}
|
|
89
|
-
})
|
|
94
|
+
})
|
|
90
95
|
|
|
91
96
|
describe('prds command', () => {
|
|
92
97
|
test('shows message when no PRDs exist', () => {
|
|
93
|
-
const result = runCli(['prds'])
|
|
94
|
-
|
|
95
|
-
expect(result.exitCode).toBe(0)
|
|
96
|
-
expect(result.stdout).toContain('No PRDs found')
|
|
97
|
-
expect(result.stdout).toContain('Create a PRD with: hone prd')
|
|
98
|
-
})
|
|
98
|
+
const result = runCli(['prds'])
|
|
99
|
+
|
|
100
|
+
expect(result.exitCode).toBe(0)
|
|
101
|
+
expect(result.stdout).toContain('No PRDs found')
|
|
102
|
+
expect(result.stdout).toContain('Create a PRD with: hone prd')
|
|
103
|
+
})
|
|
99
104
|
|
|
100
105
|
test('lists PRD with no task file', () => {
|
|
101
|
-
createMockPrd('test-feature')
|
|
102
|
-
|
|
103
|
-
const result = runCli(['prds'])
|
|
104
|
-
|
|
105
|
-
expect(result.exitCode).toBe(0)
|
|
106
|
-
expect(result.stdout).toContain('.plans/prd-test-feature.md')
|
|
107
|
-
expect(result.stdout).toContain('Tasks: none')
|
|
108
|
-
expect(result.stdout).toContain('Status: not started')
|
|
109
|
-
})
|
|
106
|
+
createMockPrd('test-feature')
|
|
107
|
+
|
|
108
|
+
const result = runCli(['prds'])
|
|
109
|
+
|
|
110
|
+
expect(result.exitCode).toBe(0)
|
|
111
|
+
expect(result.stdout).toContain('.plans/prd-test-feature.md')
|
|
112
|
+
expect(result.stdout).toContain('Tasks: none')
|
|
113
|
+
expect(result.stdout).toContain('Status: not started')
|
|
114
|
+
})
|
|
110
115
|
|
|
111
116
|
test('lists PRD with task file showing in progress status', () => {
|
|
112
|
-
createMockPrd('test-feature')
|
|
117
|
+
createMockPrd('test-feature')
|
|
113
118
|
createMockTaskFile('test-feature', [
|
|
114
|
-
{
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
{
|
|
120
|
+
id: 'task-1',
|
|
121
|
+
title: 'Task 1',
|
|
122
|
+
description: 'Test',
|
|
123
|
+
status: 'completed',
|
|
124
|
+
dependencies: [],
|
|
125
|
+
completed_at: '2026-01-28T12:00:00Z',
|
|
126
|
+
},
|
|
127
|
+
{ id: 'task-2', title: 'Task 2', description: 'Test', status: 'pending', dependencies: [] },
|
|
128
|
+
])
|
|
129
|
+
|
|
130
|
+
const result = runCli(['prds'])
|
|
131
|
+
|
|
132
|
+
expect(result.exitCode).toBe(0)
|
|
133
|
+
expect(result.stdout).toContain('.plans/prd-test-feature.md')
|
|
134
|
+
expect(result.stdout).toContain('Tasks: .plans/tasks-test-feature.yml')
|
|
135
|
+
expect(result.stdout).toContain('Status: in progress (1/2 completed)')
|
|
136
|
+
})
|
|
125
137
|
|
|
126
138
|
test('lists PRD with completed status', () => {
|
|
127
|
-
createMockPrd('test-feature')
|
|
139
|
+
createMockPrd('test-feature')
|
|
128
140
|
createMockTaskFile('test-feature', [
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
{
|
|
142
|
+
id: 'task-1',
|
|
143
|
+
title: 'Task 1',
|
|
144
|
+
description: 'Test',
|
|
145
|
+
status: 'completed',
|
|
146
|
+
dependencies: [],
|
|
147
|
+
completed_at: '2026-01-28T12:00:00Z',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'task-2',
|
|
151
|
+
title: 'Task 2',
|
|
152
|
+
description: 'Test',
|
|
153
|
+
status: 'completed',
|
|
154
|
+
dependencies: [],
|
|
155
|
+
completed_at: '2026-01-28T12:00:00Z',
|
|
156
|
+
},
|
|
157
|
+
])
|
|
158
|
+
|
|
159
|
+
const result = runCli(['prds'])
|
|
160
|
+
|
|
161
|
+
expect(result.exitCode).toBe(0)
|
|
162
|
+
expect(result.stdout).toContain('.plans/prd-test-feature.md')
|
|
163
|
+
expect(result.stdout).toContain('Status: completed')
|
|
164
|
+
})
|
|
139
165
|
|
|
140
166
|
test('lists multiple PRDs', () => {
|
|
141
|
-
createMockPrd('feature-one')
|
|
142
|
-
createMockPrd('feature-two')
|
|
167
|
+
createMockPrd('feature-one')
|
|
168
|
+
createMockPrd('feature-two')
|
|
143
169
|
createMockTaskFile('feature-one', [
|
|
144
|
-
{ id: 'task-1', title: 'Task 1', description: 'Test', status: 'pending', dependencies: [] }
|
|
145
|
-
])
|
|
146
|
-
|
|
147
|
-
const result = runCli(['prds'])
|
|
148
|
-
|
|
149
|
-
expect(result.exitCode).toBe(0)
|
|
150
|
-
expect(result.stdout).toContain('.plans/prd-feature-one.md')
|
|
151
|
-
expect(result.stdout).toContain('.plans/prd-feature-two.md')
|
|
152
|
-
expect(result.stdout).toContain('.plans/tasks-feature-one.yml')
|
|
153
|
-
expect(result.stdout).toContain('Tasks: none')
|
|
154
|
-
})
|
|
155
|
-
})
|
|
170
|
+
{ id: 'task-1', title: 'Task 1', description: 'Test', status: 'pending', dependencies: [] },
|
|
171
|
+
])
|
|
172
|
+
|
|
173
|
+
const result = runCli(['prds'])
|
|
174
|
+
|
|
175
|
+
expect(result.exitCode).toBe(0)
|
|
176
|
+
expect(result.stdout).toContain('.plans/prd-feature-one.md')
|
|
177
|
+
expect(result.stdout).toContain('.plans/prd-feature-two.md')
|
|
178
|
+
expect(result.stdout).toContain('.plans/tasks-feature-one.yml')
|
|
179
|
+
expect(result.stdout).toContain('Tasks: none')
|
|
180
|
+
})
|
|
181
|
+
})
|
|
156
182
|
|
|
157
183
|
describe('status command', () => {
|
|
158
184
|
test('shows message when no incomplete tasks', () => {
|
|
159
|
-
const result = runCli(['status'])
|
|
160
|
-
|
|
161
|
-
expect(result.exitCode).toBe(0)
|
|
162
|
-
expect(result.stdout).toContain('No incomplete task lists found')
|
|
163
|
-
expect(result.stdout).toContain('All tasks completed!')
|
|
164
|
-
})
|
|
185
|
+
const result = runCli(['status'])
|
|
186
|
+
|
|
187
|
+
expect(result.exitCode).toBe(0)
|
|
188
|
+
expect(result.stdout).toContain('No incomplete task lists found')
|
|
189
|
+
expect(result.stdout).toContain('All tasks completed!')
|
|
190
|
+
})
|
|
165
191
|
|
|
166
192
|
test('lists incomplete task file with next task', () => {
|
|
167
193
|
createMockTaskFile('test-feature', [
|
|
168
|
-
{
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
194
|
+
{
|
|
195
|
+
id: 'task-1',
|
|
196
|
+
title: 'Task 1',
|
|
197
|
+
description: 'Test',
|
|
198
|
+
status: 'completed',
|
|
199
|
+
dependencies: [],
|
|
200
|
+
completed_at: '2026-01-28T12:00:00Z',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: 'task-2',
|
|
204
|
+
title: 'Task 2',
|
|
205
|
+
description: 'Test task 2',
|
|
206
|
+
status: 'pending',
|
|
207
|
+
dependencies: [],
|
|
208
|
+
},
|
|
209
|
+
])
|
|
210
|
+
|
|
211
|
+
const result = runCli(['status'])
|
|
212
|
+
|
|
213
|
+
expect(result.exitCode).toBe(0)
|
|
214
|
+
expect(result.stdout).toContain('.plans/tasks-test-feature.yml')
|
|
215
|
+
expect(result.stdout).toContain('Feature: test-feature')
|
|
216
|
+
expect(result.stdout).toContain('Progress: 1/2 tasks completed')
|
|
217
|
+
expect(result.stdout).toContain('Next: task-2 - Task 2')
|
|
218
|
+
})
|
|
180
219
|
|
|
181
220
|
test('shows waiting for dependencies when no task available', () => {
|
|
182
221
|
createMockTaskFile('test-feature', [
|
|
183
222
|
{ id: 'task-1', title: 'Task 1', description: 'Test', status: 'pending', dependencies: [] },
|
|
184
|
-
{
|
|
185
|
-
|
|
186
|
-
|
|
223
|
+
{
|
|
224
|
+
id: 'task-2',
|
|
225
|
+
title: 'Task 2',
|
|
226
|
+
description: 'Test',
|
|
227
|
+
status: 'pending',
|
|
228
|
+
dependencies: ['task-1'],
|
|
229
|
+
},
|
|
230
|
+
])
|
|
231
|
+
|
|
187
232
|
// Mark task-1 as in_progress so task-2 is blocked
|
|
188
233
|
createMockTaskFile('test-feature', [
|
|
189
|
-
{
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
234
|
+
{
|
|
235
|
+
id: 'task-1',
|
|
236
|
+
title: 'Task 1',
|
|
237
|
+
description: 'Test',
|
|
238
|
+
status: 'in_progress',
|
|
239
|
+
dependencies: [],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: 'task-2',
|
|
243
|
+
title: 'Task 2',
|
|
244
|
+
description: 'Test',
|
|
245
|
+
status: 'pending',
|
|
246
|
+
dependencies: ['task-1'],
|
|
247
|
+
},
|
|
248
|
+
])
|
|
249
|
+
|
|
250
|
+
const result = runCli(['status'])
|
|
251
|
+
|
|
252
|
+
expect(result.exitCode).toBe(0)
|
|
253
|
+
expect(result.stdout).toContain('Next: (waiting for dependencies)')
|
|
254
|
+
})
|
|
198
255
|
|
|
199
256
|
test('excludes fully completed task files', () => {
|
|
200
257
|
createMockTaskFile('completed-feature', [
|
|
201
|
-
{
|
|
202
|
-
|
|
258
|
+
{
|
|
259
|
+
id: 'task-1',
|
|
260
|
+
title: 'Task 1',
|
|
261
|
+
description: 'Test',
|
|
262
|
+
status: 'completed',
|
|
263
|
+
dependencies: [],
|
|
264
|
+
completed_at: '2026-01-28T12:00:00Z',
|
|
265
|
+
},
|
|
266
|
+
])
|
|
203
267
|
createMockTaskFile('incomplete-feature', [
|
|
204
|
-
{ id: 'task-1', title: 'Task 1', description: 'Test', status: 'pending', dependencies: [] }
|
|
205
|
-
])
|
|
206
|
-
|
|
207
|
-
const result = runCli(['status'])
|
|
208
|
-
|
|
209
|
-
expect(result.exitCode).toBe(0)
|
|
210
|
-
expect(result.stdout).not.toContain('completed-feature')
|
|
211
|
-
expect(result.stdout).toContain('incomplete-feature')
|
|
212
|
-
})
|
|
268
|
+
{ id: 'task-1', title: 'Task 1', description: 'Test', status: 'pending', dependencies: [] },
|
|
269
|
+
])
|
|
270
|
+
|
|
271
|
+
const result = runCli(['status'])
|
|
272
|
+
|
|
273
|
+
expect(result.exitCode).toBe(0)
|
|
274
|
+
expect(result.stdout).not.toContain('completed-feature')
|
|
275
|
+
expect(result.stdout).toContain('incomplete-feature')
|
|
276
|
+
})
|
|
213
277
|
|
|
214
278
|
test('lists multiple incomplete task files', () => {
|
|
215
279
|
createMockTaskFile('feature-one', [
|
|
216
|
-
{ id: 'task-1', title: 'Task 1', description: 'Test', status: 'pending', dependencies: [] }
|
|
217
|
-
])
|
|
280
|
+
{ id: 'task-1', title: 'Task 1', description: 'Test', status: 'pending', dependencies: [] },
|
|
281
|
+
])
|
|
218
282
|
createMockTaskFile('feature-two', [
|
|
219
|
-
{
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
283
|
+
{
|
|
284
|
+
id: 'task-1',
|
|
285
|
+
title: 'Task 1',
|
|
286
|
+
description: 'Test',
|
|
287
|
+
status: 'completed',
|
|
288
|
+
dependencies: [],
|
|
289
|
+
completed_at: '2026-01-28T12:00:00Z',
|
|
290
|
+
},
|
|
291
|
+
{ id: 'task-2', title: 'Task 2', description: 'Test', status: 'pending', dependencies: [] },
|
|
292
|
+
])
|
|
293
|
+
|
|
294
|
+
const result = runCli(['status'])
|
|
295
|
+
|
|
296
|
+
expect(result.exitCode).toBe(0)
|
|
297
|
+
expect(result.stdout).toContain('feature-one')
|
|
298
|
+
expect(result.stdout).toContain('feature-two')
|
|
299
|
+
expect(result.stdout).toContain('0/1 tasks completed')
|
|
300
|
+
expect(result.stdout).toContain('1/2 tasks completed')
|
|
301
|
+
})
|
|
302
|
+
})
|
|
232
303
|
|
|
233
304
|
describe('CLI flags', () => {
|
|
234
305
|
test('--help shows usage information', () => {
|
|
235
|
-
const result = runCli(['--help'])
|
|
236
|
-
|
|
237
|
-
expect(result.exitCode).toBe(0)
|
|
238
|
-
expect(result.stdout).toContain('hone')
|
|
239
|
-
expect(result.stdout).toContain('AI Coding Agent Orchestrator')
|
|
240
|
-
})
|
|
306
|
+
const result = runCli(['--help'])
|
|
307
|
+
|
|
308
|
+
expect(result.exitCode).toBe(0)
|
|
309
|
+
expect(result.stdout).toContain('hone')
|
|
310
|
+
expect(result.stdout).toContain('AI Coding Agent Orchestrator')
|
|
311
|
+
})
|
|
241
312
|
|
|
242
313
|
test('--version shows version', () => {
|
|
243
|
-
const result = runCli(['--version'])
|
|
244
|
-
|
|
245
|
-
expect(result.exitCode).toBe(0)
|
|
246
|
-
expect(result.stdout).toContain(
|
|
247
|
-
})
|
|
248
|
-
|
|
314
|
+
const result = runCli(['--version'])
|
|
315
|
+
|
|
316
|
+
expect(result.exitCode).toBe(0)
|
|
317
|
+
expect(result.stdout).toContain(packageJson.version)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
test('-v shows version', () => {
|
|
321
|
+
const result = runCli(['-v'])
|
|
322
|
+
|
|
323
|
+
expect(result.exitCode).toBe(0)
|
|
324
|
+
expect(result.stdout).toContain(packageJson.version)
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
test('unknown option shows help instead of error', () => {
|
|
328
|
+
const result = runCli(['--unknown'])
|
|
329
|
+
|
|
330
|
+
expect(result.exitCode).toBe(0)
|
|
331
|
+
expect(result.stdout).toContain('Usage: hone [options] [command]')
|
|
332
|
+
expect(result.stdout).toContain('AI Coding Agent Orchestrator')
|
|
333
|
+
expect(result.stderr).not.toContain('unknown option')
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
test('unknown command shows help instead of error', () => {
|
|
337
|
+
const result = runCli(['unknown-command'])
|
|
338
|
+
|
|
339
|
+
expect(result.exitCode).toBe(0)
|
|
340
|
+
expect(result.stdout).toContain('Usage: hone [options] [command]')
|
|
341
|
+
expect(result.stdout).toContain('AI Coding Agent Orchestrator')
|
|
342
|
+
expect(result.stderr).not.toContain('unknown command')
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test('multiple unknown options show help instead of error', () => {
|
|
346
|
+
const result = runCli(['--test', '--another'])
|
|
347
|
+
|
|
348
|
+
expect(result.exitCode).toBe(0)
|
|
349
|
+
expect(result.stdout).toContain('Usage: hone [options] [command]')
|
|
350
|
+
expect(result.stdout).toContain('AI Coding Agent Orchestrator')
|
|
351
|
+
})
|
|
352
|
+
})
|
|
249
353
|
|
|
250
354
|
describe('prd-to-tasks command', () => {
|
|
251
355
|
test('shows error when PRD file does not exist', () => {
|
|
252
|
-
const result = runCli(['prd-to-tasks', 'nonexistent-prd.md'])
|
|
253
|
-
|
|
254
|
-
expect(result.exitCode).toBe(1)
|
|
255
|
-
expect(result.stderr).toContain('Error generating tasks')
|
|
256
|
-
})
|
|
356
|
+
const result = runCli(['prd-to-tasks', 'nonexistent-prd.md'])
|
|
357
|
+
|
|
358
|
+
expect(result.exitCode).toBe(1)
|
|
359
|
+
expect(result.stderr).toContain('Error generating tasks')
|
|
360
|
+
})
|
|
257
361
|
|
|
258
362
|
test('shows error when PRD filename format is invalid', () => {
|
|
259
363
|
// Create a file with invalid format
|
|
260
|
-
const plansDir = join(TEST_CWD, '.plans')
|
|
261
|
-
mkdirSync(plansDir, { recursive: true })
|
|
262
|
-
writeFileSync(join(plansDir, 'invalid-format.md'), '# Test')
|
|
263
|
-
|
|
264
|
-
const result = runCli(['prd-to-tasks', join(plansDir, 'invalid-format.md')])
|
|
265
|
-
|
|
266
|
-
expect(result.exitCode).toBe(1)
|
|
267
|
-
expect(result.stderr).toContain('Error generating tasks')
|
|
268
|
-
})
|
|
269
|
-
})
|
|
364
|
+
const plansDir = join(TEST_CWD, '.plans')
|
|
365
|
+
mkdirSync(plansDir, { recursive: true })
|
|
366
|
+
writeFileSync(join(plansDir, 'invalid-format.md'), '# Test')
|
|
367
|
+
|
|
368
|
+
const result = runCli(['prd-to-tasks', join(plansDir, 'invalid-format.md')])
|
|
369
|
+
|
|
370
|
+
expect(result.exitCode).toBe(1)
|
|
371
|
+
expect(result.stderr).toContain('Error generating tasks')
|
|
372
|
+
})
|
|
373
|
+
})
|
|
270
374
|
|
|
271
375
|
describe('run command', () => {
|
|
272
376
|
test('requires iterations flag', () => {
|
|
273
377
|
createMockTaskFile('test-feature', [
|
|
274
|
-
{ id: 'task-1', title: 'Task 1', description: 'Test', status: 'pending', dependencies: [] }
|
|
275
|
-
])
|
|
276
|
-
|
|
277
|
-
const result = runCli(['run', '.plans/tasks-test-feature.yml'])
|
|
278
|
-
|
|
279
|
-
expect(result.exitCode).toBe(1)
|
|
280
|
-
expect(result.stderr).toContain('required option')
|
|
281
|
-
})
|
|
378
|
+
{ id: 'task-1', title: 'Task 1', description: 'Test', status: 'pending', dependencies: [] },
|
|
379
|
+
])
|
|
380
|
+
|
|
381
|
+
const result = runCli(['run', '.plans/tasks-test-feature.yml'])
|
|
382
|
+
|
|
383
|
+
expect(result.exitCode).toBe(1)
|
|
384
|
+
expect(result.stderr).toContain('required option')
|
|
385
|
+
})
|
|
282
386
|
|
|
283
387
|
test('requires valid iterations number', () => {
|
|
284
388
|
createMockTaskFile('test-feature', [
|
|
285
|
-
{ id: 'task-1', title: 'Task 1', description: 'Test', status: 'pending', dependencies: [] }
|
|
286
|
-
])
|
|
287
|
-
|
|
288
|
-
const result = runCli(['run', '.plans/tasks-test-feature.yml', '-i', 'invalid'])
|
|
289
|
-
|
|
389
|
+
{ id: 'task-1', title: 'Task 1', description: 'Test', status: 'pending', dependencies: [] },
|
|
390
|
+
])
|
|
391
|
+
|
|
392
|
+
const result = runCli(['run', '.plans/tasks-test-feature.yml', '-i', 'invalid'])
|
|
393
|
+
|
|
290
394
|
// Should fail because 'invalid' parses to NaN which fails validation
|
|
291
|
-
expect(result.exitCode).toBe(1)
|
|
292
|
-
})
|
|
395
|
+
expect(result.exitCode).toBe(1)
|
|
396
|
+
})
|
|
293
397
|
|
|
294
398
|
test('validates tasks file exists', () => {
|
|
295
|
-
const result = runCli(['run', 'nonexistent.yml', '-i', '1'])
|
|
296
|
-
|
|
297
|
-
expect(result.exitCode).toBe(1)
|
|
298
|
-
expect(result.stderr).toContain('Error executing tasks')
|
|
299
|
-
})
|
|
300
|
-
})
|
|
399
|
+
const result = runCli(['run', 'nonexistent.yml', '-i', '1'])
|
|
400
|
+
|
|
401
|
+
expect(result.exitCode).toBe(1)
|
|
402
|
+
expect(result.stderr).toContain('Error executing tasks')
|
|
403
|
+
})
|
|
404
|
+
})
|
|
301
405
|
|
|
302
406
|
describe('init command', () => {
|
|
303
407
|
test('creates .plans directory and config file in fresh directory', () => {
|
|
304
|
-
const result = runCli(['init'])
|
|
305
|
-
|
|
306
|
-
expect(result.exitCode).toBe(0)
|
|
307
|
-
expect(result.stdout).toContain('Initialized hone successfully!')
|
|
308
|
-
expect(result.stdout).toContain('✓ Created .plans/ directory')
|
|
309
|
-
expect(result.stdout).toContain('✓ Created .plans/hone.config.yml')
|
|
310
|
-
expect(result.stdout).toContain('Next steps:')
|
|
311
|
-
|
|
408
|
+
const result = runCli(['init'])
|
|
409
|
+
|
|
410
|
+
expect(result.exitCode).toBe(0)
|
|
411
|
+
expect(result.stdout).toContain('Initialized hone successfully!')
|
|
412
|
+
expect(result.stdout).toContain('✓ Created .plans/ directory')
|
|
413
|
+
expect(result.stdout).toContain('✓ Created .plans/hone.config.yml')
|
|
414
|
+
expect(result.stdout).toContain('Next steps:')
|
|
415
|
+
|
|
312
416
|
// Verify files were created
|
|
313
|
-
expect(existsSync(join(TEST_CWD, '.plans'))).toBe(true)
|
|
314
|
-
expect(existsSync(join(TEST_CWD, '.plans', 'hone.config.yml'))).toBe(true)
|
|
315
|
-
})
|
|
417
|
+
expect(existsSync(join(TEST_CWD, '.plans'))).toBe(true)
|
|
418
|
+
expect(existsSync(join(TEST_CWD, '.plans', 'hone.config.yml'))).toBe(true)
|
|
419
|
+
})
|
|
316
420
|
|
|
317
421
|
test('detects when already initialized', () => {
|
|
318
422
|
// First init
|
|
319
|
-
runCli(['init'])
|
|
320
|
-
|
|
423
|
+
runCli(['init'])
|
|
424
|
+
|
|
321
425
|
// Second init
|
|
322
|
-
const result = runCli(['init'])
|
|
323
|
-
|
|
324
|
-
expect(result.exitCode).toBe(0)
|
|
325
|
-
expect(result.stdout).toContain('hone is already initialized')
|
|
326
|
-
expect(result.stdout).toContain('.plans/ directory: exists')
|
|
327
|
-
expect(result.stdout).toContain('config file: exists')
|
|
328
|
-
})
|
|
426
|
+
const result = runCli(['init'])
|
|
427
|
+
|
|
428
|
+
expect(result.exitCode).toBe(0)
|
|
429
|
+
expect(result.stdout).toContain('hone is already initialized')
|
|
430
|
+
expect(result.stdout).toContain('.plans/ directory: exists')
|
|
431
|
+
expect(result.stdout).toContain('config file: exists')
|
|
432
|
+
})
|
|
329
433
|
|
|
330
434
|
test('creates only missing parts when partially initialized', () => {
|
|
331
435
|
// Create .plans directory manually
|
|
332
|
-
mkdirSync(join(TEST_CWD, '.plans'), { recursive: true })
|
|
333
|
-
|
|
334
|
-
const result = runCli(['init'])
|
|
335
|
-
|
|
336
|
-
expect(result.exitCode).toBe(0)
|
|
337
|
-
expect(result.stdout).toContain('Initialized hone successfully!')
|
|
338
|
-
expect(result.stdout).toContain('• .plans/ directory already exists')
|
|
339
|
-
expect(result.stdout).toContain('✓ Created .plans/hone.config.yml')
|
|
340
|
-
|
|
436
|
+
mkdirSync(join(TEST_CWD, '.plans'), { recursive: true })
|
|
437
|
+
|
|
438
|
+
const result = runCli(['init'])
|
|
439
|
+
|
|
440
|
+
expect(result.exitCode).toBe(0)
|
|
441
|
+
expect(result.stdout).toContain('Initialized hone successfully!')
|
|
442
|
+
expect(result.stdout).toContain('• .plans/ directory already exists')
|
|
443
|
+
expect(result.stdout).toContain('✓ Created .plans/hone.config.yml')
|
|
444
|
+
|
|
341
445
|
// Verify config was created
|
|
342
|
-
expect(existsSync(join(TEST_CWD, '.plans', 'hone.config.yml'))).toBe(true)
|
|
343
|
-
})
|
|
344
|
-
})
|
|
345
|
-
})
|
|
446
|
+
expect(existsSync(join(TEST_CWD, '.plans', 'hone.config.yml'))).toBe(true)
|
|
447
|
+
})
|
|
448
|
+
})
|
|
449
|
+
})
|