claw-dashboard 2.1.1 → 2.2.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 +29 -1
- package/docs/API.md +1 -2
- package/index.js +31 -41
- package/package.json +22 -12
- package/src/auto-save.js +11 -5
- package/src/cli/export-snapshot.js +3 -1
- package/src/cli/import-snapshot.js +3 -1
- package/src/config.js +9 -15
- package/src/errors.js +0 -9
- package/src/security.js +6 -2
- package/src/snapshot.js +73 -26
- package/src/web-server.js +7 -10
- package/.c8rc.json +0 -38
- package/.dockerignore +0 -68
- package/.github/dependabot.yml +0 -45
- package/.github/pull_request_template.md +0 -39
- package/.github/workflows/ci.yml +0 -147
- package/.github/workflows/release.yml +0 -40
- package/.github/workflows/security.yml +0 -84
- package/.husky/pre-commit +0 -1
- package/.planning/codebase/ARCHITECTURE.md +0 -206
- package/.planning/codebase/INTEGRATIONS.md +0 -150
- package/.planning/codebase/STACK.md +0 -122
- package/.planning/codebase/STRUCTURE.md +0 -201
- package/CONTRIBUTING.md +0 -378
- package/Dockerfile +0 -54
- package/FEATURES.md +0 -307
- package/TODO.md +0 -28
- package/ai.openclaw.dashboard.plist +0 -35
- package/build-cjs.js +0 -127
- package/cjs-shim.js +0 -13
- package/dist/clawdash +0 -1729
- package/dist/clawdash.meta.json +0 -2236
- package/dist/widgets.cjs +0 -6511
- package/docker-compose.yml +0 -67
- package/esbuild.config.js +0 -158
- package/eslint.config.js +0 -56
- package/examples/plugins/README.md +0 -122
- package/examples/plugins/api-status/index.js +0 -294
- package/examples/plugins/api-status/plugin.json +0 -19
- package/examples/plugins/hello-world/index.js +0 -104
- package/examples/plugins/hello-world/plugin.json +0 -15
- package/examples/plugins/system-metrics-chart/index.js +0 -339
- package/examples/plugins/system-metrics-chart/plugin.json +0 -18
- package/examples/plugins/weather-widget/index.js +0 -136
- package/examples/plugins/weather-widget/plugin.json +0 -19
- package/index.cjs +0 -23005
- package/jest.config.js +0 -11
- package/scripts/release.js +0 -595
- package/src/database.js +0 -734
- package/tests/alerts.test.js +0 -437
- package/tests/auto-save.test.js +0 -529
- package/tests/cache.test.js +0 -317
- package/tests/cli.test.js +0 -351
- package/tests/command-palette.test.js +0 -320
- package/tests/config-processor.test.js +0 -452
- package/tests/config-validator.test.js +0 -710
- package/tests/config-watcher.test.js +0 -594
- package/tests/config.test.js +0 -755
- package/tests/database.test.js +0 -438
- package/tests/errors.test.js +0 -189
- package/tests/example-plugins.test.js +0 -624
- package/tests/integration.test.js +0 -1321
- package/tests/loading-states.test.js +0 -300
- package/tests/manifest-validation-on-load.test.js +0 -671
- package/tests/performance-monitor.test.js +0 -190
- package/tests/plugin-api-rate-limit.test.js +0 -302
- package/tests/plugin-errors.test.js +0 -311
- package/tests/plugin-lifecycle-e2e.test.js +0 -1036
- package/tests/plugin-reload.test.js +0 -764
- package/tests/rate-limiter.test.js +0 -539
- package/tests/removed-dependencies.test.js +0 -74
- package/tests/retry.test.js +0 -308
- package/tests/security.test.js +0 -411
- package/tests/settings-modal.test.js +0 -226
- package/tests/theme-selector.test.js +0 -57
- package/tests/utils.js +0 -242
- package/tests/utils.test.js +0 -317
- package/tests/validate-plugin-cli.test.js +0 -359
- package/tests/validation.test.js +0 -837
- package/tests/web-server.test.js +0 -646
- package/tests/widget-arrange-mode.test.js +0 -183
- package/tests/widget-config-hot-reload.test.js +0 -465
- package/tests/widget-dependency.test.js +0 -591
- package/tests/widget-error-boundary.test.js +0 -749
- package/tests/widget-error-isolation.test.js +0 -469
- package/tests/widget-integration.test.js +0 -1147
- package/tests/widget-refresh-intervals.test.js +0 -284
- package/tests/worker-pool.test.js +0 -522
package/tests/utils.js
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Test utilities extracted from index.js
|
|
3
|
-
* These are pure functions that can be unit tested without the Dashboard class
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import os from 'os';
|
|
7
|
-
import { resolve, join } from 'path';
|
|
8
|
-
|
|
9
|
-
// Color constants (same as in index.js)
|
|
10
|
-
const C = {
|
|
11
|
-
green: 'green', brightGreen: 'bright-green',
|
|
12
|
-
yellow: 'yellow', brightYellow: 'bright-yellow',
|
|
13
|
-
red: 'red', brightRed: 'bright-red',
|
|
14
|
-
cyan: 'cyan', brightCyan: 'bright-cyan',
|
|
15
|
-
magenta: 'magenta', brightMagenta: 'bright-magenta',
|
|
16
|
-
blue: 'blue', brightBlue: 'bright-blue',
|
|
17
|
-
white: 'white', brightWhite: 'bright-white',
|
|
18
|
-
gray: 'gray', black: 'black'
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Generate a gauge bar visualization
|
|
23
|
-
* @param {number} percent - Percentage (0-100)
|
|
24
|
-
* @param {number} width - Width of the gauge in characters
|
|
25
|
-
* @returns {string} Gauge visualization
|
|
26
|
-
*/
|
|
27
|
-
function gauge(percent, width = 15) {
|
|
28
|
-
const filled = Math.round((percent / 100) * width);
|
|
29
|
-
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Generate a sparkline from data points
|
|
34
|
-
* @param {number[]} data - Array of numeric values
|
|
35
|
-
* @param {number} width - Max width of sparkline
|
|
36
|
-
* @returns {string} Sparkline visualization
|
|
37
|
-
*/
|
|
38
|
-
function sparkline(data, width = 15) {
|
|
39
|
-
if (!data || data.length === 0) return '─'.repeat(width);
|
|
40
|
-
const chars = '▁▂▃▄▅▆▇█';
|
|
41
|
-
const max = Math.max(...data, 1);
|
|
42
|
-
const recent = data.slice(-width);
|
|
43
|
-
return recent.map(v => {
|
|
44
|
-
const normalized = Math.max(0, Math.min(1, v / max));
|
|
45
|
-
return chars[Math.floor(normalized * (chars.length - 1))];
|
|
46
|
-
}).join('');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Get color based on percentage threshold
|
|
51
|
-
* @param {number} percent - Percentage value
|
|
52
|
-
* @returns {string} Color name
|
|
53
|
-
*/
|
|
54
|
-
function getColor(percent) {
|
|
55
|
-
if (percent >= 80) return C.red;
|
|
56
|
-
if (percent >= 60) return C.yellow;
|
|
57
|
-
return C.green;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Format bytes to human-readable string
|
|
62
|
-
* @param {number} bytes - Number of bytes
|
|
63
|
-
* @returns {string} Formatted string (e.g., "1.5 GB")
|
|
64
|
-
*/
|
|
65
|
-
function formatBytes(bytes) {
|
|
66
|
-
if (bytes === 0) return '0 B';
|
|
67
|
-
const k = 1024;
|
|
68
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
69
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
70
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Format bytes per second to bits per second
|
|
75
|
-
* @param {number} bytesPerSec - Bytes per second
|
|
76
|
-
* @returns {string} Formatted string (e.g., "1.5M")
|
|
77
|
-
*/
|
|
78
|
-
function formatBitsPerSecond(bytesPerSec) {
|
|
79
|
-
const bitsPerSec = bytesPerSec * 8;
|
|
80
|
-
if (bitsPerSec === 0) return '0';
|
|
81
|
-
if (bitsPerSec < 1000) return Math.round(bitsPerSec) + 'b';
|
|
82
|
-
if (bitsPerSec < 1000000) return (bitsPerSec / 1000).toFixed(0) + 'K';
|
|
83
|
-
return (bitsPerSec / 1000000).toFixed(1) + 'M';
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Format duration in seconds to human-readable string
|
|
88
|
-
* @param {number} seconds - Duration in seconds
|
|
89
|
-
* @returns {string} Formatted duration (e.g., "2d 3h")
|
|
90
|
-
*/
|
|
91
|
-
function formatDuration(seconds) {
|
|
92
|
-
if (!seconds || seconds < 0) return '--';
|
|
93
|
-
const days = Math.floor(seconds / 86400);
|
|
94
|
-
const hours = Math.floor((seconds % 86400) / 3600);
|
|
95
|
-
const mins = Math.floor((seconds % 3600) / 60);
|
|
96
|
-
if (days > 0) return `${days}d ${hours}h`;
|
|
97
|
-
if (hours > 0) return `${hours}h ${mins}m`;
|
|
98
|
-
return `${mins}m`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Calculate tokens per second between two sessions
|
|
103
|
-
* @param {object} session - Current session
|
|
104
|
-
* @param {object} prevSession - Previous session
|
|
105
|
-
* @param {number} elapsedMs - Elapsed time in milliseconds
|
|
106
|
-
* @returns {number|null} Tokens per second or null
|
|
107
|
-
*/
|
|
108
|
-
function calcTPS(session, prevSession, elapsedMs) {
|
|
109
|
-
if (!session || !prevSession || elapsedMs < 100) return null;
|
|
110
|
-
const currTokens = session.totalTokens || 0;
|
|
111
|
-
const prevTokens = prevSession.totalTokens || 0;
|
|
112
|
-
const diff = currTokens - prevTokens;
|
|
113
|
-
if (diff <= 0) return null;
|
|
114
|
-
const tps = diff / (elapsedMs / 1000);
|
|
115
|
-
return tps > 0 ? parseFloat(tps.toFixed(1)) : null;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Validate file path for security
|
|
120
|
-
* @param {string} filePath - Path to validate
|
|
121
|
-
* @param {string[]} allowedDirs - Additional allowed directories
|
|
122
|
-
* @returns {object} Validation result
|
|
123
|
-
*/
|
|
124
|
-
function validateFilePath(filePath, allowedDirs = []) {
|
|
125
|
-
try {
|
|
126
|
-
if (!filePath || typeof filePath !== 'string') {
|
|
127
|
-
return { valid: false, resolvedPath: filePath, error: "Invalid file path" };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const normalizedPath = filePath.startsWith('~')
|
|
131
|
-
? join(os.homedir(), filePath.slice(1))
|
|
132
|
-
: filePath;
|
|
133
|
-
|
|
134
|
-
const resolvedPath = resolve(normalizedPath);
|
|
135
|
-
|
|
136
|
-
if (filePath.includes("..")) {
|
|
137
|
-
return { valid: false, resolvedPath, error: "Path traversal not allowed" };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const homeDir = os.homedir();
|
|
141
|
-
const defaultAllowedDirs = [
|
|
142
|
-
homeDir,
|
|
143
|
-
homeDir + "/.openclaw",
|
|
144
|
-
homeDir + "/.openclaw/agents",
|
|
145
|
-
"/tmp"
|
|
146
|
-
];
|
|
147
|
-
|
|
148
|
-
const allAllowedDirs = [...defaultAllowedDirs, ...allowedDirs];
|
|
149
|
-
|
|
150
|
-
const isAllowed = allAllowedDirs.some(allowedDir => {
|
|
151
|
-
const resolvedAllowed = resolve(allowedDir);
|
|
152
|
-
return resolvedPath.startsWith(resolvedAllowed + "/") || resolvedPath === resolvedAllowed;
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
if (!isAllowed) {
|
|
156
|
-
return { valid: false, resolvedPath, error: "Path not in allowed directories" };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return { valid: true, resolvedPath };
|
|
160
|
-
} catch (err) {
|
|
161
|
-
return { valid: false, resolvedPath: filePath, error: err.message };
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Colorize log line based on log level
|
|
167
|
-
* @param {string} line - Log line to colorize
|
|
168
|
-
* @returns {string} Colorized log line
|
|
169
|
-
*/
|
|
170
|
-
function colorizeLogLine(line) {
|
|
171
|
-
if (!line || typeof line !== 'string') return line;
|
|
172
|
-
|
|
173
|
-
let matchedLevel = null;
|
|
174
|
-
let levelStart = -1;
|
|
175
|
-
let levelEnd = -1;
|
|
176
|
-
|
|
177
|
-
for (const level of ['error', 'warn', 'info', 'debug']) {
|
|
178
|
-
const escapedLevel = level.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
179
|
-
const pattern = new RegExp(`\\[${escapedLevel.toUpperCase()}\\]`, 'i');
|
|
180
|
-
const match = line.match(pattern);
|
|
181
|
-
if (match) {
|
|
182
|
-
matchedLevel = level;
|
|
183
|
-
levelStart = match.index;
|
|
184
|
-
levelEnd = levelStart + match[0].length;
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (!matchedLevel) {
|
|
190
|
-
for (const level of ['error', 'warn', 'info', 'debug']) {
|
|
191
|
-
const escapedLevel = level.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
192
|
-
const pattern = new RegExp(`^\\d{4}-\\d{2}-\\d{2}[T ].*?${escapedLevel}:`, 'i');
|
|
193
|
-
const match = line.match(pattern);
|
|
194
|
-
if (match) {
|
|
195
|
-
matchedLevel = level;
|
|
196
|
-
levelStart = match.index;
|
|
197
|
-
const afterMatch = line.slice(match.index + match[0].length);
|
|
198
|
-
const spaceMatch = afterMatch.match(/^(\s*)/);
|
|
199
|
-
levelEnd = match.index + match[0].length + (spaceMatch ? spaceMatch[1].length : 0);
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (!matchedLevel) return line;
|
|
206
|
-
|
|
207
|
-
const LOG_COLORS = {
|
|
208
|
-
error: C.brightRed,
|
|
209
|
-
warn: C.brightYellow,
|
|
210
|
-
info: C.cyan,
|
|
211
|
-
debug: C.gray
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const color = LOG_COLORS[matchedLevel] || C.white;
|
|
215
|
-
const colorTag = `{${color}-fg}`;
|
|
216
|
-
const resetTag = '{/}';
|
|
217
|
-
|
|
218
|
-
return line.slice(0, levelStart) + colorTag + line.slice(levelStart, levelEnd) + resetTag + line.slice(levelEnd);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Convert camelCase to dash-case for tag colors
|
|
223
|
-
* @param {string} color - Color name in camelCase
|
|
224
|
-
* @returns {string} Color name in dash-case
|
|
225
|
-
*/
|
|
226
|
-
function toTagColor(color) {
|
|
227
|
-
return color.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
export {
|
|
231
|
-
C,
|
|
232
|
-
gauge,
|
|
233
|
-
sparkline,
|
|
234
|
-
getColor,
|
|
235
|
-
formatBytes,
|
|
236
|
-
formatBitsPerSecond,
|
|
237
|
-
formatDuration,
|
|
238
|
-
calcTPS,
|
|
239
|
-
validateFilePath,
|
|
240
|
-
colorizeLogLine,
|
|
241
|
-
toTagColor
|
|
242
|
-
};
|
package/tests/utils.test.js
DELETED
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for utility functions
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
gauge,
|
|
7
|
-
sparkline,
|
|
8
|
-
getColor,
|
|
9
|
-
formatBytes,
|
|
10
|
-
formatBitsPerSecond,
|
|
11
|
-
formatDuration,
|
|
12
|
-
calcTPS,
|
|
13
|
-
validateFilePath,
|
|
14
|
-
colorizeLogLine,
|
|
15
|
-
toTagColor
|
|
16
|
-
} from './utils.js';
|
|
17
|
-
|
|
18
|
-
describe('gauge', () => {
|
|
19
|
-
test('returns empty gauge for 0%', () => {
|
|
20
|
-
const result = gauge(0, 10);
|
|
21
|
-
expect(result).toBe('░'.repeat(10));
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
test('returns full gauge for 100%', () => {
|
|
25
|
-
const result = gauge(100, 10);
|
|
26
|
-
expect(result).toBe('█'.repeat(10));
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('returns half gauge for 50%', () => {
|
|
30
|
-
const result = gauge(50, 10);
|
|
31
|
-
expect(result).toBe('█'.repeat(5) + '░'.repeat(5));
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test('handles decimal percentages correctly', () => {
|
|
35
|
-
const result = gauge(33, 10);
|
|
36
|
-
// 33% of 10 = 3.3 rounded = 3 filled
|
|
37
|
-
expect(result.startsWith('███')).toBe(true);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('defaults to width 15', () => {
|
|
41
|
-
const result = gauge(50);
|
|
42
|
-
expect(result.length).toBe(15);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test('handles boundary values', () => {
|
|
46
|
-
expect(gauge(1, 10).length).toBe(10);
|
|
47
|
-
expect(gauge(99, 10).length).toBe(10);
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
describe('sparkline', () => {
|
|
52
|
-
test('returns dashes for empty array', () => {
|
|
53
|
-
const result = sparkline([]);
|
|
54
|
-
expect(result).toBe('─'.repeat(15));
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test('returns dashes for null/undefined', () => {
|
|
58
|
-
expect(sparkline(null)).toBe('─'.repeat(15));
|
|
59
|
-
expect(sparkline(undefined)).toBe('─'.repeat(15));
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test('returns single character for single value', () => {
|
|
63
|
-
const result = sparkline([100], 10);
|
|
64
|
-
expect(result.length).toBe(1);
|
|
65
|
-
expect(result).toBe('█');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('handles increasing values', () => {
|
|
69
|
-
const data = [10, 20, 30, 40, 50];
|
|
70
|
-
const result = sparkline(data, 5);
|
|
71
|
-
expect(result.length).toBe(5);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('handles decreasing values', () => {
|
|
75
|
-
const data = [50, 40, 30, 20, 10];
|
|
76
|
-
const result = sparkline(data, 5);
|
|
77
|
-
expect(result.length).toBe(5);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('uses only recent data when array is larger than width', () => {
|
|
81
|
-
const data = Array.from({ length: 30 }, (_, i) => i * 10);
|
|
82
|
-
const result = sparkline(data, 10);
|
|
83
|
-
expect(result.length).toBe(10);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('handles all same values', () => {
|
|
87
|
-
const data = [50, 50, 50, 50, 50];
|
|
88
|
-
const result = sparkline(data, 5);
|
|
89
|
-
// Should use middle character for 50% value
|
|
90
|
-
expect(result.length).toBe(5);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
describe('getColor', () => {
|
|
95
|
-
test('returns green for low percentages', () => {
|
|
96
|
-
expect(getColor(0)).toBe('green');
|
|
97
|
-
expect(getColor(30)).toBe('green');
|
|
98
|
-
expect(getColor(59)).toBe('green');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
test('returns yellow for medium percentages', () => {
|
|
102
|
-
expect(getColor(60)).toBe('yellow');
|
|
103
|
-
expect(getColor(70)).toBe('yellow');
|
|
104
|
-
expect(getColor(79)).toBe('yellow');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('returns red for high percentages', () => {
|
|
108
|
-
expect(getColor(80)).toBe('red');
|
|
109
|
-
expect(getColor(90)).toBe('red');
|
|
110
|
-
expect(getColor(100)).toBe('red');
|
|
111
|
-
});
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
describe('formatBytes', () => {
|
|
115
|
-
test('formats 0 bytes', () => {
|
|
116
|
-
expect(formatBytes(0)).toBe('0 B');
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test('formats bytes correctly', () => {
|
|
120
|
-
expect(formatBytes(512)).toBe('512 B');
|
|
121
|
-
expect(formatBytes(1023)).toBe('1023 B');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test('formats kilobytes correctly', () => {
|
|
125
|
-
expect(formatBytes(1024)).toBe('1 KB');
|
|
126
|
-
expect(formatBytes(1536)).toBe('1.5 KB');
|
|
127
|
-
expect(formatBytes(10240)).toBe('10 KB');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test('formats megabytes correctly', () => {
|
|
131
|
-
expect(formatBytes(1048576)).toBe('1 MB');
|
|
132
|
-
expect(formatBytes(1572864)).toBe('1.5 MB');
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test('formats gigabytes correctly', () => {
|
|
136
|
-
expect(formatBytes(1073741824)).toBe('1 GB');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test('handles large values', () => {
|
|
140
|
-
const result = formatBytes(5000000000);
|
|
141
|
-
expect(result).toContain('GB');
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe('formatBitsPerSecond', () => {
|
|
146
|
-
test('formats 0 bps', () => {
|
|
147
|
-
expect(formatBitsPerSecond(0)).toBe('0');
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('formats bits correctly', () => {
|
|
151
|
-
expect(formatBitsPerSecond(1)).toBe('8b');
|
|
152
|
-
expect(formatBitsPerSecond(100)).toBe('800b');
|
|
153
|
-
expect(formatBitsPerSecond(125)).toBe('1K');
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
test('formats kilobits correctly', () => {
|
|
157
|
-
expect(formatBitsPerSecond(125)).toBe('1K');
|
|
158
|
-
expect(formatBitsPerSecond(1250)).toBe('10K');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test('formats megabits correctly', () => {
|
|
162
|
-
expect(formatBitsPerSecond(125000)).toBe('1.0M');
|
|
163
|
-
expect(formatBitsPerSecond(1250000)).toBe('10.0M');
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe('formatDuration', () => {
|
|
168
|
-
test('returns -- for null/undefined', () => {
|
|
169
|
-
expect(formatDuration(null)).toBe('--');
|
|
170
|
-
expect(formatDuration(undefined)).toBe('--');
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
test('returns -- for negative values', () => {
|
|
174
|
-
expect(formatDuration(-1)).toBe('--');
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test('formats minutes correctly', () => {
|
|
178
|
-
expect(formatDuration(60)).toBe('1m');
|
|
179
|
-
expect(formatDuration(300)).toBe('5m');
|
|
180
|
-
expect(formatDuration(3599)).toBe('59m');
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test('formats hours correctly', () => {
|
|
184
|
-
expect(formatDuration(3600)).toBe('1h 0m');
|
|
185
|
-
expect(formatDuration(7200)).toBe('2h 0m');
|
|
186
|
-
expect(formatDuration(3660)).toBe('1h 1m');
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test('formats days correctly', () => {
|
|
190
|
-
expect(formatDuration(86400)).toBe('1d 0h');
|
|
191
|
-
expect(formatDuration(90000)).toBe('1d 1h');
|
|
192
|
-
expect(formatDuration(172800)).toBe('2d 0h');
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
describe('calcTPS', () => {
|
|
197
|
-
test('returns null for missing sessions', () => {
|
|
198
|
-
expect(calcTPS(null, {}, 1000)).toBeNull();
|
|
199
|
-
expect(calcTPS({}, null, 1000)).toBeNull();
|
|
200
|
-
expect(calcTPS({}, {}, 1000)).toBeNull();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test('returns null for elapsed time less than 100ms', () => {
|
|
204
|
-
const session = { totalTokens: 100 };
|
|
205
|
-
const prevSession = { totalTokens: 0 };
|
|
206
|
-
expect(calcTPS(session, prevSession, 50)).toBeNull();
|
|
207
|
-
expect(calcTPS(session, prevSession, 99)).toBeNull();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test('returns null when tokens decreased', () => {
|
|
211
|
-
const session = { totalTokens: 50 };
|
|
212
|
-
const prevSession = { totalTokens: 100 };
|
|
213
|
-
expect(calcTPS(session, prevSession, 1000)).toBeNull();
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test('returns null when tokens are equal', () => {
|
|
217
|
-
const session = { totalTokens: 100 };
|
|
218
|
-
const prevSession = { totalTokens: 100 };
|
|
219
|
-
expect(calcTPS(session, prevSession, 1000)).toBeNull();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
test('calculates TPS correctly', () => {
|
|
223
|
-
const session = { totalTokens: 1000 };
|
|
224
|
-
const prevSession = { totalTokens: 0 };
|
|
225
|
-
// 1000 tokens in 1 second = 1000 TPS
|
|
226
|
-
const result = calcTPS(session, prevSession, 1000);
|
|
227
|
-
expect(result).toBe(1000);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
test('handles missing totalTokens property (defaults to 0)', () => {
|
|
231
|
-
const session = {};
|
|
232
|
-
const prevSession = {};
|
|
233
|
-
expect(calcTPS(session, prevSession, 1000)).toBeNull(); // 0 - 0 = 0, so null
|
|
234
|
-
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
describe('validateFilePath', () => {
|
|
238
|
-
// Note: validateFilePath now uses ESM imports instead of require()
|
|
239
|
-
// These tests verify basic behavior
|
|
240
|
-
|
|
241
|
-
test('rejects null/undefined paths', () => {
|
|
242
|
-
expect(validateFilePath(null).valid).toBe(false);
|
|
243
|
-
expect(validateFilePath(undefined).valid).toBe(false);
|
|
244
|
-
expect(validateFilePath('').valid).toBe(false);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
test('rejects path traversal attempts', () => {
|
|
248
|
-
// This works because it checks for ".." in the path
|
|
249
|
-
const result = validateFilePath('/home/user/../../../etc/passwd');
|
|
250
|
-
expect(result.valid).toBe(false);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
test('rejects paths outside allowed directories', () => {
|
|
254
|
-
const result = validateFilePath('/usr/bin/test');
|
|
255
|
-
expect(result.valid).toBe(false);
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
describe('colorizeLogLine', () => {
|
|
260
|
-
test('returns unchanged for null/undefined', () => {
|
|
261
|
-
expect(colorizeLogLine(null)).toBeNull();
|
|
262
|
-
expect(colorizeLogLine(undefined)).toBeUndefined();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test('returns unchanged for non-string', () => {
|
|
266
|
-
expect(colorizeLogLine(123)).toBe(123);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
test('colorizes error lines with brackets', () => {
|
|
270
|
-
const line = '[ERROR] Something went wrong';
|
|
271
|
-
const result = colorizeLogLine(line);
|
|
272
|
-
expect(result).toContain('{bright-red-fg}');
|
|
273
|
-
expect(result).toContain('{/}');
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test('colorizes warn lines with brackets', () => {
|
|
277
|
-
const line = '[WARN] Warning message';
|
|
278
|
-
const result = colorizeLogLine(line);
|
|
279
|
-
expect(result).toContain('{bright-yellow-fg}');
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
test('colorizes info lines with brackets', () => {
|
|
283
|
-
const line = '[INFO] Info message';
|
|
284
|
-
const result = colorizeLogLine(line);
|
|
285
|
-
expect(result).toContain('{cyan-fg}');
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
test('colorizes debug lines with brackets', () => {
|
|
289
|
-
const line = '[DEBUG] Debug message';
|
|
290
|
-
const result = colorizeLogLine(line);
|
|
291
|
-
expect(result).toContain('{gray-fg}');
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test('handles lowercase brackets', () => {
|
|
295
|
-
const line = '[error] Lowercase error';
|
|
296
|
-
const result = colorizeLogLine(line);
|
|
297
|
-
expect(result).toContain('{bright-red-fg}');
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test('returns unchanged for lines without log levels', () => {
|
|
301
|
-
const line = 'Just a regular message';
|
|
302
|
-
const result = colorizeLogLine(line);
|
|
303
|
-
expect(result).toBe(line);
|
|
304
|
-
});
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
describe('toTagColor', () => {
|
|
308
|
-
test('converts camelCase to dash-case', () => {
|
|
309
|
-
expect(toTagColor('brightRed')).toBe('bright-red');
|
|
310
|
-
expect(toTagColor('brightGreen')).toBe('bright-green');
|
|
311
|
-
expect(toTagColor('brightYellow')).toBe('bright-yellow');
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
test('handles single words', () => {
|
|
315
|
-
expect(toTagColor('red')).toBe('red');
|
|
316
|
-
});
|
|
317
|
-
});
|