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.
- package/CHANGELOG.md +111 -0
- 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/commands.js +0 -3
- package/core/infrastructure/agent-detector.js +2 -0
- package/core/infrastructure/session-manager.js +0 -1
- package/core/utils/file-helper.js +2 -0
- package/package.json +8 -3
|
@@ -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
|
+
})
|