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.
- package/CHANGELOG.md +29 -1
- package/docs/API.md +1 -2
- package/index.js +32 -42
- package/package.json +23 -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/src/widgets/builtin-widgets.js +0 -94
- package/src/widgets/index.js +0 -1
- 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/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
|
@@ -1,594 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for config-watcher module
|
|
3
|
-
* Debouncing, polling, reload events, and file watching
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
ConfigWatcher,
|
|
8
|
-
createConfigWatcher,
|
|
9
|
-
watchSettingsFile,
|
|
10
|
-
watchPluginsDirectory,
|
|
11
|
-
getConfigWatcher,
|
|
12
|
-
DEFAULT_WATCHER_OPTIONS,
|
|
13
|
-
} from '../src/config-watcher.js';
|
|
14
|
-
|
|
15
|
-
import { writeFileSync, unlinkSync, mkdirSync, rmdirSync, existsSync, readdirSync } from 'fs';
|
|
16
|
-
import { join } from 'path';
|
|
17
|
-
import { mkdtempSync } from 'fs';
|
|
18
|
-
import { tmpdir } from 'os';
|
|
19
|
-
|
|
20
|
-
// Simple mock function for tests
|
|
21
|
-
const createMockHandler = () => {
|
|
22
|
-
const calls = [];
|
|
23
|
-
const fn = (...args) => {
|
|
24
|
-
calls.push(args);
|
|
25
|
-
};
|
|
26
|
-
fn.getCalls = () => calls;
|
|
27
|
-
fn.mockClear = () => {
|
|
28
|
-
calls.length = 0;
|
|
29
|
-
};
|
|
30
|
-
return fn;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
describe('config-watcher', () => {
|
|
34
|
-
let testDir;
|
|
35
|
-
|
|
36
|
-
beforeEach(() => {
|
|
37
|
-
// Create a temporary directory for test files
|
|
38
|
-
testDir = mkdtempSync(join(tmpdir(), 'config-watcher-test-'));
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
afterEach(async () => {
|
|
42
|
-
// Clean up: stop all watchers and remove temp directory
|
|
43
|
-
try {
|
|
44
|
-
const { rmdirSync, unlinkSync, readdirSync } = await import('fs');
|
|
45
|
-
|
|
46
|
-
// Stop any existing watchers
|
|
47
|
-
const watcher = getConfigWatcher();
|
|
48
|
-
if (watcher) {
|
|
49
|
-
watcher.unwatchAll();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Remove test directory
|
|
53
|
-
if (existsSync(testDir)) {
|
|
54
|
-
const files = readdirSync(testDir);
|
|
55
|
-
for (const file of files) {
|
|
56
|
-
unlinkSync(join(testDir, file));
|
|
57
|
-
}
|
|
58
|
-
rmdirSync(testDir);
|
|
59
|
-
}
|
|
60
|
-
} catch (e) {
|
|
61
|
-
// Ignore cleanup errors
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe('DEFAULT_WATCHER_OPTIONS', () => {
|
|
66
|
-
test('should have correct default values', () => {
|
|
67
|
-
expect(DEFAULT_WATCHER_OPTIONS.debounceMs).toBe(500);
|
|
68
|
-
expect(DEFAULT_WATCHER_OPTIONS.persistent).toBe(true);
|
|
69
|
-
expect(DEFAULT_WATCHER_OPTIONS.encoding).toBe('utf8');
|
|
70
|
-
expect(DEFAULT_WATCHER_OPTIONS.usePolling).toBe(false);
|
|
71
|
-
expect(DEFAULT_WATCHER_OPTIONS.pollInterval).toBe(1000);
|
|
72
|
-
expect(DEFAULT_WATCHER_OPTIONS.ignoreInitial).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe('ConfigWatcher constructor', () => {
|
|
77
|
-
test('should create instance with default options', () => {
|
|
78
|
-
const watcher = new ConfigWatcher();
|
|
79
|
-
expect(watcher.options).toEqual(DEFAULT_WATCHER_OPTIONS);
|
|
80
|
-
expect(watcher.watchers.size).toBe(0);
|
|
81
|
-
expect(watcher.pollWatchers.size).toBe(0);
|
|
82
|
-
expect(watcher.watchedFiles.size).toBe(0);
|
|
83
|
-
expect(watcher.isRunning).toBe(false);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('should merge custom options with defaults', () => {
|
|
87
|
-
const customOptions = { debounceMs: 1000, usePolling: true };
|
|
88
|
-
const watcher = new ConfigWatcher(customOptions);
|
|
89
|
-
expect(watcher.options.debounceMs).toBe(1000);
|
|
90
|
-
expect(watcher.options.usePolling).toBe(true);
|
|
91
|
-
expect(watcher.options.pollInterval).toBe(DEFAULT_WATCHER_OPTIONS.pollInterval); // default preserved
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe('watchFile', () => {
|
|
96
|
-
test('should return false for invalid file path', () => {
|
|
97
|
-
const watcher = new ConfigWatcher();
|
|
98
|
-
expect(watcher.watchFile(null)).toBe(false);
|
|
99
|
-
expect(watcher.watchFile('')).toBe(false);
|
|
100
|
-
expect(watcher.watchFile(123)).toBe(false);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('should return false for non-existent file', () => {
|
|
104
|
-
const watcher = new ConfigWatcher();
|
|
105
|
-
expect(watcher.watchFile('/nonexistent/file.json')).toBe(false);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('should return true for already watched file', () => {
|
|
109
|
-
const testFile = join(testDir, 'test.json');
|
|
110
|
-
writeFileSync(testFile, '{}');
|
|
111
|
-
|
|
112
|
-
const watcher = new ConfigWatcher();
|
|
113
|
-
expect(watcher.watchFile(testFile)).toBe(true);
|
|
114
|
-
expect(watcher.watchFile(testFile)).toBe(true); // second call
|
|
115
|
-
|
|
116
|
-
watcher.unwatchAll();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test('should start watching file and set isRunning to true', () => {
|
|
120
|
-
const testFile = join(testDir, 'test.json');
|
|
121
|
-
writeFileSync(testFile, '{}');
|
|
122
|
-
|
|
123
|
-
const watcher = new ConfigWatcher();
|
|
124
|
-
const result = watcher.watchFile(testFile);
|
|
125
|
-
|
|
126
|
-
expect(result).toBe(true);
|
|
127
|
-
expect(watcher.isRunning).toBe(true);
|
|
128
|
-
expect(watcher.isWatching(testFile)).toBe(true);
|
|
129
|
-
expect(watcher.getWatchedFiles()).toContain(testFile);
|
|
130
|
-
|
|
131
|
-
watcher.unwatchAll();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test('should support polling mode', () => {
|
|
135
|
-
const testFile = join(testDir, 'test.json');
|
|
136
|
-
writeFileSync(testFile, '{}');
|
|
137
|
-
|
|
138
|
-
const watcher = new ConfigWatcher({ usePolling: true });
|
|
139
|
-
const result = watcher.watchFile(testFile);
|
|
140
|
-
|
|
141
|
-
expect(result).toBe(true);
|
|
142
|
-
expect(watcher.pollWatchers.size).toBe(1);
|
|
143
|
-
|
|
144
|
-
watcher.unwatchAll();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test('should allow per-file option override', () => {
|
|
148
|
-
const testFile = join(testDir, 'test.json');
|
|
149
|
-
writeFileSync(testFile, '{}');
|
|
150
|
-
|
|
151
|
-
const watcher = new ConfigWatcher({ debounceMs: 100 });
|
|
152
|
-
const result = watcher.watchFile(testFile, { debounceMs: 200 });
|
|
153
|
-
|
|
154
|
-
expect(result).toBe(true);
|
|
155
|
-
|
|
156
|
-
watcher.unwatchAll();
|
|
157
|
-
});
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
describe('unwatchFile', () => {
|
|
161
|
-
test('should do nothing for non-watched file', () => {
|
|
162
|
-
const watcher = new ConfigWatcher();
|
|
163
|
-
watcher.unwatchFile('/nonexistent');
|
|
164
|
-
expect(watcher.watchedFiles.size).toBe(0);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test('should stop watching file', () => {
|
|
168
|
-
const testFile = join(testDir, 'test.json');
|
|
169
|
-
writeFileSync(testFile, '{}');
|
|
170
|
-
|
|
171
|
-
const watcher = new ConfigWatcher();
|
|
172
|
-
watcher.watchFile(testFile);
|
|
173
|
-
expect(watcher.isWatching(testFile)).toBe(true);
|
|
174
|
-
|
|
175
|
-
watcher.unwatchFile(testFile);
|
|
176
|
-
expect(watcher.isWatching(testFile)).toBe(false);
|
|
177
|
-
expect(watcher.getWatchedFiles()).not.toContain(testFile);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test('should set isRunning to false when all watchers closed', () => {
|
|
181
|
-
const testFile = join(testDir, 'test.json');
|
|
182
|
-
writeFileSync(testFile, '{}');
|
|
183
|
-
|
|
184
|
-
const watcher = new ConfigWatcher();
|
|
185
|
-
watcher.watchFile(testFile);
|
|
186
|
-
expect(watcher.isRunning).toBe(true);
|
|
187
|
-
|
|
188
|
-
watcher.unwatchFile(testFile);
|
|
189
|
-
expect(watcher.isRunning).toBe(false);
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
describe('watchFiles', () => {
|
|
194
|
-
test('should watch multiple files', () => {
|
|
195
|
-
const file1 = join(testDir, 'file1.json');
|
|
196
|
-
const file2 = join(testDir, 'file2.json');
|
|
197
|
-
writeFileSync(file1, '{}');
|
|
198
|
-
writeFileSync(file2, '{}');
|
|
199
|
-
|
|
200
|
-
const watcher = new ConfigWatcher();
|
|
201
|
-
const result = watcher.watchFiles([file1, file2]);
|
|
202
|
-
|
|
203
|
-
expect(result.successful).toHaveLength(2);
|
|
204
|
-
expect(result.failed).toHaveLength(0);
|
|
205
|
-
|
|
206
|
-
watcher.unwatchAll();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test('should track failed files', () => {
|
|
210
|
-
const file1 = join(testDir, 'exists.json');
|
|
211
|
-
const file2 = join(testDir, 'missing.json');
|
|
212
|
-
writeFileSync(file1, '{}');
|
|
213
|
-
|
|
214
|
-
const watcher = new ConfigWatcher();
|
|
215
|
-
const result = watcher.watchFiles([file1, file2]);
|
|
216
|
-
|
|
217
|
-
expect(result.successful).toContain(file1);
|
|
218
|
-
expect(result.failed).toContain(file2);
|
|
219
|
-
|
|
220
|
-
watcher.unwatchAll();
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
describe('unwatchAll', () => {
|
|
225
|
-
test('should unwatch all files', () => {
|
|
226
|
-
const file1 = join(testDir, 'file1.json');
|
|
227
|
-
const file2 = join(testDir, 'file2.json');
|
|
228
|
-
writeFileSync(file1, '{}');
|
|
229
|
-
writeFileSync(file2, '{}');
|
|
230
|
-
|
|
231
|
-
const watcher = new ConfigWatcher();
|
|
232
|
-
watcher.watchFile(file1);
|
|
233
|
-
watcher.watchFile(file2);
|
|
234
|
-
|
|
235
|
-
expect(watcher.getWatchedFiles()).toHaveLength(2);
|
|
236
|
-
|
|
237
|
-
watcher.unwatchAll();
|
|
238
|
-
|
|
239
|
-
expect(watcher.getWatchedFiles()).toHaveLength(0);
|
|
240
|
-
expect(watcher.isRunning).toBe(false);
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
describe('getWatchedFiles', () => {
|
|
245
|
-
test('should return empty array when nothing watched', () => {
|
|
246
|
-
const watcher = new ConfigWatcher();
|
|
247
|
-
expect(watcher.getWatchedFiles()).toEqual([]);
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
test('should return array of watched file paths', () => {
|
|
251
|
-
const file1 = join(testDir, 'file1.json');
|
|
252
|
-
const file2 = join(testDir, 'file2.json');
|
|
253
|
-
writeFileSync(file1, '{}');
|
|
254
|
-
writeFileSync(file2, '{}');
|
|
255
|
-
|
|
256
|
-
const watcher = new ConfigWatcher();
|
|
257
|
-
watcher.watchFile(file1);
|
|
258
|
-
watcher.watchFile(file2);
|
|
259
|
-
|
|
260
|
-
const files = watcher.getWatchedFiles();
|
|
261
|
-
expect(files).toHaveLength(2);
|
|
262
|
-
expect(files).toContain(file1);
|
|
263
|
-
expect(files).toContain(file2);
|
|
264
|
-
|
|
265
|
-
watcher.unwatchAll();
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
describe('isWatching', () => {
|
|
270
|
-
test('should return false for non-watched file', () => {
|
|
271
|
-
const watcher = new ConfigWatcher();
|
|
272
|
-
expect(watcher.isWatching('/nonexistent')).toBe(false);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test('should return true for watched file', () => {
|
|
276
|
-
const testFile = join(testDir, 'test.json');
|
|
277
|
-
writeFileSync(testFile, '{}');
|
|
278
|
-
|
|
279
|
-
const watcher = new ConfigWatcher();
|
|
280
|
-
watcher.watchFile(testFile);
|
|
281
|
-
|
|
282
|
-
expect(watcher.isWatching(testFile)).toBe(true);
|
|
283
|
-
|
|
284
|
-
watcher.unwatchAll();
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
describe('getStats', () => {
|
|
289
|
-
test('should return zero stats when not running', () => {
|
|
290
|
-
const watcher = new ConfigWatcher();
|
|
291
|
-
const stats = watcher.getStats();
|
|
292
|
-
|
|
293
|
-
expect(stats.isRunning).toBe(false);
|
|
294
|
-
expect(stats.watchedFiles).toBe(0);
|
|
295
|
-
expect(stats.nativeWatchers).toBe(0);
|
|
296
|
-
expect(stats.pollWatchers).toBe(0);
|
|
297
|
-
expect(stats.pendingDebounces).toBe(0);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test('should return correct stats when watching', () => {
|
|
301
|
-
const testFile = join(testDir, 'test.json');
|
|
302
|
-
writeFileSync(testFile, '{}');
|
|
303
|
-
|
|
304
|
-
const watcher = new ConfigWatcher();
|
|
305
|
-
watcher.watchFile(testFile);
|
|
306
|
-
|
|
307
|
-
const stats = watcher.getStats();
|
|
308
|
-
|
|
309
|
-
expect(stats.isRunning).toBe(true);
|
|
310
|
-
expect(stats.watchedFiles).toBe(1);
|
|
311
|
-
expect(stats.nativeWatchers).toBe(1);
|
|
312
|
-
expect(stats.pollWatchers).toBe(0);
|
|
313
|
-
|
|
314
|
-
watcher.unwatchAll();
|
|
315
|
-
});
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
describe('reload events', () => {
|
|
319
|
-
test('should emit reload event on file change', async () => {
|
|
320
|
-
const testFile = join(testDir, 'test.json');
|
|
321
|
-
writeFileSync(testFile, '{"initial": true}');
|
|
322
|
-
|
|
323
|
-
const watcher = new ConfigWatcher({ debounceMs: 100 });
|
|
324
|
-
watcher.watchFile(testFile);
|
|
325
|
-
|
|
326
|
-
const reloadPromise = new Promise((resolve) => {
|
|
327
|
-
watcher.on('reload', ({ filePath, timestamp }) => {
|
|
328
|
-
resolve({ filePath, timestamp });
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
// Modify the file
|
|
333
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
334
|
-
writeFileSync(testFile, '{"changed": true}');
|
|
335
|
-
|
|
336
|
-
// Wait for debounce
|
|
337
|
-
const result = await reloadPromise;
|
|
338
|
-
|
|
339
|
-
expect(result.filePath).toBe(testFile);
|
|
340
|
-
expect(result.timestamp).toBeDefined();
|
|
341
|
-
|
|
342
|
-
watcher.unwatchAll();
|
|
343
|
-
}, 5000);
|
|
344
|
-
|
|
345
|
-
test('should debounce multiple rapid changes', async () => {
|
|
346
|
-
const testFile = join(testDir, 'test.json');
|
|
347
|
-
writeFileSync(testFile, '{}');
|
|
348
|
-
|
|
349
|
-
let reloadCount = 0;
|
|
350
|
-
const watcher = new ConfigWatcher({ debounceMs: 200 });
|
|
351
|
-
watcher.watchFile(testFile);
|
|
352
|
-
|
|
353
|
-
watcher.on('reload', () => {
|
|
354
|
-
reloadCount++;
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// Make multiple rapid changes
|
|
358
|
-
for (let i = 0; i < 5; i++) {
|
|
359
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
360
|
-
writeFileSync(testFile, `{ "change": ${i} }`);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Wait for debounce to settle
|
|
364
|
-
await new Promise(resolve => setTimeout(resolve, 400));
|
|
365
|
-
|
|
366
|
-
// Should only get one reload event due to debouncing
|
|
367
|
-
expect(reloadCount).toBe(1);
|
|
368
|
-
|
|
369
|
-
watcher.unwatchAll();
|
|
370
|
-
}, 5000);
|
|
371
|
-
|
|
372
|
-
test('should emit error event on watcher error', async () => {
|
|
373
|
-
const watcher = new ConfigWatcher();
|
|
374
|
-
|
|
375
|
-
const errorPromise = new Promise((resolve) => {
|
|
376
|
-
watcher.on('error', ({ filePath, error }) => {
|
|
377
|
-
resolve({ filePath, errorMessage: error.message });
|
|
378
|
-
});
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
// Try to watch a non-existent file (this might trigger an error)
|
|
382
|
-
watcher.watchFile('/invalid/path/that/does/not/exist');
|
|
383
|
-
|
|
384
|
-
// The watcher might just return false rather than emit error
|
|
385
|
-
// So let's just verify basic functionality works
|
|
386
|
-
expect(watcher.getWatchedFiles()).toEqual([]);
|
|
387
|
-
});
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
describe('createConfigWatcher', () => {
|
|
391
|
-
test('should create ConfigWatcher instance', () => {
|
|
392
|
-
const watcher = createConfigWatcher();
|
|
393
|
-
expect(watcher).toBeInstanceOf(ConfigWatcher);
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
test('should pass options to ConfigWatcher', () => {
|
|
397
|
-
const watcher = createConfigWatcher({ debounceMs: 1000 });
|
|
398
|
-
expect(watcher.options.debounceMs).toBe(1000);
|
|
399
|
-
});
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
describe('getConfigWatcher', () => {
|
|
403
|
-
test('should return singleton instance', () => {
|
|
404
|
-
const watcher1 = getConfigWatcher();
|
|
405
|
-
const watcher2 = getConfigWatcher();
|
|
406
|
-
|
|
407
|
-
expect(watcher1).toBe(watcher2);
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
test('should create instance with options if none exists', () => {
|
|
411
|
-
// Note: This test might interact with other tests using getConfigWatcher
|
|
412
|
-
// In a clean environment, this would work properly
|
|
413
|
-
const watcher = getConfigWatcher({ debounceMs: 500 });
|
|
414
|
-
expect(watcher).toBeInstanceOf(ConfigWatcher);
|
|
415
|
-
});
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
describe('watchSettingsFile', () => {
|
|
419
|
-
test('should return null for non-existent settings file', () => {
|
|
420
|
-
const result = watchSettingsFile('/nonexistent/settings.json', () => {});
|
|
421
|
-
expect(result).toBeNull();
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
test('should watch settings file and call callback on change', async () => {
|
|
425
|
-
const settingsFile = join(testDir, 'settings.json');
|
|
426
|
-
writeFileSync(settingsFile, JSON.stringify({ theme: 'dark' }));
|
|
427
|
-
|
|
428
|
-
let receivedSettings = null;
|
|
429
|
-
const watcher = watchSettingsFile(settingsFile, (settings) => {
|
|
430
|
-
receivedSettings = settings;
|
|
431
|
-
}, { debounceMs: 100 });
|
|
432
|
-
|
|
433
|
-
expect(watcher).not.toBeNull();
|
|
434
|
-
expect(watcher.isWatching(settingsFile)).toBe(true);
|
|
435
|
-
|
|
436
|
-
// Wait for initial setup
|
|
437
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
438
|
-
|
|
439
|
-
// Change the file
|
|
440
|
-
writeFileSync(settingsFile, JSON.stringify({ theme: 'light' }));
|
|
441
|
-
|
|
442
|
-
// Wait for reload
|
|
443
|
-
await new Promise(resolve => setTimeout(resolve, 300));
|
|
444
|
-
|
|
445
|
-
expect(receivedSettings).toEqual({ theme: 'light' });
|
|
446
|
-
|
|
447
|
-
watcher.unwatchAll();
|
|
448
|
-
}, 5000);
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
describe('watchPluginsDirectory', () => {
|
|
452
|
-
test('should return null for non-existent plugins directory', () => {
|
|
453
|
-
const result = watchPluginsDirectory('/nonexistent/plugins', () => {});
|
|
454
|
-
expect(result).toBeNull();
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
test('should watch plugin.json files in directory', async () => {
|
|
458
|
-
const pluginsDir = join(testDir, 'plugins');
|
|
459
|
-
mkdirSync(pluginsDir);
|
|
460
|
-
|
|
461
|
-
// Create a test plugin
|
|
462
|
-
const pluginDir = join(pluginsDir, 'test-plugin');
|
|
463
|
-
mkdirSync(pluginDir);
|
|
464
|
-
writeFileSync(join(pluginDir, 'plugin.json'), JSON.stringify({
|
|
465
|
-
name: 'Test Plugin',
|
|
466
|
-
version: '1.0.0',
|
|
467
|
-
}));
|
|
468
|
-
|
|
469
|
-
let receivedPluginId = null;
|
|
470
|
-
let receivedManifest = null;
|
|
471
|
-
|
|
472
|
-
const watcher = watchPluginsDirectory(pluginsDir, (pluginId, manifest) => {
|
|
473
|
-
receivedPluginId = pluginId;
|
|
474
|
-
receivedManifest = manifest;
|
|
475
|
-
}, { debounceMs: 100 });
|
|
476
|
-
|
|
477
|
-
expect(watcher).not.toBeNull();
|
|
478
|
-
|
|
479
|
-
// Wait for watcher to scan directory
|
|
480
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
481
|
-
|
|
482
|
-
// Modify the plugin.json
|
|
483
|
-
writeFileSync(join(pluginDir, 'plugin.json'), JSON.stringify({
|
|
484
|
-
name: 'Test Plugin',
|
|
485
|
-
version: '1.0.1',
|
|
486
|
-
}));
|
|
487
|
-
|
|
488
|
-
// Wait for reload
|
|
489
|
-
await new Promise(resolve => setTimeout(resolve, 300));
|
|
490
|
-
|
|
491
|
-
expect(receivedPluginId).toBe('test-plugin');
|
|
492
|
-
expect(receivedManifest.name).toBe('Test Plugin');
|
|
493
|
-
|
|
494
|
-
watcher.unwatchAll();
|
|
495
|
-
}, 5000);
|
|
496
|
-
|
|
497
|
-
test('should handle directory with no plugins', () => {
|
|
498
|
-
const emptyDir = join(testDir, 'empty-plugins');
|
|
499
|
-
mkdirSync(emptyDir);
|
|
500
|
-
|
|
501
|
-
const watcher = watchPluginsDirectory(emptyDir, () => {});
|
|
502
|
-
|
|
503
|
-
expect(watcher).not.toBeNull();
|
|
504
|
-
// Should return watcher but watch 0 files
|
|
505
|
-
expect(watcher.getWatchedFiles()).toHaveLength(0);
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
test('should ignore non-directory entries', () => {
|
|
509
|
-
const pluginsDir = join(testDir, 'plugins2');
|
|
510
|
-
mkdirSync(pluginsDir);
|
|
511
|
-
|
|
512
|
-
// Create some files (not directories)
|
|
513
|
-
writeFileSync(join(pluginsDir, 'readme.txt'), 'Readme');
|
|
514
|
-
writeFileSync(join(pluginsDir, 'config.json'), '{}');
|
|
515
|
-
|
|
516
|
-
const watcher = watchPluginsDirectory(pluginsDir, () => {});
|
|
517
|
-
|
|
518
|
-
expect(watcher).not.toBeNull();
|
|
519
|
-
expect(watcher.getWatchedFiles()).toHaveLength(0);
|
|
520
|
-
});
|
|
521
|
-
});
|
|
522
|
-
|
|
523
|
-
describe('EventEmitter integration', () => {
|
|
524
|
-
test('should support on/emit pattern', () => {
|
|
525
|
-
const watcher = new ConfigWatcher();
|
|
526
|
-
|
|
527
|
-
const calls = [];
|
|
528
|
-
const handler = (...args) => {
|
|
529
|
-
calls.push(args);
|
|
530
|
-
};
|
|
531
|
-
watcher.on('testEvent', handler);
|
|
532
|
-
|
|
533
|
-
watcher.emit('testEvent', { data: 'test' });
|
|
534
|
-
|
|
535
|
-
expect(calls).toHaveLength(1);
|
|
536
|
-
expect(calls[0]).toEqual([{ data: 'test' }]);
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
test('should support removing listeners', () => {
|
|
540
|
-
const watcher = new ConfigWatcher();
|
|
541
|
-
|
|
542
|
-
const calls = [];
|
|
543
|
-
const handler = (...args) => {
|
|
544
|
-
calls.push(args);
|
|
545
|
-
};
|
|
546
|
-
watcher.on('testEvent', handler);
|
|
547
|
-
watcher.removeListener('testEvent', handler);
|
|
548
|
-
|
|
549
|
-
watcher.emit('testEvent');
|
|
550
|
-
|
|
551
|
-
expect(calls).toHaveLength(0);
|
|
552
|
-
});
|
|
553
|
-
});
|
|
554
|
-
|
|
555
|
-
describe('edge cases', () => {
|
|
556
|
-
test('should handle very short debounce', async () => {
|
|
557
|
-
const testFile = join(testDir, 'test.json');
|
|
558
|
-
writeFileSync(testFile, '{}');
|
|
559
|
-
|
|
560
|
-
let eventCount = 0;
|
|
561
|
-
const watcher = new ConfigWatcher({ debounceMs: 10 });
|
|
562
|
-
watcher.watchFile(testFile);
|
|
563
|
-
|
|
564
|
-
watcher.on('reload', () => {
|
|
565
|
-
eventCount++;
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
writeFileSync(testFile, '{ "a": 1 }');
|
|
569
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
570
|
-
writeFileSync(testFile, '{ "b": 2 }');
|
|
571
|
-
await new Promise(resolve => setTimeout(resolve, 50));
|
|
572
|
-
|
|
573
|
-
// Wait for all debounces
|
|
574
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
575
|
-
|
|
576
|
-
expect(eventCount).toBeGreaterThan(0);
|
|
577
|
-
|
|
578
|
-
watcher.unwatchAll();
|
|
579
|
-
}, 5000);
|
|
580
|
-
|
|
581
|
-
test('should handle special characters in file paths', () => {
|
|
582
|
-
const testFile = join(testDir, 'test file with spaces.json');
|
|
583
|
-
writeFileSync(testFile, '{}');
|
|
584
|
-
|
|
585
|
-
const watcher = new ConfigWatcher();
|
|
586
|
-
const result = watcher.watchFile(testFile);
|
|
587
|
-
|
|
588
|
-
expect(result).toBe(true);
|
|
589
|
-
expect(watcher.isWatching(testFile)).toBe(true);
|
|
590
|
-
|
|
591
|
-
watcher.unwatchAll();
|
|
592
|
-
});
|
|
593
|
-
});
|
|
594
|
-
});
|