claw-dashboard 2.1.0 → 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.
Files changed (90) hide show
  1. package/CHANGELOG.md +29 -1
  2. package/docs/API.md +1 -2
  3. package/index.js +32 -42
  4. package/package.json +23 -12
  5. package/src/auto-save.js +11 -5
  6. package/src/cli/export-snapshot.js +3 -1
  7. package/src/cli/import-snapshot.js +3 -1
  8. package/src/config.js +9 -15
  9. package/src/errors.js +0 -9
  10. package/src/security.js +6 -2
  11. package/src/snapshot.js +73 -26
  12. package/src/web-server.js +7 -10
  13. package/src/widgets/builtin-widgets.js +0 -94
  14. package/src/widgets/index.js +0 -1
  15. package/.c8rc.json +0 -38
  16. package/.dockerignore +0 -68
  17. package/.github/dependabot.yml +0 -45
  18. package/.github/pull_request_template.md +0 -39
  19. package/.github/workflows/ci.yml +0 -147
  20. package/.github/workflows/release.yml +0 -40
  21. package/.github/workflows/security.yml +0 -84
  22. package/.husky/pre-commit +0 -1
  23. package/.planning/codebase/ARCHITECTURE.md +0 -206
  24. package/.planning/codebase/INTEGRATIONS.md +0 -150
  25. package/.planning/codebase/STACK.md +0 -122
  26. package/.planning/codebase/STRUCTURE.md +0 -201
  27. package/CONTRIBUTING.md +0 -378
  28. package/Dockerfile +0 -54
  29. package/FEATURES.md +0 -307
  30. package/TODO.md +0 -28
  31. package/ai.openclaw.dashboard.plist +0 -35
  32. package/build-cjs.js +0 -127
  33. package/cjs-shim.js +0 -13
  34. package/dist/clawdash +0 -1729
  35. package/dist/clawdash.meta.json +0 -2236
  36. package/dist/widgets.cjs +0 -6511
  37. package/docker-compose.yml +0 -67
  38. package/esbuild.config.js +0 -158
  39. package/eslint.config.js +0 -56
  40. package/examples/plugins/README.md +0 -122
  41. package/examples/plugins/api-status/index.js +0 -294
  42. package/examples/plugins/api-status/plugin.json +0 -19
  43. package/examples/plugins/hello-world/index.js +0 -104
  44. package/examples/plugins/hello-world/plugin.json +0 -15
  45. package/examples/plugins/system-metrics-chart/index.js +0 -339
  46. package/examples/plugins/system-metrics-chart/plugin.json +0 -18
  47. package/examples/plugins/weather-widget/index.js +0 -136
  48. package/examples/plugins/weather-widget/plugin.json +0 -19
  49. package/index.cjs +0 -23005
  50. package/jest.config.js +0 -11
  51. package/scripts/release.js +0 -595
  52. package/src/database.js +0 -734
  53. package/tests/alerts.test.js +0 -437
  54. package/tests/auto-save.test.js +0 -529
  55. package/tests/cache.test.js +0 -317
  56. package/tests/cli.test.js +0 -351
  57. package/tests/command-palette.test.js +0 -320
  58. package/tests/config-processor.test.js +0 -452
  59. package/tests/config-validator.test.js +0 -710
  60. package/tests/config-watcher.test.js +0 -594
  61. package/tests/config.test.js +0 -755
  62. package/tests/database.test.js +0 -438
  63. package/tests/errors.test.js +0 -189
  64. package/tests/example-plugins.test.js +0 -624
  65. package/tests/integration.test.js +0 -1321
  66. package/tests/loading-states.test.js +0 -300
  67. package/tests/manifest-validation-on-load.test.js +0 -671
  68. package/tests/performance-monitor.test.js +0 -190
  69. package/tests/plugin-api-rate-limit.test.js +0 -302
  70. package/tests/plugin-errors.test.js +0 -311
  71. package/tests/plugin-lifecycle-e2e.test.js +0 -1036
  72. package/tests/plugin-reload.test.js +0 -764
  73. package/tests/rate-limiter.test.js +0 -539
  74. package/tests/retry.test.js +0 -308
  75. package/tests/security.test.js +0 -411
  76. package/tests/settings-modal.test.js +0 -226
  77. package/tests/theme-selector.test.js +0 -57
  78. package/tests/utils.js +0 -242
  79. package/tests/utils.test.js +0 -317
  80. package/tests/validate-plugin-cli.test.js +0 -359
  81. package/tests/validation.test.js +0 -837
  82. package/tests/web-server.test.js +0 -646
  83. package/tests/widget-arrange-mode.test.js +0 -183
  84. package/tests/widget-config-hot-reload.test.js +0 -465
  85. package/tests/widget-dependency.test.js +0 -591
  86. package/tests/widget-error-boundary.test.js +0 -749
  87. package/tests/widget-error-isolation.test.js +0 -469
  88. package/tests/widget-integration.test.js +0 -1147
  89. package/tests/widget-refresh-intervals.test.js +0 -284
  90. 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
- };
@@ -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
- });