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