prjct-cli 0.7.1 → 0.7.3

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.
@@ -0,0 +1,296 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import AgentGenerator from '../../domain/agent-generator.js'
3
+ import fs from 'fs/promises'
4
+ import os from 'os'
5
+ import path from 'path'
6
+
7
+ describe('Agent Generator', () => {
8
+ const testProjectId = 'test-agent-gen-' + Date.now()
9
+ let generator
10
+ let agentsDir
11
+
12
+ beforeEach(() => {
13
+ generator = new AgentGenerator(testProjectId)
14
+ agentsDir = path.join(os.homedir(), '.prjct-cli', 'projects', testProjectId, 'agents')
15
+ })
16
+
17
+ afterEach(async () => {
18
+ // Cleanup test files
19
+ try {
20
+ await fs.rm(agentsDir, { recursive: true, force: true })
21
+ } catch (error) {
22
+ // Ignore cleanup errors
23
+ }
24
+ })
25
+
26
+ describe('Constructor', () => {
27
+ it('should create generator with project ID', () => {
28
+ expect(generator.projectId).toBe(testProjectId)
29
+ expect(generator.outputDir).toContain(testProjectId)
30
+ })
31
+
32
+ it('should use fallback directory without project ID', () => {
33
+ const fallbackGenerator = new AgentGenerator()
34
+ expect(fallbackGenerator.outputDir).toContain('.prjct-cli/agents')
35
+ expect(fallbackGenerator.outputDir).not.toContain('projects')
36
+ })
37
+
38
+ it('should construct correct output path', () => {
39
+ expect(generator.outputDir).toBe(agentsDir)
40
+ })
41
+ })
42
+
43
+ describe('generateDynamicAgent()', () => {
44
+ it('should generate agent file', async () => {
45
+ await generator.generateDynamicAgent('test-agent', {
46
+ role: 'Test Agent Role',
47
+ expertise: 'Test Technologies',
48
+ responsibilities: 'Test Responsibilities',
49
+ })
50
+
51
+ const agentFile = path.join(agentsDir, 'test-agent.md')
52
+ const exists = await fs
53
+ .access(agentFile)
54
+ .then(() => true)
55
+ .catch(() => false)
56
+
57
+ expect(exists).toBe(true)
58
+ })
59
+
60
+ it('should create agent with correct content', async () => {
61
+ await generator.generateDynamicAgent('backend-agent', {
62
+ role: 'Backend Developer',
63
+ expertise: 'Node.js, Express, PostgreSQL',
64
+ responsibilities: 'API development and database management',
65
+ })
66
+
67
+ const content = await fs.readFile(path.join(agentsDir, 'backend-agent.md'), 'utf-8')
68
+
69
+ expect(content).toContain('# Backend Developer')
70
+ expect(content).toContain('## Role')
71
+ expect(content).toContain('Backend Developer')
72
+ expect(content).toContain('## Expertise')
73
+ expect(content).toContain('Node.js, Express, PostgreSQL')
74
+ expect(content).toContain('## Responsibilities')
75
+ expect(content).toContain('API development and database management')
76
+ })
77
+
78
+ it('should include project context in agent file', async () => {
79
+ await generator.generateDynamicAgent('context-agent', {
80
+ role: 'Agent with Context',
81
+ expertise: 'Testing',
82
+ responsibilities: 'Test things',
83
+ projectContext: {
84
+ framework: 'React',
85
+ version: '18.0',
86
+ },
87
+ })
88
+
89
+ const content = await fs.readFile(path.join(agentsDir, 'context-agent.md'), 'utf-8')
90
+
91
+ expect(content).toContain('## Project Context')
92
+ expect(content).toContain('framework')
93
+ expect(content).toContain('React')
94
+ expect(content).toContain('version')
95
+ expect(content).toContain('18.0')
96
+ })
97
+
98
+ it('should handle missing optional fields', async () => {
99
+ await generator.generateDynamicAgent('minimal-agent', {
100
+ role: 'Minimal Role',
101
+ })
102
+
103
+ const content = await fs.readFile(path.join(agentsDir, 'minimal-agent.md'), 'utf-8')
104
+
105
+ expect(content).toContain('# Minimal Role')
106
+ expect(content).toContain('Technologies used in this project')
107
+ expect(content).toContain('Handle specific aspects of development')
108
+ expect(content).toContain('No additional context')
109
+ })
110
+
111
+ it('should create output directory if not exists', async () => {
112
+ const newProjectId = 'new-project-' + Date.now()
113
+ const newGenerator = new AgentGenerator(newProjectId)
114
+ const newAgentsDir = path.join(os.homedir(), '.prjct-cli', 'projects', newProjectId, 'agents')
115
+
116
+ await newGenerator.generateDynamicAgent('auto-create', {
117
+ role: 'Auto Created Agent',
118
+ })
119
+
120
+ const exists = await fs
121
+ .access(newAgentsDir)
122
+ .then(() => true)
123
+ .catch(() => false)
124
+
125
+ expect(exists).toBe(true)
126
+
127
+ // Cleanup
128
+ await fs.rm(path.join(os.homedir(), '.prjct-cli', 'projects', newProjectId), {
129
+ recursive: true,
130
+ force: true,
131
+ })
132
+ })
133
+
134
+ it('should create multiple agents', async () => {
135
+ await generator.generateDynamicAgent('agent-1', { role: 'Agent One' })
136
+ await generator.generateDynamicAgent('agent-2', { role: 'Agent Two' })
137
+ await generator.generateDynamicAgent('agent-3', { role: 'Agent Three' })
138
+
139
+ const agents = await generator.listAgents()
140
+
141
+ expect(agents).toHaveLength(3)
142
+ expect(agents).toContain('agent-1')
143
+ expect(agents).toContain('agent-2')
144
+ expect(agents).toContain('agent-3')
145
+ })
146
+
147
+ it('should use agent name as fallback for role', async () => {
148
+ await generator.generateDynamicAgent('fallback-agent', {})
149
+
150
+ const content = await fs.readFile(path.join(agentsDir, 'fallback-agent.md'), 'utf-8')
151
+
152
+ expect(content).toContain('# fallback-agent')
153
+ })
154
+ })
155
+
156
+ describe('cleanupObsoleteAgents()', () => {
157
+ beforeEach(async () => {
158
+ // Create some test agents
159
+ await generator.generateDynamicAgent('agent-1', { role: 'Agent 1' })
160
+ await generator.generateDynamicAgent('agent-2', { role: 'Agent 2' })
161
+ await generator.generateDynamicAgent('agent-3', { role: 'Agent 3' })
162
+ })
163
+
164
+ it('should remove obsolete agents', async () => {
165
+ const removed = await generator.cleanupObsoleteAgents(['agent-1', 'agent-2'])
166
+
167
+ expect(removed).toContain('agent-3')
168
+ expect(removed).toHaveLength(1)
169
+ })
170
+
171
+ it('should keep required agents', async () => {
172
+ await generator.cleanupObsoleteAgents(['agent-1', 'agent-2'])
173
+
174
+ const agents = await generator.listAgents()
175
+
176
+ expect(agents).toContain('agent-1')
177
+ expect(agents).toContain('agent-2')
178
+ expect(agents).not.toContain('agent-3')
179
+ })
180
+
181
+ it('should remove multiple obsolete agents', async () => {
182
+ await generator.generateDynamicAgent('agent-4', { role: 'Agent 4' })
183
+
184
+ const removed = await generator.cleanupObsoleteAgents(['agent-1'])
185
+
186
+ expect(removed).toHaveLength(3)
187
+ expect(removed).toContain('agent-2')
188
+ expect(removed).toContain('agent-3')
189
+ expect(removed).toContain('agent-4')
190
+ })
191
+
192
+ it('should return empty array if all agents are required', async () => {
193
+ const removed = await generator.cleanupObsoleteAgents(['agent-1', 'agent-2', 'agent-3'])
194
+
195
+ expect(removed).toEqual([])
196
+ })
197
+
198
+ it('should handle non-existent directory gracefully', async () => {
199
+ const emptyGenerator = new AgentGenerator('empty-' + Date.now())
200
+
201
+ const removed = await emptyGenerator.cleanupObsoleteAgents(['agent-1'])
202
+
203
+ expect(Array.isArray(removed)).toBe(true)
204
+ })
205
+ })
206
+
207
+ describe('listAgents()', () => {
208
+ it('should list all agents', async () => {
209
+ await generator.generateDynamicAgent('frontend', { role: 'Frontend' })
210
+ await generator.generateDynamicAgent('backend', { role: 'Backend' })
211
+
212
+ const agents = await generator.listAgents()
213
+
214
+ expect(agents).toHaveLength(2)
215
+ expect(agents).toContain('frontend')
216
+ expect(agents).toContain('backend')
217
+ })
218
+
219
+ it('should return empty array for no agents', async () => {
220
+ const agents = await generator.listAgents()
221
+
222
+ expect(agents).toEqual([])
223
+ })
224
+
225
+ it('should ignore non-.md files', async () => {
226
+ await generator.generateDynamicAgent('valid-agent', { role: 'Valid' })
227
+ await fs.writeFile(path.join(agentsDir, 'not-agent.txt'), 'text file')
228
+ await fs.writeFile(path.join(agentsDir, 'config.json'), '{}')
229
+
230
+ const agents = await generator.listAgents()
231
+
232
+ expect(agents).toHaveLength(1)
233
+ expect(agents).toContain('valid-agent')
234
+ })
235
+
236
+ it('should ignore hidden files', async () => {
237
+ await generator.generateDynamicAgent('visible', { role: 'Visible' })
238
+ await fs.writeFile(path.join(agentsDir, '.hidden.md'), 'hidden')
239
+
240
+ const agents = await generator.listAgents()
241
+
242
+ expect(agents).toHaveLength(1)
243
+ expect(agents).toContain('visible')
244
+ })
245
+ })
246
+
247
+ describe('Integration', () => {
248
+ it('should create, list, and cleanup agents', async () => {
249
+ // Create agents
250
+ await generator.generateDynamicAgent('keep-me', { role: 'Keep' })
251
+ await generator.generateDynamicAgent('remove-me', { role: 'Remove' })
252
+
253
+ // Verify they exist
254
+ let agents = await generator.listAgents()
255
+ expect(agents).toHaveLength(2)
256
+
257
+ // Cleanup obsolete
258
+ const removed = await generator.cleanupObsoleteAgents(['keep-me'])
259
+ expect(removed).toContain('remove-me')
260
+
261
+ // Verify cleanup
262
+ agents = await generator.listAgents()
263
+ expect(agents).toHaveLength(1)
264
+ expect(agents).toContain('keep-me')
265
+ })
266
+
267
+ it('should handle agent file content correctly', async () => {
268
+ await generator.generateDynamicAgent('full-agent', {
269
+ role: 'Full Stack Developer',
270
+ expertise: 'React, Node.js, PostgreSQL, Docker',
271
+ responsibilities: 'Build and deploy full stack applications',
272
+ projectContext: {
273
+ stack: 'MERN',
274
+ deployment: 'AWS',
275
+ },
276
+ })
277
+
278
+ const content = await fs.readFile(path.join(agentsDir, 'full-agent.md'), 'utf-8')
279
+
280
+ // Should have all sections
281
+ expect(content).toContain('# Full Stack Developer')
282
+ expect(content).toContain('## Role')
283
+ expect(content).toContain('## Expertise')
284
+ expect(content).toContain('## Responsibilities')
285
+ expect(content).toContain('## Project Context')
286
+ expect(content).toContain('## Guidelines')
287
+
288
+ // Should have all content
289
+ expect(content).toContain('Full Stack Developer')
290
+ expect(content).toContain('React, Node.js, PostgreSQL, Docker')
291
+ expect(content).toContain('Build and deploy full stack applications')
292
+ expect(content).toContain('MERN')
293
+ expect(content).toContain('AWS')
294
+ })
295
+ })
296
+ })
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ describe('Vitest Setup', () => {
4
+ it('should pass basic assertion', () => {
5
+ expect(true).toBe(true)
6
+ })
7
+
8
+ it('should handle numbers', () => {
9
+ expect(2 + 2).toBe(4)
10
+ })
11
+
12
+ it('should handle strings', () => {
13
+ expect('prjct-cli').toContain('prjct')
14
+ })
15
+ })
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import * as dateHelper from '../../utils/date-helper.js'
3
+
4
+ describe('Date Helper', () => {
5
+ describe('formatDate()', () => {
6
+ it('should format date to YYYY-MM-DD', () => {
7
+ const date = new Date(2025, 9, 4, 12, 0, 0) // October 4, 2025
8
+ const formatted = dateHelper.formatDate(date)
9
+
10
+ expect(formatted).toMatch(/^\d{4}-\d{2}-\d{2}$/)
11
+ })
12
+
13
+ it('should pad single digit month', () => {
14
+ const date = new Date(2025, 2, 15) // March 15, 2025
15
+ const formatted = dateHelper.formatDate(date)
16
+
17
+ expect(formatted).toContain('-03-')
18
+ })
19
+
20
+ it('should pad single digit day', () => {
21
+ const date = new Date(2025, 9, 5) // October 5, 2025
22
+ const formatted = dateHelper.formatDate(date)
23
+
24
+ expect(formatted).toContain('-05')
25
+ })
26
+
27
+ it('should handle edge case dates', () => {
28
+ const date = new Date(2025, 0, 1) // January 1, 2025
29
+ const formatted = dateHelper.formatDate(date)
30
+
31
+ expect(formatted).toBe('2025-01-01')
32
+ })
33
+
34
+ it('should handle end of year', () => {
35
+ const date = new Date(2025, 11, 31) // December 31, 2025
36
+ const formatted = dateHelper.formatDate(date)
37
+
38
+ expect(formatted).toBe('2025-12-31')
39
+ })
40
+ })
41
+
42
+ describe('formatMonth()', () => {
43
+ it('should format date to YYYY-MM', () => {
44
+ const date = new Date('2025-10-04')
45
+ const formatted = dateHelper.formatMonth(date)
46
+
47
+ expect(formatted).toMatch(/^\d{4}-\d{2}$/)
48
+ expect(formatted).toBe('2025-10')
49
+ })
50
+
51
+ it('should pad single digit month', () => {
52
+ const date = new Date('2025-03-15')
53
+ const formatted = dateHelper.formatMonth(date)
54
+
55
+ expect(formatted).toBe('2025-03')
56
+ })
57
+
58
+ it('should handle January', () => {
59
+ const date = new Date('2025-01-15')
60
+ const formatted = dateHelper.formatMonth(date)
61
+
62
+ expect(formatted).toBe('2025-01')
63
+ })
64
+
65
+ it('should handle December', () => {
66
+ const date = new Date('2025-12-25')
67
+ const formatted = dateHelper.formatMonth(date)
68
+
69
+ expect(formatted).toBe('2025-12')
70
+ })
71
+ })
72
+
73
+ describe('getTodayKey()', () => {
74
+ it('should return today in YYYY-MM-DD format', () => {
75
+ const today = dateHelper.getTodayKey()
76
+
77
+ expect(today).toMatch(/^\d{4}-\d{2}-\d{2}$/)
78
+ })
79
+
80
+ it('should match formatDate for current date', () => {
81
+ const today = dateHelper.getTodayKey()
82
+ const now = dateHelper.formatDate(new Date())
83
+
84
+ expect(today).toBe(now)
85
+ })
86
+ })
87
+
88
+ describe('getDateKey()', () => {
89
+ it('should return date key in YYYY-MM-DD format', () => {
90
+ const date = new Date(2025, 9, 4) // October 4, 2025
91
+ const key = dateHelper.getDateKey(date)
92
+
93
+ expect(key).toBe('2025-10-04')
94
+ })
95
+
96
+ it('should be alias for formatDate', () => {
97
+ const date = new Date(2025, 9, 4) // October 4, 2025
98
+
99
+ expect(dateHelper.getDateKey(date)).toBe(dateHelper.formatDate(date))
100
+ })
101
+ })
102
+
103
+ describe('getTimestamp()', () => {
104
+ it('should return ISO timestamp', () => {
105
+ const timestamp = dateHelper.getTimestamp()
106
+
107
+ expect(timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
108
+ })
109
+
110
+ it('should be valid date string', () => {
111
+ const timestamp = dateHelper.getTimestamp()
112
+ const date = new Date(timestamp)
113
+
114
+ expect(date.toString()).not.toBe('Invalid Date')
115
+ })
116
+ })
117
+
118
+ describe('calculateDuration()', () => {
119
+ it('should calculate duration in minutes', () => {
120
+ const start = new Date('2025-10-04T10:00:00Z')
121
+ const end = new Date('2025-10-04T10:30:00Z')
122
+ const duration = dateHelper.calculateDuration(start, end)
123
+
124
+ expect(duration).toContain('30m')
125
+ })
126
+
127
+ it('should calculate duration in hours and minutes', () => {
128
+ const start = new Date('2025-10-04T10:00:00Z')
129
+ const end = new Date('2025-10-04T12:30:00Z')
130
+ const duration = dateHelper.calculateDuration(start, end)
131
+
132
+ expect(duration).toContain('2h')
133
+ expect(duration).toContain('30m')
134
+ })
135
+
136
+ it('should handle exact hours', () => {
137
+ const start = new Date('2025-10-04T10:00:00Z')
138
+ const end = new Date('2025-10-04T13:00:00Z')
139
+ const duration = dateHelper.calculateDuration(start, end)
140
+
141
+ expect(duration).toContain('3h')
142
+ })
143
+
144
+ it('should handle less than 1 minute', () => {
145
+ const start = new Date('2025-10-04T10:00:00Z')
146
+ const end = new Date('2025-10-04T10:00:30Z')
147
+ const duration = dateHelper.calculateDuration(start, end)
148
+
149
+ expect(duration).toContain('30s')
150
+ })
151
+ })
152
+
153
+ describe('Edge Cases', () => {
154
+ it('should handle leap year', () => {
155
+ const date = new Date(2024, 1, 29) // February 29, 2024
156
+ const formatted = dateHelper.formatDate(date)
157
+
158
+ expect(formatted).toBe('2024-02-29')
159
+ })
160
+
161
+ it('should handle different years', () => {
162
+ const date2024 = new Date(2024, 9, 4) // October 4, 2024
163
+ const date2025 = new Date(2025, 9, 4) // October 4, 2025
164
+
165
+ expect(dateHelper.formatDate(date2024)).toBe('2024-10-04')
166
+ expect(dateHelper.formatDate(date2025)).toBe('2025-10-04')
167
+ })
168
+ })
169
+ })