@renseiai/agentfactory-dashboard 0.8.3 → 0.8.4
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@renseiai/agentfactory-dashboard",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "Premium dashboard UI components for AgentFactory",
|
|
5
5
|
"author": "Rensei AI (https://rensei.ai)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -69,10 +69,13 @@
|
|
|
69
69
|
"react": "^19.0.0",
|
|
70
70
|
"react-dom": "^19.0.0",
|
|
71
71
|
"tailwindcss": "^3.4.0",
|
|
72
|
-
"typescript": "^5"
|
|
72
|
+
"typescript": "^5",
|
|
73
|
+
"vitest": "^3.2.3"
|
|
73
74
|
},
|
|
74
75
|
"scripts": {
|
|
75
76
|
"dev": "cd dev && next dev",
|
|
76
|
-
"typecheck": "tsc --noEmit"
|
|
77
|
+
"typecheck": "tsc --noEmit",
|
|
78
|
+
"test": "vitest run --passWithNoTests",
|
|
79
|
+
"test:watch": "vitest"
|
|
77
80
|
}
|
|
78
81
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { formatDuration, formatCost, formatTokens, formatRelativeTime } from './format.js'
|
|
3
|
+
|
|
4
|
+
describe('formatDuration', () => {
|
|
5
|
+
it('returns "0s" for zero seconds', () => {
|
|
6
|
+
expect(formatDuration(0)).toBe('0s')
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('returns seconds for values under 60', () => {
|
|
10
|
+
expect(formatDuration(30)).toBe('30s')
|
|
11
|
+
expect(formatDuration(1)).toBe('1s')
|
|
12
|
+
expect(formatDuration(59)).toBe('59s')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('returns minutes for values under 3600', () => {
|
|
16
|
+
expect(formatDuration(60)).toBe('1m')
|
|
17
|
+
expect(formatDuration(300)).toBe('5m')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('returns minutes and seconds when remainder exists', () => {
|
|
21
|
+
expect(formatDuration(90)).toBe('1m 30s')
|
|
22
|
+
expect(formatDuration(125)).toBe('2m 5s')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('returns hours for values >= 3600', () => {
|
|
26
|
+
expect(formatDuration(3600)).toBe('1h')
|
|
27
|
+
expect(formatDuration(7200)).toBe('2h')
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('returns hours and minutes when remainder exists', () => {
|
|
31
|
+
expect(formatDuration(5400)).toBe('1h 30m')
|
|
32
|
+
expect(formatDuration(3660)).toBe('1h 1m')
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('formatCost', () => {
|
|
37
|
+
it('returns "$0.00" for null, undefined, or zero', () => {
|
|
38
|
+
expect(formatCost(null)).toBe('$0.00')
|
|
39
|
+
expect(formatCost(undefined)).toBe('$0.00')
|
|
40
|
+
expect(formatCost(0)).toBe('$0.00')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('returns 4 decimal places for sub-cent amounts', () => {
|
|
44
|
+
expect(formatCost(0.001)).toBe('$0.0010')
|
|
45
|
+
expect(formatCost(0.0001)).toBe('$0.0001')
|
|
46
|
+
expect(formatCost(0.0099)).toBe('$0.0099')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('returns 2 decimal places for amounts >= $0.01', () => {
|
|
50
|
+
expect(formatCost(0.01)).toBe('$0.01')
|
|
51
|
+
expect(formatCost(1.23)).toBe('$1.23')
|
|
52
|
+
expect(formatCost(99.99)).toBe('$99.99')
|
|
53
|
+
expect(formatCost(100)).toBe('$100.00')
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('formatTokens', () => {
|
|
58
|
+
it('returns "0" for null, undefined, or zero', () => {
|
|
59
|
+
expect(formatTokens(null)).toBe('0')
|
|
60
|
+
expect(formatTokens(undefined)).toBe('0')
|
|
61
|
+
expect(formatTokens(0)).toBe('0')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('returns raw number for values under 1000', () => {
|
|
65
|
+
expect(formatTokens(1)).toBe('1')
|
|
66
|
+
expect(formatTokens(500)).toBe('500')
|
|
67
|
+
expect(formatTokens(999)).toBe('999')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('returns "k" suffix for values in thousands', () => {
|
|
71
|
+
expect(formatTokens(1000)).toBe('1.0k')
|
|
72
|
+
expect(formatTokens(1500)).toBe('1.5k')
|
|
73
|
+
expect(formatTokens(999_999)).toBe('1000.0k')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('returns "M" suffix for values in millions', () => {
|
|
77
|
+
expect(formatTokens(1_000_000)).toBe('1.00M')
|
|
78
|
+
expect(formatTokens(1_234_567)).toBe('1.23M')
|
|
79
|
+
expect(formatTokens(10_000_000)).toBe('10.00M')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('formatRelativeTime', () => {
|
|
84
|
+
it('returns "just now" for timestamps within 60 seconds', () => {
|
|
85
|
+
const now = new Date().toISOString()
|
|
86
|
+
expect(formatRelativeTime(now)).toBe('just now')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('returns minutes ago for timestamps within the hour', () => {
|
|
90
|
+
const fiveMinAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString()
|
|
91
|
+
expect(formatRelativeTime(fiveMinAgo)).toBe('5m ago')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('returns hours ago for timestamps within the day', () => {
|
|
95
|
+
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString()
|
|
96
|
+
expect(formatRelativeTime(twoHoursAgo)).toBe('2h ago')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('returns days ago for timestamps older than 24 hours', () => {
|
|
100
|
+
const threeDaysAgo = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString()
|
|
101
|
+
expect(formatRelativeTime(threeDaysAgo)).toBe('3d ago')
|
|
102
|
+
})
|
|
103
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getStatusConfig } from './status-config.js'
|
|
3
|
+
import type { SessionStatus, StatusConfig } from './status-config.js'
|
|
4
|
+
|
|
5
|
+
const allStatuses: SessionStatus[] = ['queued', 'parked', 'working', 'completed', 'failed', 'stopped']
|
|
6
|
+
|
|
7
|
+
const requiredFields: (keyof StatusConfig)[] = ['label', 'dotColor', 'textColor', 'bgColor', 'borderColor', 'glowClass']
|
|
8
|
+
|
|
9
|
+
describe('getStatusConfig', () => {
|
|
10
|
+
it.each(allStatuses)('returns a config for status "%s"', (status) => {
|
|
11
|
+
const config = getStatusConfig(status)
|
|
12
|
+
expect(config).toBeDefined()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it.each(allStatuses)('config for "%s" has all required string fields', (status) => {
|
|
16
|
+
const config = getStatusConfig(status)
|
|
17
|
+
for (const field of requiredFields) {
|
|
18
|
+
expect(typeof config[field]).toBe('string')
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it.each(allStatuses)('config for "%s" has an animate boolean', (status) => {
|
|
23
|
+
const config = getStatusConfig(status)
|
|
24
|
+
expect(typeof config.animate).toBe('boolean')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('returns a label matching the status name (capitalized)', () => {
|
|
28
|
+
expect(getStatusConfig('working').label).toBe('Working')
|
|
29
|
+
expect(getStatusConfig('queued').label).toBe('Queued')
|
|
30
|
+
expect(getStatusConfig('parked').label).toBe('Parked')
|
|
31
|
+
expect(getStatusConfig('completed').label).toBe('Completed')
|
|
32
|
+
expect(getStatusConfig('failed').label).toBe('Failed')
|
|
33
|
+
expect(getStatusConfig('stopped').label).toBe('Stopped')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('returns animate: true only for active statuses', () => {
|
|
37
|
+
expect(getStatusConfig('working').animate).toBe(true)
|
|
38
|
+
expect(getStatusConfig('queued').animate).toBe(true)
|
|
39
|
+
expect(getStatusConfig('completed').animate).toBe(false)
|
|
40
|
+
expect(getStatusConfig('failed').animate).toBe(false)
|
|
41
|
+
expect(getStatusConfig('stopped').animate).toBe(false)
|
|
42
|
+
expect(getStatusConfig('parked').animate).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('falls back to queued config for unknown status', () => {
|
|
46
|
+
const config = getStatusConfig('nonexistent' as SessionStatus)
|
|
47
|
+
const queuedConfig = getStatusConfig('queued')
|
|
48
|
+
expect(config).toEqual(queuedConfig)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { getWorkTypeConfig } from './work-type-config.js'
|
|
3
|
+
import type { WorkTypeConfig } from './work-type-config.js'
|
|
4
|
+
|
|
5
|
+
const knownWorkTypes = [
|
|
6
|
+
'development',
|
|
7
|
+
'bugfix',
|
|
8
|
+
'feature',
|
|
9
|
+
'qa',
|
|
10
|
+
'qa-coordination',
|
|
11
|
+
'acceptance',
|
|
12
|
+
'acceptance-coordination',
|
|
13
|
+
'coordination',
|
|
14
|
+
'research',
|
|
15
|
+
'backlog-creation',
|
|
16
|
+
'inflight',
|
|
17
|
+
'refinement',
|
|
18
|
+
'refinement-coordination',
|
|
19
|
+
'refactor',
|
|
20
|
+
'review',
|
|
21
|
+
'docs',
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const requiredFields: (keyof WorkTypeConfig)[] = ['label', 'color', 'bgColor', 'borderColor']
|
|
25
|
+
|
|
26
|
+
describe('getWorkTypeConfig', () => {
|
|
27
|
+
it.each(knownWorkTypes)('returns a config for work type "%s"', (workType) => {
|
|
28
|
+
const config = getWorkTypeConfig(workType)
|
|
29
|
+
expect(config).toBeDefined()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it.each(knownWorkTypes)('config for "%s" has all required string fields', (workType) => {
|
|
33
|
+
const config = getWorkTypeConfig(workType)
|
|
34
|
+
for (const field of requiredFields) {
|
|
35
|
+
expect(typeof config[field]).toBe('string')
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it.each(knownWorkTypes)('config for "%s" has non-empty label', (workType) => {
|
|
40
|
+
const config = getWorkTypeConfig(workType)
|
|
41
|
+
expect(config.label.length).toBeGreaterThan(0)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('is case-insensitive', () => {
|
|
45
|
+
expect(getWorkTypeConfig('Development')).toEqual(getWorkTypeConfig('development'))
|
|
46
|
+
expect(getWorkTypeConfig('QA')).toEqual(getWorkTypeConfig('qa'))
|
|
47
|
+
expect(getWorkTypeConfig('BUGFIX')).toEqual(getWorkTypeConfig('bugfix'))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('returns default config for unknown work types', () => {
|
|
51
|
+
const config = getWorkTypeConfig('nonexistent')
|
|
52
|
+
expect(config.label).toBe('Unknown')
|
|
53
|
+
expect(config.color).toBeDefined()
|
|
54
|
+
expect(config.bgColor).toBeDefined()
|
|
55
|
+
expect(config.borderColor).toBeDefined()
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('returns distinct labels for each known work type', () => {
|
|
59
|
+
const labels = knownWorkTypes.map((wt) => getWorkTypeConfig(wt).label)
|
|
60
|
+
const uniqueLabels = new Set(labels)
|
|
61
|
+
expect(uniqueLabels.size).toBe(labels.length)
|
|
62
|
+
})
|
|
63
|
+
})
|