ultravisor-beacon 0.0.1

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.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Ultravisor Beacon Provider — Shell
3
+ *
4
+ * Built-in provider that executes shell commands via child_process.exec().
5
+ *
6
+ * Capability: 'Shell'
7
+ * Actions: 'Execute' — run a shell command with optional parameters.
8
+ *
9
+ * Provider config:
10
+ * MaxBufferBytes {number} — max stdout/stderr buffer (default: 10MB)
11
+ */
12
+
13
+ const libChildProcess = require('child_process');
14
+
15
+ const libBeaconCapabilityProvider = require('../Ultravisor-Beacon-CapabilityProvider.cjs');
16
+
17
+ class UltravisorBeaconProviderShell extends libBeaconCapabilityProvider
18
+ {
19
+ constructor(pProviderConfig)
20
+ {
21
+ super(pProviderConfig);
22
+
23
+ this.Name = 'Shell';
24
+ this.Capability = 'Shell';
25
+
26
+ this._MaxBufferBytes = this._ProviderConfig.MaxBufferBytes || 10485760;
27
+ }
28
+
29
+ get actions()
30
+ {
31
+ return {
32
+ 'Execute':
33
+ {
34
+ Description: 'Execute a shell command.',
35
+ SettingsSchema:
36
+ [
37
+ { Name: 'Command', DataType: 'String', Required: true, Description: 'The command to run' },
38
+ { Name: 'Parameters', DataType: 'String', Required: false, Description: 'Command-line arguments' }
39
+ ]
40
+ }
41
+ };
42
+ }
43
+
44
+ execute(pAction, pWorkItem, pContext, fCallback, fReportProgress)
45
+ {
46
+ let tmpSettings = pWorkItem.Settings || {};
47
+ let tmpCommand = tmpSettings.Command || '';
48
+ let tmpParameters = tmpSettings.Parameters || '';
49
+
50
+ if (!tmpCommand)
51
+ {
52
+ return fCallback(null, {
53
+ Outputs: { StdOut: 'No command specified.', ExitCode: -1, Result: '' },
54
+ Log: ['Shell Provider: no command specified.']
55
+ });
56
+ }
57
+
58
+ let tmpFullCommand = tmpParameters ? (tmpCommand + ' ' + tmpParameters) : tmpCommand;
59
+ let tmpTimeout = pWorkItem.TimeoutMs || 300000;
60
+
61
+ console.log(` [Shell] Running: ${tmpFullCommand}`);
62
+
63
+ libChildProcess.exec(tmpFullCommand,
64
+ {
65
+ cwd: pContext.StagingPath || process.cwd(),
66
+ timeout: tmpTimeout,
67
+ maxBuffer: this._MaxBufferBytes
68
+ },
69
+ function (pError, pStdOut, pStdErr)
70
+ {
71
+ if (pError)
72
+ {
73
+ return fCallback(null, {
74
+ Outputs: {
75
+ StdOut: (pStdOut || '') + (pStdErr || ''),
76
+ ExitCode: pError.code || 1,
77
+ Result: ''
78
+ },
79
+ Log: [`Command failed: ${pError.message}`, pStdErr || ''].filter(Boolean)
80
+ });
81
+ }
82
+
83
+ return fCallback(null, {
84
+ Outputs: {
85
+ StdOut: pStdOut || '',
86
+ ExitCode: 0,
87
+ Result: pStdOut || ''
88
+ },
89
+ Log: [`Command executed: ${tmpFullCommand}`]
90
+ });
91
+ });
92
+ }
93
+ }
94
+
95
+ module.exports = UltravisorBeaconProviderShell;
@@ -0,0 +1,608 @@
1
+ /**
2
+ * Ultravisor Beacon Service Tests
3
+ *
4
+ * Tests the Fable service layer: CapabilityManager, CapabilityAdapter,
5
+ * ConnectivityHTTP, and the main BeaconService.
6
+ *
7
+ * NOTE: These tests do NOT require a running Ultravisor server.
8
+ * They verify the service registration, capability management, and
9
+ * adapter bridging logic in isolation.
10
+ */
11
+
12
+ const libAssert = require('assert');
13
+
14
+ const libCapabilityProvider = require('../source/Ultravisor-Beacon-CapabilityProvider.cjs');
15
+ const libCapabilityAdapter = require('../source/Ultravisor-Beacon-CapabilityAdapter.cjs');
16
+ const libCapabilityManager = require('../source/Ultravisor-Beacon-CapabilityManager.cjs');
17
+ const libConnectivityHTTP = require('../source/Ultravisor-Beacon-ConnectivityHTTP.cjs');
18
+ const libProviderRegistry = require('../source/Ultravisor-Beacon-ProviderRegistry.cjs');
19
+
20
+ // We can require the service without Fable for standalone testing
21
+ const libBeaconService = require('../source/Ultravisor-Beacon-Service.cjs');
22
+
23
+ suite
24
+ (
25
+ 'Ultravisor Beacon',
26
+ function ()
27
+ {
28
+ // ============================================================
29
+ // CapabilityProvider Base Class
30
+ // ============================================================
31
+ suite
32
+ (
33
+ 'CapabilityProvider Base',
34
+ function ()
35
+ {
36
+ test
37
+ (
38
+ 'Should instantiate with defaults',
39
+ function ()
40
+ {
41
+ let tmpProvider = new libCapabilityProvider();
42
+ libAssert.strictEqual(tmpProvider.Name, 'BaseProvider');
43
+ libAssert.strictEqual(tmpProvider.Capability, 'Unknown');
44
+ libAssert.deepStrictEqual(tmpProvider.actions, {});
45
+ libAssert.deepStrictEqual(tmpProvider.getCapabilities(), ['Unknown']);
46
+ }
47
+ );
48
+
49
+ test
50
+ (
51
+ 'Should return error from default execute',
52
+ function (fDone)
53
+ {
54
+ let tmpProvider = new libCapabilityProvider();
55
+ tmpProvider.execute('DoSomething', {}, {}, function (pError)
56
+ {
57
+ libAssert.ok(pError);
58
+ libAssert.ok(pError.message.includes('has not implemented execute()'));
59
+ fDone();
60
+ });
61
+ }
62
+ );
63
+
64
+ test
65
+ (
66
+ 'Should have no-op initialize and shutdown',
67
+ function (fDone)
68
+ {
69
+ let tmpProvider = new libCapabilityProvider();
70
+ tmpProvider.initialize(function (pError)
71
+ {
72
+ libAssert.ifError(pError);
73
+ tmpProvider.shutdown(function (pError2)
74
+ {
75
+ libAssert.ifError(pError2);
76
+ fDone();
77
+ });
78
+ });
79
+ }
80
+ );
81
+ }
82
+ );
83
+
84
+ // ============================================================
85
+ // CapabilityAdapter
86
+ // ============================================================
87
+ suite
88
+ (
89
+ 'CapabilityAdapter',
90
+ function ()
91
+ {
92
+ test
93
+ (
94
+ 'Should create adapter from descriptor',
95
+ function ()
96
+ {
97
+ let tmpAdapter = new libCapabilityAdapter({
98
+ Capability: 'TestCap',
99
+ Name: 'TestProvider',
100
+ actions: {
101
+ 'DoThing': {
102
+ Description: 'Does a thing',
103
+ SettingsSchema: [{ Name: 'Input', DataType: 'String' }],
104
+ Handler: function () {}
105
+ }
106
+ }
107
+ });
108
+
109
+ libAssert.strictEqual(tmpAdapter.Name, 'TestProvider');
110
+ libAssert.strictEqual(tmpAdapter.Capability, 'TestCap');
111
+ libAssert.deepStrictEqual(tmpAdapter.getCapabilities(), ['TestCap']);
112
+
113
+ let tmpActions = tmpAdapter.actions;
114
+ libAssert.ok(tmpActions.DoThing);
115
+ libAssert.strictEqual(tmpActions.DoThing.Description, 'Does a thing');
116
+ // Handler should not leak into actions
117
+ libAssert.strictEqual(tmpActions.DoThing.Handler, undefined);
118
+ }
119
+ );
120
+
121
+ test
122
+ (
123
+ 'Should be an instance of CapabilityProvider',
124
+ function ()
125
+ {
126
+ let tmpAdapter = new libCapabilityAdapter({
127
+ Capability: 'TestCap',
128
+ Name: 'TestProvider',
129
+ actions: {}
130
+ });
131
+
132
+ libAssert.ok(tmpAdapter instanceof libCapabilityProvider);
133
+ }
134
+ );
135
+
136
+ test
137
+ (
138
+ 'Should execute action via Handler',
139
+ function (fDone)
140
+ {
141
+ let tmpHandlerCalled = false;
142
+
143
+ let tmpAdapter = new libCapabilityAdapter({
144
+ Capability: 'TestCap',
145
+ Name: 'TestProvider',
146
+ actions: {
147
+ 'ReadFile': {
148
+ Description: 'Read a file',
149
+ Handler: function (pWorkItem, pContext, fCallback)
150
+ {
151
+ tmpHandlerCalled = true;
152
+ fCallback(null, {
153
+ Outputs: { Content: 'Hello World' },
154
+ Log: ['Read complete']
155
+ });
156
+ }
157
+ }
158
+ }
159
+ });
160
+
161
+ let tmpWorkItem = { WorkItemHash: 'test-123', Settings: { FilePath: '/tmp/test.md' } };
162
+ let tmpContext = { StagingPath: '/tmp' };
163
+
164
+ tmpAdapter.execute('ReadFile', tmpWorkItem, tmpContext, function (pError, pResult)
165
+ {
166
+ libAssert.ifError(pError);
167
+ libAssert.ok(tmpHandlerCalled);
168
+ libAssert.strictEqual(pResult.Outputs.Content, 'Hello World');
169
+ libAssert.strictEqual(pResult.Log[0], 'Read complete');
170
+ fDone();
171
+ });
172
+ }
173
+ );
174
+
175
+ test
176
+ (
177
+ 'Should error on unknown action',
178
+ function (fDone)
179
+ {
180
+ let tmpAdapter = new libCapabilityAdapter({
181
+ Capability: 'TestCap',
182
+ Name: 'TestProvider',
183
+ actions: {}
184
+ });
185
+
186
+ tmpAdapter.execute('NonExistent', {}, {}, function (pError)
187
+ {
188
+ libAssert.ok(pError);
189
+ libAssert.ok(pError.message.includes('no Handler'));
190
+ fDone();
191
+ });
192
+ }
193
+ );
194
+
195
+ test
196
+ (
197
+ 'Should delegate initialize and shutdown',
198
+ function (fDone)
199
+ {
200
+ let tmpInitCalled = false;
201
+ let tmpShutdownCalled = false;
202
+
203
+ let tmpAdapter = new libCapabilityAdapter({
204
+ Capability: 'TestCap',
205
+ Name: 'TestProvider',
206
+ actions: {},
207
+ initialize: function (fCallback)
208
+ {
209
+ tmpInitCalled = true;
210
+ fCallback(null);
211
+ },
212
+ shutdown: function (fCallback)
213
+ {
214
+ tmpShutdownCalled = true;
215
+ fCallback(null);
216
+ }
217
+ });
218
+
219
+ tmpAdapter.initialize(function (pError)
220
+ {
221
+ libAssert.ifError(pError);
222
+ libAssert.ok(tmpInitCalled);
223
+
224
+ tmpAdapter.shutdown(function (pError2)
225
+ {
226
+ libAssert.ifError(pError2);
227
+ libAssert.ok(tmpShutdownCalled);
228
+ fDone();
229
+ });
230
+ });
231
+ }
232
+ );
233
+
234
+ test
235
+ (
236
+ 'Should catch Handler exceptions',
237
+ function (fDone)
238
+ {
239
+ let tmpAdapter = new libCapabilityAdapter({
240
+ Capability: 'TestCap',
241
+ Name: 'TestProvider',
242
+ actions: {
243
+ 'Boom': {
244
+ Description: 'Throws',
245
+ Handler: function ()
246
+ {
247
+ throw new Error('Intentional explosion');
248
+ }
249
+ }
250
+ }
251
+ });
252
+
253
+ tmpAdapter.execute('Boom', {}, {}, function (pError)
254
+ {
255
+ libAssert.ok(pError);
256
+ libAssert.strictEqual(pError.message, 'Intentional explosion');
257
+ fDone();
258
+ });
259
+ }
260
+ );
261
+ }
262
+ );
263
+
264
+ // ============================================================
265
+ // CapabilityManager
266
+ // ============================================================
267
+ suite
268
+ (
269
+ 'CapabilityManager',
270
+ function ()
271
+ {
272
+ test
273
+ (
274
+ 'Should register and list capabilities',
275
+ function ()
276
+ {
277
+ let tmpManager = new libCapabilityManager();
278
+
279
+ tmpManager.registerCapability({
280
+ Capability: 'ContentSystem',
281
+ Name: 'ContentProvider',
282
+ actions: {
283
+ 'ReadFile': { Description: 'Read', Handler: function () {} }
284
+ }
285
+ });
286
+
287
+ tmpManager.registerCapability({
288
+ Capability: 'MediaProcessing',
289
+ Name: 'MediaProvider',
290
+ actions: {
291
+ 'GenThumbnail': { Description: 'Thumbnail', Handler: function () {} }
292
+ }
293
+ });
294
+
295
+ let tmpNames = tmpManager.getCapabilityNames();
296
+ libAssert.strictEqual(tmpNames.length, 2);
297
+ libAssert.ok(tmpNames.includes('ContentSystem'));
298
+ libAssert.ok(tmpNames.includes('MediaProcessing'));
299
+ }
300
+ );
301
+
302
+ test
303
+ (
304
+ 'Should reject descriptor without Capability',
305
+ function ()
306
+ {
307
+ let tmpManager = new libCapabilityManager();
308
+ let tmpResult = tmpManager.registerCapability({ Name: 'BadProvider' });
309
+ libAssert.strictEqual(tmpResult, false);
310
+ libAssert.strictEqual(tmpManager.getCapabilityNames().length, 0);
311
+ }
312
+ );
313
+
314
+ test
315
+ (
316
+ 'Should remove capabilities',
317
+ function ()
318
+ {
319
+ let tmpManager = new libCapabilityManager();
320
+
321
+ tmpManager.registerCapability({
322
+ Capability: 'TestCap',
323
+ actions: { 'Do': { Handler: function () {} } }
324
+ });
325
+
326
+ libAssert.strictEqual(tmpManager.getCapabilityNames().length, 1);
327
+
328
+ tmpManager.removeCapability('TestCap');
329
+ libAssert.strictEqual(tmpManager.getCapabilityNames().length, 0);
330
+ }
331
+ );
332
+
333
+ test
334
+ (
335
+ 'Should build provider descriptors as adapter instances',
336
+ function ()
337
+ {
338
+ let tmpManager = new libCapabilityManager();
339
+
340
+ tmpManager.registerCapability({
341
+ Capability: 'ContentSystem',
342
+ Name: 'ContentProvider',
343
+ actions: {
344
+ 'ReadFile': { Description: 'Read', Handler: function () {} },
345
+ 'SaveFile': { Description: 'Save', Handler: function () {} }
346
+ }
347
+ });
348
+
349
+ let tmpDescriptors = tmpManager.buildProviderDescriptors();
350
+ libAssert.strictEqual(tmpDescriptors.length, 1);
351
+
352
+ let tmpAdapter = tmpDescriptors[0];
353
+ libAssert.ok(tmpAdapter instanceof libCapabilityProvider);
354
+ libAssert.strictEqual(tmpAdapter.Capability, 'ContentSystem');
355
+
356
+ let tmpActions = tmpAdapter.actions;
357
+ libAssert.ok(tmpActions.ReadFile);
358
+ libAssert.ok(tmpActions.SaveFile);
359
+ }
360
+ );
361
+
362
+ test
363
+ (
364
+ 'Built adapters should be registrable with ProviderRegistry',
365
+ function ()
366
+ {
367
+ let tmpManager = new libCapabilityManager();
368
+
369
+ tmpManager.registerCapability({
370
+ Capability: 'TestCap',
371
+ Name: 'TestProvider',
372
+ actions: {
373
+ 'DoThing': { Description: 'Do a thing', Handler: function () {} }
374
+ }
375
+ });
376
+
377
+ let tmpDescriptors = tmpManager.buildProviderDescriptors();
378
+ let tmpRegistry = new libProviderRegistry();
379
+
380
+ let tmpResult = tmpRegistry.registerProvider(tmpDescriptors[0]);
381
+ libAssert.strictEqual(tmpResult, true);
382
+
383
+ let tmpCapabilities = tmpRegistry.getCapabilities();
384
+ libAssert.ok(tmpCapabilities.includes('TestCap'));
385
+
386
+ let tmpResolved = tmpRegistry.resolve('TestCap', 'DoThing');
387
+ libAssert.ok(tmpResolved);
388
+ libAssert.strictEqual(tmpResolved.action, 'DoThing');
389
+ }
390
+ );
391
+ }
392
+ );
393
+
394
+ // ============================================================
395
+ // ConnectivityHTTP
396
+ // ============================================================
397
+ suite
398
+ (
399
+ 'ConnectivityHTTP',
400
+ function ()
401
+ {
402
+ test
403
+ (
404
+ 'Should return transport config',
405
+ function ()
406
+ {
407
+ let tmpConn = new libConnectivityHTTP({
408
+ ServerURL: 'http://myserver:9999',
409
+ Password: 'secret',
410
+ PollIntervalMs: 3000
411
+ });
412
+
413
+ let tmpConfig = tmpConn.getTransportConfig();
414
+ libAssert.strictEqual(tmpConfig.ServerURL, 'http://myserver:9999');
415
+ libAssert.strictEqual(tmpConfig.Password, 'secret');
416
+ libAssert.strictEqual(tmpConfig.PollIntervalMs, 3000);
417
+ }
418
+ );
419
+
420
+ test
421
+ (
422
+ 'Should report HTTP transport type',
423
+ function ()
424
+ {
425
+ let tmpConn = new libConnectivityHTTP();
426
+ libAssert.strictEqual(tmpConn.getTransportType(), 'HTTP');
427
+ }
428
+ );
429
+
430
+ test
431
+ (
432
+ 'Should use defaults',
433
+ function ()
434
+ {
435
+ let tmpConn = new libConnectivityHTTP();
436
+ let tmpConfig = tmpConn.getTransportConfig();
437
+ libAssert.strictEqual(tmpConfig.ServerURL, 'http://localhost:54321');
438
+ libAssert.strictEqual(tmpConfig.PollIntervalMs, 5000);
439
+ libAssert.strictEqual(tmpConfig.HeartbeatIntervalMs, 30000);
440
+ }
441
+ );
442
+ }
443
+ );
444
+
445
+ // ============================================================
446
+ // Beacon Service (standalone, no Fable)
447
+ // ============================================================
448
+ suite
449
+ (
450
+ 'BeaconService Standalone',
451
+ function ()
452
+ {
453
+ test
454
+ (
455
+ 'Should instantiate without Fable',
456
+ function ()
457
+ {
458
+ let tmpService = new libBeaconService({
459
+ ServerURL: 'http://localhost:54321',
460
+ Name: 'test-beacon'
461
+ });
462
+
463
+ libAssert.strictEqual(tmpService.serviceType, 'UltravisorBeacon');
464
+ libAssert.strictEqual(tmpService.isEnabled(), false);
465
+ libAssert.strictEqual(tmpService.options.ServerURL, 'http://localhost:54321');
466
+ }
467
+ );
468
+
469
+ test
470
+ (
471
+ 'Should register capabilities via public API',
472
+ function ()
473
+ {
474
+ let tmpService = new libBeaconService({ Name: 'test' });
475
+
476
+ let tmpResult = tmpService.registerCapability({
477
+ Capability: 'ContentSystem',
478
+ Name: 'ContentProvider',
479
+ actions: {
480
+ 'ReadFile': { Description: 'Read', Handler: function () {} }
481
+ }
482
+ });
483
+
484
+ // Should be chainable
485
+ libAssert.strictEqual(tmpResult, tmpService);
486
+
487
+ let tmpNames = tmpService.getCapabilityNames();
488
+ libAssert.strictEqual(tmpNames.length, 1);
489
+ libAssert.strictEqual(tmpNames[0], 'ContentSystem');
490
+ }
491
+ );
492
+
493
+ test
494
+ (
495
+ 'Should chain multiple registerCapability calls',
496
+ function ()
497
+ {
498
+ let tmpService = new libBeaconService({ Name: 'test' });
499
+
500
+ tmpService
501
+ .registerCapability({
502
+ Capability: 'ContentSystem',
503
+ actions: { 'Read': { Handler: function () {} } }
504
+ })
505
+ .registerCapability({
506
+ Capability: 'MediaProcessing',
507
+ actions: { 'Thumb': { Handler: function () {} } }
508
+ });
509
+
510
+ libAssert.strictEqual(tmpService.getCapabilityNames().length, 2);
511
+ }
512
+ );
513
+
514
+ test
515
+ (
516
+ 'Should export sub-components',
517
+ function ()
518
+ {
519
+ libAssert.ok(libBeaconService.BeaconClient);
520
+ libAssert.ok(libBeaconService.CapabilityManager);
521
+ libAssert.ok(libBeaconService.CapabilityAdapter);
522
+ libAssert.ok(libBeaconService.CapabilityProvider);
523
+ libAssert.ok(libBeaconService.ProviderRegistry);
524
+ libAssert.ok(libBeaconService.ConnectivityHTTP);
525
+ }
526
+ );
527
+
528
+ test
529
+ (
530
+ 'Should report not enabled when disable called without enable',
531
+ function (fDone)
532
+ {
533
+ let tmpService = new libBeaconService({ Name: 'test' });
534
+
535
+ tmpService.disable(function (pError)
536
+ {
537
+ libAssert.ifError(pError);
538
+ libAssert.strictEqual(tmpService.isEnabled(), false);
539
+ fDone();
540
+ });
541
+ }
542
+ );
543
+
544
+ test
545
+ (
546
+ 'Should return null thin client when not enabled',
547
+ function ()
548
+ {
549
+ let tmpService = new libBeaconService({ Name: 'test' });
550
+ libAssert.strictEqual(tmpService.getThinClient(), null);
551
+ }
552
+ );
553
+ }
554
+ );
555
+
556
+ // ============================================================
557
+ // Integration: Full adapter round-trip
558
+ // ============================================================
559
+ suite
560
+ (
561
+ 'Integration',
562
+ function ()
563
+ {
564
+ test
565
+ (
566
+ 'Capability registered with service should be executable through adapter',
567
+ function (fDone)
568
+ {
569
+ let tmpService = new libBeaconService({ Name: 'integration-test' });
570
+
571
+ tmpService.registerCapability({
572
+ Capability: 'TestSystem',
573
+ Name: 'TestProvider',
574
+ actions: {
575
+ 'Greet': {
576
+ Description: 'Say hello',
577
+ SettingsSchema: [{ Name: 'Name', DataType: 'String' }],
578
+ Handler: function (pWorkItem, pContext, fCallback)
579
+ {
580
+ let tmpName = pWorkItem.Settings.Name || 'World';
581
+ fCallback(null, {
582
+ Outputs: { Greeting: `Hello, ${tmpName}!` },
583
+ Log: ['Greeted successfully']
584
+ });
585
+ }
586
+ }
587
+ }
588
+ });
589
+
590
+ // Build adapters and test them directly
591
+ let tmpAdapters = tmpService.getCapabilityManager().buildProviderDescriptors();
592
+ libAssert.strictEqual(tmpAdapters.length, 1);
593
+
594
+ let tmpAdapter = tmpAdapters[0];
595
+ let tmpWorkItem = { WorkItemHash: 'int-test-1', Settings: { Name: 'Ultravisor' } };
596
+
597
+ tmpAdapter.execute('Greet', tmpWorkItem, {}, function (pError, pResult)
598
+ {
599
+ libAssert.ifError(pError);
600
+ libAssert.strictEqual(pResult.Outputs.Greeting, 'Hello, Ultravisor!');
601
+ fDone();
602
+ });
603
+ }
604
+ );
605
+ }
606
+ );
607
+ }
608
+ );