prjct-cli 0.7.2 → 0.8.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/CHANGELOG.md +224 -0
- package/CLAUDE.md +219 -2
- package/README.md +32 -0
- package/core/__tests__/agentic/command-executor.test.js +223 -0
- package/core/__tests__/agentic/context-builder.test.js +160 -0
- package/core/__tests__/agentic/prompt-builder.test.js +212 -0
- package/core/__tests__/agentic/template-loader.test.js +164 -0
- package/core/__tests__/agentic/tool-registry.test.js +243 -0
- package/core/__tests__/domain/agent-generator.test.js +296 -0
- package/core/__tests__/setup.test.js +15 -0
- package/core/__tests__/utils/date-helper.test.js +169 -0
- package/core/__tests__/utils/file-helper.test.js +258 -0
- package/core/command-registry.js +52 -4
- package/core/commands.js +0 -3
- package/core/infrastructure/session-manager.js +0 -1
- package/core/utils/file-helper.js +2 -0
- package/package.json +8 -3
- package/templates/commands/ask.md +386 -0
- package/templates/commands/feature.md +127 -1
- package/templates/commands/help.md +330 -20
- package/templates/commands/init.md +116 -9
- package/templates/commands/suggest.md +555 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import commandExecutor from '../../agentic/command-executor.js'
|
|
3
|
+
|
|
4
|
+
describe('Command Executor', () => {
|
|
5
|
+
const testProjectPath = process.cwd()
|
|
6
|
+
|
|
7
|
+
describe('execute()', () => {
|
|
8
|
+
it('should execute command successfully', async () => {
|
|
9
|
+
const result = await commandExecutor.execute('now', {}, testProjectPath)
|
|
10
|
+
|
|
11
|
+
expect(result).toBeDefined()
|
|
12
|
+
expect(result.success).toBe(true)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('should load template', async () => {
|
|
16
|
+
const result = await commandExecutor.execute('now', {}, testProjectPath)
|
|
17
|
+
|
|
18
|
+
expect(result.template).toBeDefined()
|
|
19
|
+
expect(result.template).toHaveProperty('content')
|
|
20
|
+
expect(result.template).toHaveProperty('frontmatter')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('should build context', async () => {
|
|
24
|
+
const result = await commandExecutor.execute('now', {}, testProjectPath)
|
|
25
|
+
|
|
26
|
+
expect(result.context).toBeDefined()
|
|
27
|
+
expect(result.context).toHaveProperty('projectId')
|
|
28
|
+
expect(result.context).toHaveProperty('projectPath')
|
|
29
|
+
expect(result.context).toHaveProperty('paths')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should load state', async () => {
|
|
33
|
+
const result = await commandExecutor.execute('now', {}, testProjectPath)
|
|
34
|
+
|
|
35
|
+
expect(result.state).toBeDefined()
|
|
36
|
+
expect(typeof result.state).toBe('object')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('should build prompt', async () => {
|
|
40
|
+
const result = await commandExecutor.execute('now', {}, testProjectPath)
|
|
41
|
+
|
|
42
|
+
expect(result.prompt).toBeDefined()
|
|
43
|
+
expect(typeof result.prompt).toBe('string')
|
|
44
|
+
expect(result.prompt.length).toBeGreaterThan(0)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should pass parameters to context', async () => {
|
|
48
|
+
const params = { taskName: 'Test Task', feature: 'Test Feature' }
|
|
49
|
+
const result = await commandExecutor.execute('now', params, testProjectPath)
|
|
50
|
+
|
|
51
|
+
expect(result.context.params).toEqual(params)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should handle different commands', async () => {
|
|
55
|
+
const commands = ['now', 'done', 'next', 'ship']
|
|
56
|
+
|
|
57
|
+
for (const cmd of commands) {
|
|
58
|
+
const result = await commandExecutor.execute(cmd, {}, testProjectPath)
|
|
59
|
+
expect(result.success).toBe(true)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should handle non-existent command', async () => {
|
|
64
|
+
const result = await commandExecutor.execute('nonexistent', {}, testProjectPath)
|
|
65
|
+
|
|
66
|
+
expect(result.success).toBe(false)
|
|
67
|
+
expect(result.error).toBeDefined()
|
|
68
|
+
expect(result.error).toContain('Template not found')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should include all execution data', async () => {
|
|
72
|
+
const result = await commandExecutor.execute('now', {}, testProjectPath)
|
|
73
|
+
|
|
74
|
+
expect(result).toHaveProperty('success')
|
|
75
|
+
expect(result).toHaveProperty('template')
|
|
76
|
+
expect(result).toHaveProperty('context')
|
|
77
|
+
expect(result).toHaveProperty('state')
|
|
78
|
+
expect(result).toHaveProperty('prompt')
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('executeTool()', () => {
|
|
83
|
+
it('should execute allowed tool', async () => {
|
|
84
|
+
const allowedTools = ['Read', 'Write', 'Bash']
|
|
85
|
+
const result = await commandExecutor.executeTool('Read', [__filename], allowedTools)
|
|
86
|
+
|
|
87
|
+
expect(result).toBeDefined()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should throw error for disallowed tool', async () => {
|
|
91
|
+
const allowedTools = ['Read']
|
|
92
|
+
|
|
93
|
+
await expect(commandExecutor.executeTool('Write', ['file.txt', 'content'], allowedTools)).rejects.toThrow(
|
|
94
|
+
'Tool Write not allowed for this command'
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should execute Read tool', async () => {
|
|
99
|
+
const allowedTools = ['Read']
|
|
100
|
+
const content = await commandExecutor.executeTool('Read', [__filename], allowedTools)
|
|
101
|
+
|
|
102
|
+
expect(content).toBeDefined()
|
|
103
|
+
expect(content).toContain('Command Executor')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('should handle tool errors', async () => {
|
|
107
|
+
const allowedTools = ['Read']
|
|
108
|
+
|
|
109
|
+
await expect(commandExecutor.executeTool('UnknownTool', [], allowedTools)).rejects.toThrow()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should check permissions before execution', async () => {
|
|
113
|
+
const allowedTools = ['Read']
|
|
114
|
+
|
|
115
|
+
// Bash not allowed
|
|
116
|
+
await expect(commandExecutor.executeTool('Bash', ['echo test'], allowedTools)).rejects.toThrow(
|
|
117
|
+
'not allowed'
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('executeSimple()', () => {
|
|
123
|
+
it('should execute simple command', async () => {
|
|
124
|
+
const executionFn = async (_tools, _context) => {
|
|
125
|
+
return { message: 'Executed successfully' }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const result = await commandExecutor.executeSimple('now', executionFn, testProjectPath)
|
|
129
|
+
|
|
130
|
+
expect(result.success).toBe(true)
|
|
131
|
+
expect(result.result).toEqual({ message: 'Executed successfully' })
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('should provide tools to execution function', async () => {
|
|
135
|
+
const executionFn = async (_tools, _context) => {
|
|
136
|
+
expect(_tools).toHaveProperty('read')
|
|
137
|
+
expect(_tools).toHaveProperty('write')
|
|
138
|
+
expect(_tools).toHaveProperty('bash')
|
|
139
|
+
return { tools: 'available' }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const result = await commandExecutor.executeSimple('now', executionFn, testProjectPath)
|
|
143
|
+
|
|
144
|
+
expect(result.success).toBe(true)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should provide context to execution function', async () => {
|
|
148
|
+
const executionFn = async (_tools, _context) => {
|
|
149
|
+
expect(_context).toBeDefined()
|
|
150
|
+
expect(_context).toHaveProperty('projectId')
|
|
151
|
+
expect(_context).toHaveProperty('projectPath')
|
|
152
|
+
return { context: 'received' }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const result = await commandExecutor.executeSimple('now', executionFn, testProjectPath)
|
|
156
|
+
|
|
157
|
+
expect(result.success).toBe(true)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should enforce tool permissions', async () => {
|
|
161
|
+
const executionFn = async (_tools, _context) => {
|
|
162
|
+
// Try to use a disallowed tool
|
|
163
|
+
await _tools.write('/test/file.txt', 'content')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const result = await commandExecutor.executeSimple('now', executionFn, testProjectPath)
|
|
167
|
+
|
|
168
|
+
// Should fail if Write is not in allowed tools for 'now'
|
|
169
|
+
expect(result.success).toBe(false)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('should handle execution errors', async () => {
|
|
173
|
+
const executionFn = async (_tools, _context) => {
|
|
174
|
+
throw new Error('Execution failed')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const result = await commandExecutor.executeSimple('now', executionFn, testProjectPath)
|
|
178
|
+
|
|
179
|
+
expect(result.success).toBe(false)
|
|
180
|
+
expect(result.error).toContain('Execution failed')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('should work with allowed tools', async () => {
|
|
184
|
+
const executionFn = async (_tools, _context) => {
|
|
185
|
+
// Read is typically allowed
|
|
186
|
+
const content = await _tools.read(__filename)
|
|
187
|
+
return { readSuccess: content !== null }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const result = await commandExecutor.executeSimple('now', executionFn, testProjectPath)
|
|
191
|
+
|
|
192
|
+
expect(result.success).toBe(true)
|
|
193
|
+
expect(result.result.readSuccess).toBe(true)
|
|
194
|
+
})
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
describe('Integration', () => {
|
|
198
|
+
it('should execute full command flow', async () => {
|
|
199
|
+
const result = await commandExecutor.execute('now', { task: 'Test Task' }, testProjectPath)
|
|
200
|
+
|
|
201
|
+
expect(result.success).toBe(true)
|
|
202
|
+
expect(result.template).toBeDefined()
|
|
203
|
+
expect(result.context).toBeDefined()
|
|
204
|
+
expect(result.state).toBeDefined()
|
|
205
|
+
expect(result.prompt).toBeDefined()
|
|
206
|
+
|
|
207
|
+
// Prompt should include the template content
|
|
208
|
+
expect(result.prompt).toContain(result.template.content)
|
|
209
|
+
|
|
210
|
+
// Context should include parameters
|
|
211
|
+
expect(result.context.params.task).toBe('Test Task')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('should build proper prompt structure', async () => {
|
|
215
|
+
const result = await commandExecutor.execute('now', {}, testProjectPath)
|
|
216
|
+
|
|
217
|
+
expect(result.prompt).toContain('# Command Instructions')
|
|
218
|
+
expect(result.prompt).toContain('## Project Context')
|
|
219
|
+
expect(result.prompt).toContain('## Current State')
|
|
220
|
+
expect(result.prompt).toContain('## Execute')
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
})
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import contextBuilder from '../../agentic/context-builder.js'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
describe('Context Builder', () => {
|
|
6
|
+
const testProjectPath = process.cwd()
|
|
7
|
+
|
|
8
|
+
describe('build()', () => {
|
|
9
|
+
it('should build context with all required fields', async () => {
|
|
10
|
+
const context = await contextBuilder.build(testProjectPath)
|
|
11
|
+
|
|
12
|
+
expect(context).toBeDefined()
|
|
13
|
+
expect(context).toHaveProperty('projectId')
|
|
14
|
+
expect(context).toHaveProperty('projectPath')
|
|
15
|
+
expect(context).toHaveProperty('globalPath')
|
|
16
|
+
expect(context).toHaveProperty('paths')
|
|
17
|
+
expect(context).toHaveProperty('params')
|
|
18
|
+
expect(context).toHaveProperty('timestamp')
|
|
19
|
+
expect(context).toHaveProperty('date')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should include all file paths', async () => {
|
|
23
|
+
const context = await contextBuilder.build(testProjectPath)
|
|
24
|
+
|
|
25
|
+
expect(context.paths).toHaveProperty('now')
|
|
26
|
+
expect(context.paths).toHaveProperty('next')
|
|
27
|
+
expect(context.paths).toHaveProperty('context')
|
|
28
|
+
expect(context.paths).toHaveProperty('shipped')
|
|
29
|
+
expect(context.paths).toHaveProperty('metrics')
|
|
30
|
+
expect(context.paths).toHaveProperty('ideas')
|
|
31
|
+
expect(context.paths).toHaveProperty('roadmap')
|
|
32
|
+
expect(context.paths).toHaveProperty('memory')
|
|
33
|
+
expect(context.paths).toHaveProperty('analysis')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should use correct project path', async () => {
|
|
37
|
+
const context = await contextBuilder.build(testProjectPath)
|
|
38
|
+
|
|
39
|
+
expect(context.projectPath).toBe(testProjectPath)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should include command parameters', async () => {
|
|
43
|
+
const params = { taskName: 'test task', feature: 'test feature' }
|
|
44
|
+
const context = await contextBuilder.build(testProjectPath, params)
|
|
45
|
+
|
|
46
|
+
expect(context.params).toEqual(params)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should include timestamp', async () => {
|
|
50
|
+
const context = await contextBuilder.build(testProjectPath)
|
|
51
|
+
|
|
52
|
+
expect(context.timestamp).toBeDefined()
|
|
53
|
+
expect(typeof context.timestamp).toBe('string')
|
|
54
|
+
expect(new Date(context.timestamp).toString()).not.toBe('Invalid Date')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should include date', async () => {
|
|
58
|
+
const context = await contextBuilder.build(testProjectPath)
|
|
59
|
+
|
|
60
|
+
expect(context.date).toBeDefined()
|
|
61
|
+
expect(typeof context.date).toBe('string')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should build global path from project ID', async () => {
|
|
65
|
+
const context = await contextBuilder.build(testProjectPath)
|
|
66
|
+
|
|
67
|
+
expect(context.globalPath).toContain('.prjct-cli')
|
|
68
|
+
expect(context.globalPath).toContain('projects')
|
|
69
|
+
expect(context.globalPath).toContain(context.projectId)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe('loadState()', () => {
|
|
74
|
+
it('should load state from context', async () => {
|
|
75
|
+
const context = await contextBuilder.build(testProjectPath)
|
|
76
|
+
const state = await contextBuilder.loadState(context)
|
|
77
|
+
|
|
78
|
+
expect(state).toBeDefined()
|
|
79
|
+
expect(typeof state).toBe('object')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should return null for non-existent files', async () => {
|
|
83
|
+
const context = await contextBuilder.build(testProjectPath)
|
|
84
|
+
const state = await contextBuilder.loadState(context)
|
|
85
|
+
|
|
86
|
+
// Some files might not exist
|
|
87
|
+
Object.values(state).forEach((value) => {
|
|
88
|
+
expect(value === null || typeof value === 'string').toBe(true)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should load existing files as strings', async () => {
|
|
93
|
+
const context = await contextBuilder.build(testProjectPath)
|
|
94
|
+
const state = await contextBuilder.loadState(context)
|
|
95
|
+
|
|
96
|
+
// At least some state values should be strings (if files exist)
|
|
97
|
+
const hasStrings = Object.values(state).some((value) => typeof value === 'string')
|
|
98
|
+
expect(typeof hasStrings).toBe('boolean')
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('fileExists()', () => {
|
|
103
|
+
it('should return true for existing file', async () => {
|
|
104
|
+
const exists = await contextBuilder.fileExists(__filename)
|
|
105
|
+
|
|
106
|
+
expect(exists).toBe(true)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should return false for non-existent file', async () => {
|
|
110
|
+
const exists = await contextBuilder.fileExists('/nonexistent/path/file.txt')
|
|
111
|
+
|
|
112
|
+
expect(exists).toBe(false)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should work with package.json', async () => {
|
|
116
|
+
const packagePath = path.join(process.cwd(), 'package.json')
|
|
117
|
+
const exists = await contextBuilder.fileExists(packagePath)
|
|
118
|
+
|
|
119
|
+
expect(exists).toBe(true)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
describe('Path Construction', () => {
|
|
124
|
+
it('should construct paths with correct structure', async () => {
|
|
125
|
+
const context = await contextBuilder.build(testProjectPath)
|
|
126
|
+
|
|
127
|
+
expect(context.paths.now).toContain('core/now.md')
|
|
128
|
+
expect(context.paths.next).toContain('core/next.md')
|
|
129
|
+
expect(context.paths.context).toContain('core/context.md')
|
|
130
|
+
expect(context.paths.shipped).toContain('progress/shipped.md')
|
|
131
|
+
expect(context.paths.metrics).toContain('progress/metrics.md')
|
|
132
|
+
expect(context.paths.ideas).toContain('planning/ideas.md')
|
|
133
|
+
expect(context.paths.roadmap).toContain('planning/roadmap.md')
|
|
134
|
+
expect(context.paths.memory).toContain('memory/context.jsonl')
|
|
135
|
+
expect(context.paths.analysis).toContain('analysis/repo-summary.md')
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should use global path for all file paths', async () => {
|
|
139
|
+
const context = await contextBuilder.build(testProjectPath)
|
|
140
|
+
|
|
141
|
+
Object.values(context.paths).forEach((filePath) => {
|
|
142
|
+
expect(filePath).toContain(context.globalPath)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('Empty Parameters', () => {
|
|
148
|
+
it('should handle empty command params', async () => {
|
|
149
|
+
const context = await contextBuilder.build(testProjectPath, {})
|
|
150
|
+
|
|
151
|
+
expect(context.params).toEqual({})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should handle undefined command params', async () => {
|
|
155
|
+
const context = await contextBuilder.build(testProjectPath)
|
|
156
|
+
|
|
157
|
+
expect(context.params).toEqual({})
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import promptBuilder from '../../agentic/prompt-builder.js'
|
|
3
|
+
|
|
4
|
+
describe('Prompt Builder', () => {
|
|
5
|
+
const mockTemplate = {
|
|
6
|
+
frontmatter: {
|
|
7
|
+
'allowed-tools': ['Read', 'Write', 'Bash'],
|
|
8
|
+
},
|
|
9
|
+
content: '# Command: Test\n\nExecute test command.',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const mockContext = {
|
|
13
|
+
projectId: 'test-project-123',
|
|
14
|
+
projectPath: '/test/path',
|
|
15
|
+
globalPath: '/global/path',
|
|
16
|
+
timestamp: '2025-10-04T12:00:00Z',
|
|
17
|
+
date: '2025-10-04',
|
|
18
|
+
params: {},
|
|
19
|
+
paths: {
|
|
20
|
+
now: '/global/path/core/now.md',
|
|
21
|
+
next: '/global/path/core/next.md',
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const mockState = {
|
|
26
|
+
now: '# Current Task\n\nTest task in progress',
|
|
27
|
+
next: '# Priority Queue\n\nTask 1, Task 2',
|
|
28
|
+
context: null, // Simulate non-existent file
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('build()', () => {
|
|
32
|
+
it('should build a complete prompt', () => {
|
|
33
|
+
const prompt = promptBuilder.build(mockTemplate, mockContext, mockState)
|
|
34
|
+
|
|
35
|
+
expect(prompt).toBeDefined()
|
|
36
|
+
expect(typeof prompt).toBe('string')
|
|
37
|
+
expect(prompt.length).toBeGreaterThan(0)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should include command instructions', () => {
|
|
41
|
+
const prompt = promptBuilder.build(mockTemplate, mockContext, mockState)
|
|
42
|
+
|
|
43
|
+
expect(prompt).toContain('# Command Instructions')
|
|
44
|
+
expect(prompt).toContain('Execute test command')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should include allowed tools', () => {
|
|
48
|
+
const prompt = promptBuilder.build(mockTemplate, mockContext, mockState)
|
|
49
|
+
|
|
50
|
+
expect(prompt).toContain('## Allowed Tools')
|
|
51
|
+
expect(prompt).toContain('Read, Write, Bash')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should include project context', () => {
|
|
55
|
+
const prompt = promptBuilder.build(mockTemplate, mockContext, mockState)
|
|
56
|
+
|
|
57
|
+
expect(prompt).toContain('## Project Context')
|
|
58
|
+
expect(prompt).toContain('Project ID: test-project-123')
|
|
59
|
+
expect(prompt).toContain('Timestamp: 2025-10-04T12:00:00Z')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should include current state', () => {
|
|
63
|
+
const prompt = promptBuilder.build(mockTemplate, mockContext, mockState)
|
|
64
|
+
|
|
65
|
+
expect(prompt).toContain('## Current State')
|
|
66
|
+
expect(prompt).toContain('### now')
|
|
67
|
+
expect(prompt).toContain('Test task in progress')
|
|
68
|
+
expect(prompt).toContain('### next')
|
|
69
|
+
expect(prompt).toContain('Task 1, Task 2')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should exclude null or empty state values', () => {
|
|
73
|
+
const prompt = promptBuilder.build(mockTemplate, mockContext, mockState)
|
|
74
|
+
|
|
75
|
+
// Should not include 'context' since it's null
|
|
76
|
+
expect(prompt).not.toContain('### context')
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should include parameters when present', () => {
|
|
80
|
+
const contextWithParams = {
|
|
81
|
+
...mockContext,
|
|
82
|
+
params: {
|
|
83
|
+
taskName: 'Test Task',
|
|
84
|
+
feature: 'Test Feature',
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const prompt = promptBuilder.build(mockTemplate, contextWithParams, mockState)
|
|
89
|
+
|
|
90
|
+
expect(prompt).toContain('## Parameters')
|
|
91
|
+
expect(prompt).toContain('taskName: Test Task')
|
|
92
|
+
expect(prompt).toContain('feature: Test Feature')
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should exclude parameters section when empty', () => {
|
|
96
|
+
const prompt = promptBuilder.build(mockTemplate, mockContext, mockState)
|
|
97
|
+
|
|
98
|
+
// Should not include Parameters section since params is empty
|
|
99
|
+
const hasParametersSection = prompt.includes('## Parameters')
|
|
100
|
+
expect(hasParametersSection).toBe(false)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should include final execution instructions', () => {
|
|
104
|
+
const prompt = promptBuilder.build(mockTemplate, mockContext, mockState)
|
|
105
|
+
|
|
106
|
+
expect(prompt).toContain('## Execute')
|
|
107
|
+
expect(prompt).toContain('execute the command')
|
|
108
|
+
expect(prompt).toContain('Use ONLY the allowed tools')
|
|
109
|
+
expect(prompt).toContain('do not follow rigid if/else rules')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should handle template without allowed-tools', () => {
|
|
113
|
+
const templateNoTools = {
|
|
114
|
+
frontmatter: {},
|
|
115
|
+
content: 'Simple command',
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const prompt = promptBuilder.build(templateNoTools, mockContext, mockState)
|
|
119
|
+
|
|
120
|
+
expect(prompt).toBeDefined()
|
|
121
|
+
expect(prompt).toContain('Simple command')
|
|
122
|
+
// Should not have Allowed Tools section
|
|
123
|
+
expect(prompt).not.toContain('## Allowed Tools')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should handle empty state', () => {
|
|
127
|
+
const emptyState = {}
|
|
128
|
+
|
|
129
|
+
const prompt = promptBuilder.build(mockTemplate, mockContext, emptyState)
|
|
130
|
+
|
|
131
|
+
expect(prompt).toBeDefined()
|
|
132
|
+
expect(prompt).toContain('## Current State')
|
|
133
|
+
// But no actual state entries
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('should format state content in code blocks', () => {
|
|
137
|
+
const prompt = promptBuilder.build(mockTemplate, mockContext, mockState)
|
|
138
|
+
|
|
139
|
+
expect(prompt).toContain('```')
|
|
140
|
+
expect(prompt).toMatch(/```\n# Current Task/)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('buildAnalysis()', () => {
|
|
145
|
+
it('should build analysis prompt', () => {
|
|
146
|
+
const prompt = promptBuilder.buildAnalysis('repository', mockContext)
|
|
147
|
+
|
|
148
|
+
expect(prompt).toBeDefined()
|
|
149
|
+
expect(typeof prompt).toBe('string')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('should include analysis type', () => {
|
|
153
|
+
const prompt = promptBuilder.buildAnalysis('repository', mockContext)
|
|
154
|
+
|
|
155
|
+
expect(prompt).toContain('# Analyze: repository')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('should include project context', () => {
|
|
159
|
+
const prompt = promptBuilder.buildAnalysis('repository', mockContext)
|
|
160
|
+
|
|
161
|
+
expect(prompt).toContain('## Project Context')
|
|
162
|
+
expect(prompt).toContain('Path: /test/path')
|
|
163
|
+
expect(prompt).toContain('ID: test-project-123')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should include analysis instructions', () => {
|
|
167
|
+
const prompt = promptBuilder.buildAnalysis('repository', mockContext)
|
|
168
|
+
|
|
169
|
+
expect(prompt).toContain('Read the project context')
|
|
170
|
+
expect(prompt).toContain('provide your analysis')
|
|
171
|
+
expect(prompt).toContain('No predetermined patterns')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should work with different analysis types', () => {
|
|
175
|
+
const types = ['repository', 'feature', 'bug', 'performance']
|
|
176
|
+
|
|
177
|
+
types.forEach((type) => {
|
|
178
|
+
const prompt = promptBuilder.buildAnalysis(type, mockContext)
|
|
179
|
+
expect(prompt).toContain(`# Analyze: ${type}`)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('Prompt Structure', () => {
|
|
185
|
+
it('should have clear sections in order', () => {
|
|
186
|
+
const prompt = promptBuilder.build(mockTemplate, mockContext, mockState)
|
|
187
|
+
|
|
188
|
+
const instructionsIndex = prompt.indexOf('# Command Instructions')
|
|
189
|
+
const toolsIndex = prompt.indexOf('## Allowed Tools')
|
|
190
|
+
const contextIndex = prompt.indexOf('## Project Context')
|
|
191
|
+
const stateIndex = prompt.indexOf('## Current State')
|
|
192
|
+
const executeIndex = prompt.indexOf('## Execute')
|
|
193
|
+
|
|
194
|
+
expect(instructionsIndex).toBeGreaterThan(-1)
|
|
195
|
+
expect(toolsIndex).toBeGreaterThan(instructionsIndex)
|
|
196
|
+
expect(contextIndex).toBeGreaterThan(toolsIndex)
|
|
197
|
+
expect(stateIndex).toBeGreaterThan(contextIndex)
|
|
198
|
+
expect(executeIndex).toBeGreaterThan(stateIndex)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('should use proper markdown formatting', () => {
|
|
202
|
+
const prompt = promptBuilder.build(mockTemplate, mockContext, mockState)
|
|
203
|
+
|
|
204
|
+
// Should have proper headings
|
|
205
|
+
expect(prompt).toMatch(/^# /m)
|
|
206
|
+
expect(prompt).toMatch(/^## /m)
|
|
207
|
+
|
|
208
|
+
// Should have code blocks
|
|
209
|
+
expect(prompt).toContain('```')
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
})
|