@djodjonx/x32-simulator 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.
Files changed (86) hide show
  1. package/.commitlintrc.json +3 -0
  2. package/.github/workflows/publish.yml +38 -0
  3. package/.husky/commit-msg +1 -0
  4. package/.husky/pre-commit +1 -0
  5. package/.oxlintrc.json +56 -0
  6. package/CHANGELOG.md +11 -0
  7. package/INSTALL.md +107 -0
  8. package/LICENSE +21 -0
  9. package/README.md +141 -0
  10. package/dist/UdpNetworkGateway-BrroQ6-Q.mjs +1189 -0
  11. package/dist/UdpNetworkGateway-Ccdd7Us5.cjs +1265 -0
  12. package/dist/index.cjs +7 -0
  13. package/dist/index.d.cts +207 -0
  14. package/dist/index.d.mts +207 -0
  15. package/dist/index.mjs +3 -0
  16. package/dist/server.cjs +1060 -0
  17. package/dist/server.d.cts +10 -0
  18. package/dist/server.d.mts +10 -0
  19. package/dist/server.mjs +1055 -0
  20. package/docs/OSC-Communication.md +184 -0
  21. package/docs/X32-INTERNAL.md +262 -0
  22. package/docs/X32-OSC.pdf +0 -0
  23. package/docs/behringer-x32-x32-osc-remote-protocol-en-44463.pdf +0 -0
  24. package/package.json +68 -0
  25. package/src/application/use-cases/BroadcastUpdatesUseCase.ts +120 -0
  26. package/src/application/use-cases/ManageSessionsUseCase.ts +9 -0
  27. package/src/application/use-cases/ProcessPacketUseCase.ts +26 -0
  28. package/src/application/use-cases/SimulationService.ts +122 -0
  29. package/src/domain/entities/SubscriptionManager.ts +126 -0
  30. package/src/domain/entities/X32State.ts +78 -0
  31. package/src/domain/models/MeterConfig.ts +22 -0
  32. package/src/domain/models/MeterData.ts +59 -0
  33. package/src/domain/models/OscMessage.ts +93 -0
  34. package/src/domain/models/X32Address.ts +78 -0
  35. package/src/domain/models/X32Node.ts +43 -0
  36. package/src/domain/models/types.ts +96 -0
  37. package/src/domain/ports/ILogger.ts +27 -0
  38. package/src/domain/ports/INetworkGateway.ts +8 -0
  39. package/src/domain/ports/IStateRepository.ts +16 -0
  40. package/src/domain/services/MeterService.ts +46 -0
  41. package/src/domain/services/OscMessageHandler.ts +88 -0
  42. package/src/domain/services/SchemaFactory.ts +308 -0
  43. package/src/domain/services/SchemaRegistry.ts +67 -0
  44. package/src/domain/services/StaticResponseService.ts +52 -0
  45. package/src/domain/services/strategies/BatchStrategy.ts +74 -0
  46. package/src/domain/services/strategies/MeterStrategy.ts +45 -0
  47. package/src/domain/services/strategies/NodeDiscoveryStrategy.ts +36 -0
  48. package/src/domain/services/strategies/OscCommandStrategy.ts +22 -0
  49. package/src/domain/services/strategies/StateAccessStrategy.ts +71 -0
  50. package/src/domain/services/strategies/StaticResponseStrategy.ts +42 -0
  51. package/src/domain/services/strategies/SubscriptionStrategy.ts +56 -0
  52. package/src/infrastructure/mappers/OscCodec.ts +54 -0
  53. package/src/infrastructure/repositories/InMemoryStateRepository.ts +21 -0
  54. package/src/infrastructure/services/ConsoleLogger.ts +177 -0
  55. package/src/infrastructure/services/UdpNetworkGateway.ts +71 -0
  56. package/src/presentation/cli/server.ts +194 -0
  57. package/src/presentation/library/library.ts +9 -0
  58. package/tests/application/use-cases/BroadcastUpdatesUseCase.test.ts +104 -0
  59. package/tests/application/use-cases/ManageSessionsUseCase.test.ts +12 -0
  60. package/tests/application/use-cases/ProcessPacketUseCase.test.ts +49 -0
  61. package/tests/application/use-cases/SimulationService.test.ts +77 -0
  62. package/tests/domain/entities/SubscriptionManager.test.ts +50 -0
  63. package/tests/domain/entities/X32State.test.ts +52 -0
  64. package/tests/domain/models/MeterData.test.ts +23 -0
  65. package/tests/domain/models/OscMessage.test.ts +38 -0
  66. package/tests/domain/models/X32Address.test.ts +30 -0
  67. package/tests/domain/models/X32Node.test.ts +30 -0
  68. package/tests/domain/services/MeterService.test.ts +27 -0
  69. package/tests/domain/services/OscMessageHandler.test.ts +51 -0
  70. package/tests/domain/services/SchemaRegistry.test.ts +47 -0
  71. package/tests/domain/services/StaticResponseService.test.ts +15 -0
  72. package/tests/domain/services/strategies/BatchStrategy.test.ts +41 -0
  73. package/tests/domain/services/strategies/MeterStrategy.test.ts +19 -0
  74. package/tests/domain/services/strategies/NodeDiscoveryStrategy.test.ts +22 -0
  75. package/tests/domain/services/strategies/StateAccessStrategy.test.ts +49 -0
  76. package/tests/domain/services/strategies/StaticResponseStrategy.test.ts +15 -0
  77. package/tests/domain/services/strategies/SubscriptionStrategy.test.ts +45 -0
  78. package/tests/infrastructure/mappers/OscCodec.test.ts +41 -0
  79. package/tests/infrastructure/repositories/InMemoryStateRepository.test.ts +29 -0
  80. package/tests/infrastructure/services/ConsoleLogger.test.ts +74 -0
  81. package/tests/infrastructure/services/UdpNetworkGateway.test.ts +61 -0
  82. package/tests/presentation/cli/server.test.ts +178 -0
  83. package/tests/presentation/library/library.test.ts +13 -0
  84. package/tsconfig.json +21 -0
  85. package/tsdown.config.ts +15 -0
  86. package/vitest.config.ts +9 -0
@@ -0,0 +1,38 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { OscMessage } from '../../../src/domain/models/OscMessage';
3
+
4
+ describe('OscMessage', () => {
5
+ it('should create an instance correctly', () => {
6
+ const msg = new OscMessage('/test', [1, 'string', 0.5]);
7
+ expect(msg.address).toBe('/test');
8
+ expect(msg.args).toEqual([1, 'string', 0.5]);
9
+ });
10
+
11
+ it('should validate address prefix', () => {
12
+ const msg = new OscMessage('/ch/01/mix', []);
13
+ expect(msg.startsWith('/ch')).toBe(true);
14
+ expect(msg.startsWith('/bus')).toBe(false);
15
+ });
16
+
17
+ it('should retrieve typed arguments safely', () => {
18
+ const msg = new OscMessage('/test', [123, 'hello']);
19
+ expect(msg.getArgAsNumber(0)).toBe(123);
20
+ expect(msg.getArgAsString(1)).toBe('hello');
21
+ });
22
+
23
+ it('should throw when retrieving wrong type', () => {
24
+ const msg = new OscMessage('/test', [123]);
25
+ expect(() => msg.getArgAsString(0)).toThrow();
26
+ });
27
+
28
+ it('should create from packet', () => {
29
+ const packet = {
30
+ oscType: 'message' as const,
31
+ address: '/foo',
32
+ args: [{ type: 'i', value: 10 }, 20]
33
+ };
34
+ const msg = OscMessage.fromPacket(packet);
35
+ expect(msg.address).toBe('/foo');
36
+ expect(msg.args).toEqual([10, 20]);
37
+ });
38
+ });
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { X32Address } from '../../../src/domain/models/X32Address';
3
+
4
+ describe('X32Address', () => {
5
+ it('should parse path correctly', () => {
6
+ const addr = new X32Address('/ch/01/mix/fader');
7
+ expect(addr.path).toBe('/ch/01/mix/fader');
8
+ expect(addr.root).toBe('ch');
9
+ expect(addr.index).toBe('01');
10
+ expect(addr.suffix).toBe('/mix/fader');
11
+ });
12
+
13
+ it('should handle short paths', () => {
14
+ const addr = new X32Address('/status');
15
+ expect(addr.root).toBe('status');
16
+ expect(addr.index).toBeUndefined();
17
+ expect(addr.suffix).toBe('');
18
+ });
19
+
20
+ it('should match category', () => {
21
+ const addr = new X32Address('/ch/01');
22
+ expect(addr.isCategory('ch')).toBe(true);
23
+ expect(addr.isCategory('bus')).toBe(false);
24
+ });
25
+
26
+ it('should match regex pattern', () => {
27
+ const addr = new X32Address('/ch/01/mix/on');
28
+ expect(addr.matches(/^\/ch\/\d+\/mix\/on$/)).toBe(true);
29
+ });
30
+ });
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { X32Node } from '../../../src/domain/models/X32Node';
3
+
4
+ describe('X32Node', () => {
5
+ it('should validate float types', () => {
6
+ const node = new X32Node('f', 0.0);
7
+ expect(node.validate(0.5)).toBe(true);
8
+ expect(node.validate(1)).toBe(true);
9
+ expect(node.validate('0.5')).toBe(false);
10
+ });
11
+
12
+ it('should validate int types', () => {
13
+ const node = new X32Node('i', 0);
14
+ expect(node.validate(1)).toBe(true);
15
+ expect(node.validate(1.5)).toBe(true); // JS numbers are floats, simulation logic accepts it
16
+ expect(node.validate('1')).toBe(false);
17
+ });
18
+
19
+ it('should validate string types', () => {
20
+ const node = new X32Node('s', '');
21
+ expect(node.validate('text')).toBe(true);
22
+ expect(node.validate(123)).toBe(false);
23
+ });
24
+
25
+ it('should create from factory', () => {
26
+ const node = X32Node.from({ type: 'i', default: 1 });
27
+ expect(node.type).toBe('i');
28
+ expect(node.default).toBe(1);
29
+ });
30
+ });
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { MeterService } from '../../../src/domain/services/MeterService';
3
+ import { X32State } from '../../../src/domain/entities/X32State';
4
+ import { X32Node } from '../../../src/domain/models/X32Node';
5
+
6
+ describe('MeterService', () => {
7
+ it('should generate meter data with noise floor', () => {
8
+ const service = new MeterService();
9
+ // /meters/0 has 70 meters
10
+ const data = service.generateMeterData('/meters/0');
11
+ expect(data.path).toBe('/meters/0');
12
+ expect(data.values).toHaveLength(70);
13
+ // Expect small random values
14
+ expect(data.values[0]).toBeGreaterThanOrEqual(0);
15
+ expect(data.values[0]).toBeLessThan(0.06);
16
+ });
17
+
18
+ it('should simulate fader signal', () => {
19
+ const service = new MeterService();
20
+ const state = new X32State({ '/ch/01/mix/fader': new X32Node('f', 0.0) });
21
+ state.set('/ch/01/mix/fader', 0.8);
22
+
23
+ const data = service.generateMeterData('/meters/1', state);
24
+ // Meter 1 corresponds to CH 01
25
+ expect(data.values[0]).toBeGreaterThan(0.5); // Should reflect fader level
26
+ });
27
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { OscMessageHandler } from '../../../src/domain/services/OscMessageHandler';
3
+ import { X32State } from '../../../src/domain/entities/X32State';
4
+ import { SubscriptionManager } from '../../../src/domain/entities/SubscriptionManager';
5
+ import { MeterService } from '../../../src/domain/services/MeterService';
6
+ import { SchemaRegistry } from '../../../src/domain/services/SchemaRegistry';
7
+ import { StaticResponseService } from '../../../src/domain/services/StaticResponseService';
8
+ import { ILogger } from '../../../src/domain/ports/ILogger';
9
+
10
+ describe('OscMessageHandler', () => {
11
+ let handler: OscMessageHandler;
12
+ let state: X32State;
13
+ let subManager: SubscriptionManager;
14
+ let logger: ILogger;
15
+
16
+ beforeEach(() => {
17
+ state = new X32State({});
18
+ logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
19
+ subManager = new SubscriptionManager(logger);
20
+ const meterService = new MeterService();
21
+ const schemaRegistry = {
22
+ has: vi.fn().mockReturnValue(false),
23
+ getNode: vi.fn(),
24
+ getAllPaths: vi.fn().mockReturnValue([])
25
+ } as unknown as SchemaRegistry;
26
+ const staticResponseService = new StaticResponseService();
27
+
28
+ handler = new OscMessageHandler(
29
+ state,
30
+ subManager,
31
+ logger,
32
+ '127.0.0.1',
33
+ 'Mixer',
34
+ 'X32',
35
+ meterService,
36
+ schemaRegistry,
37
+ staticResponseService
38
+ );
39
+ });
40
+
41
+ it('should dispatch /status to StaticResponseStrategy', () => {
42
+ const replies = handler.handle({ address: '/status', args: [] }, { address: '1.2.3.4', port: 1234 });
43
+ expect(replies).toHaveLength(1);
44
+ expect(replies[0].address).toBe('/status');
45
+ });
46
+
47
+ it('should warn on unknown command', () => {
48
+ handler.handle({ address: '/unknown', args: [] }, { address: '1.2.3.4', port: 1234 });
49
+ expect(logger.warn).toHaveBeenCalled();
50
+ });
51
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SchemaRegistry } from '../../../src/domain/services/SchemaRegistry';
3
+ import { SchemaFactory } from '../../../src/domain/services/SchemaFactory';
4
+
5
+ describe('SchemaRegistry', () => {
6
+ const factory = new SchemaFactory();
7
+ const registry = new SchemaRegistry(factory);
8
+
9
+ it('should load full schema', () => {
10
+ const schema = registry.getSchema();
11
+ expect(Object.keys(schema).length).toBeGreaterThan(1000);
12
+ });
13
+
14
+ it('should find specific nodes', () => {
15
+ const node = registry.getNode('/ch/01/mix/fader');
16
+ expect(node).toBeDefined();
17
+ expect(node?.type).toBe('f');
18
+ });
19
+
20
+ it('should map index to root', () => {
21
+ expect(registry.getRootFromIndex(0)).toBe('/ch/01');
22
+ expect(registry.getRootFromIndex(31)).toBe('/ch/32');
23
+ expect(registry.getRootFromIndex(32)).toBe('/auxin/01');
24
+ expect(registry.getRootFromIndex(39)).toBe('/auxin/08');
25
+ expect(registry.getRootFromIndex(40)).toBe('/fxrtn/01');
26
+ expect(registry.getRootFromIndex(47)).toBe('/fxrtn/08');
27
+ expect(registry.getRootFromIndex(48)).toBe('/bus/01');
28
+ expect(registry.getRootFromIndex(63)).toBe('/bus/16');
29
+ expect(registry.getRootFromIndex(64)).toBe('/mtx/01');
30
+ expect(registry.getRootFromIndex(69)).toBe('/mtx/06');
31
+ expect(registry.getRootFromIndex(70)).toBe('/main/st');
32
+ expect(registry.getRootFromIndex(71)).toBe('/main/m');
33
+ expect(registry.getRootFromIndex(72)).toBe('/dca/01');
34
+ expect(registry.getRootFromIndex(79)).toBe('/dca/08');
35
+ expect(registry.getRootFromIndex(80)).toBeNull();
36
+ });
37
+
38
+ it('should check if path exists', () => {
39
+ expect(registry.has('/ch/01/mix/fader')).toBe(true);
40
+ expect(registry.has('/non/existent')).toBe(false);
41
+ });
42
+
43
+ it('should return all paths', () => {
44
+ const paths = registry.getAllPaths();
45
+ expect(paths).toContain('/ch/01/mix/fader');
46
+ });
47
+ });
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { StaticResponseService } from '../../../src/domain/services/StaticResponseService';
3
+
4
+ describe('StaticResponseService', () => {
5
+ it('should return static data', () => {
6
+ const service = new StaticResponseService();
7
+ const resp = service.getResponse('/status');
8
+ expect(resp).toEqual(['active', '{{ip}}', '{{name}}']);
9
+ });
10
+
11
+ it('should return undefined for unknown path', () => {
12
+ const service = new StaticResponseService();
13
+ expect(service.getResponse('/unknown')).toBeUndefined();
14
+ });
15
+ });
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { BatchStrategy } from '../../../../src/domain/services/strategies/BatchStrategy';
3
+ import { SubscriptionManager } from '../../../../src/domain/entities/SubscriptionManager';
4
+
5
+ describe('BatchStrategy', () => {
6
+ it('should handle /formatsubscribe', () => {
7
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
8
+ const manager = new SubscriptionManager(logger);
9
+ const strategy = new BatchStrategy(manager, logger);
10
+
11
+ // /formatsubscribe, alias, pattern, start, end, factor
12
+ strategy.execute({ address: '/formatsubscribe', args: ['alias', '/ch/*/mix/on', 1, 10, 5] }, { address: '1.2.3.4', port: 1234 });
13
+ expect(manager.getSubscribers()).toHaveLength(1);
14
+ expect(manager.getSubscribers()[0].type).toBe('format');
15
+ });
16
+
17
+ it('should handle /batchsubscribe', () => {
18
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
19
+ const manager = new SubscriptionManager(logger);
20
+ const strategy = new BatchStrategy(manager, logger);
21
+
22
+ // /batchsubscribe, alias, path1, path2, ..., int1, int2, ..., factor
23
+ // args: ['alias', '/mix/fader', '/mix/on', 10] (factor=10)
24
+ strategy.execute({ address: '/batchsubscribe', args: ['b_alias', '/mix/fader', '/mix/on', 10] }, { address: '1.2.3.4', port: 1234 });
25
+
26
+ const subs = manager.getSubscribers();
27
+ expect(subs).toHaveLength(1);
28
+ expect(subs[0].type).toBe('batch');
29
+ expect(subs[0].paths).toEqual(['/mix/fader', '/mix/on']);
30
+ expect(subs[0].factor).toBe(10);
31
+ });
32
+
33
+ it('should return empty if no integer found in batchsubscribe', () => {
34
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
35
+ const manager = new SubscriptionManager(logger);
36
+ const strategy = new BatchStrategy(manager, logger);
37
+
38
+ const replies = strategy.execute({ address: '/batchsubscribe', args: ['alias', 'path'] }, { address: '1.2.3.4', port: 1234 });
39
+ expect(replies).toHaveLength(0);
40
+ });
41
+ });
@@ -0,0 +1,19 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { MeterStrategy } from '../../../../src/domain/services/strategies/MeterStrategy';
3
+ import { SubscriptionManager } from '../../../../src/domain/entities/SubscriptionManager';
4
+ import { MeterService } from '../../../../src/domain/services/MeterService';
5
+ import { X32State } from '../../../../src/domain/entities/X32State';
6
+
7
+ describe('MeterStrategy', () => {
8
+ it('should handle /meters', () => {
9
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
10
+ const manager = new SubscriptionManager(logger);
11
+ const meterService = new MeterService();
12
+ const state = new X32State({});
13
+ const strategy = new MeterStrategy(manager, state, meterService);
14
+
15
+ strategy.execute({ address: '/meters', args: ['/meters/1'] }, { address: '1.2.3.4', port: 1234 });
16
+ expect(manager.getSubscribers()).toHaveLength(1);
17
+ expect(manager.getSubscribers()[0].type).toBe('meter');
18
+ });
19
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { NodeDiscoveryStrategy } from '../../../../src/domain/services/strategies/NodeDiscoveryStrategy';
3
+ import { SchemaRegistry } from '../../../../src/domain/services/SchemaRegistry';
4
+
5
+ describe('NodeDiscoveryStrategy', () => {
6
+ it('should list children', () => {
7
+ // Mock registry with getAllPaths
8
+ const registry = {
9
+ getAllPaths: () => ['/ch/01/mix', '/ch/01/config', '/bus/01']
10
+ } as unknown as SchemaRegistry;
11
+
12
+ const strategy = new NodeDiscoveryStrategy(registry);
13
+
14
+ // Query /ch/01
15
+ const replies = strategy.execute({ address: '/node', args: ['/ch/01'] }, { address: '127.0.0.1', port: 10000 });
16
+
17
+ expect(replies).toHaveLength(1);
18
+ expect(replies[0].args).toContain('mix');
19
+ expect(replies[0].args).toContain('config');
20
+ expect(replies[0].args).not.toContain('bus');
21
+ });
22
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { StateAccessStrategy } from '../../../../src/domain/services/strategies/StateAccessStrategy';
3
+ import { X32State } from '../../../../src/domain/entities/X32State';
4
+ import { SchemaRegistry } from '../../../../src/domain/services/SchemaRegistry';
5
+ import { X32Node } from '../../../../src/domain/models/X32Node';
6
+
7
+ describe('StateAccessStrategy', () => {
8
+ let strategy: StateAccessStrategy;
9
+ let state: X32State;
10
+
11
+ beforeEach(() => {
12
+ // Mini schema for testing
13
+ const schema = {
14
+ '/test/fader': new X32Node('f', 0.0),
15
+ '/config/mute/1': new X32Node('i', 0),
16
+ };
17
+
18
+ // Mock registry to return our mini schema nodes
19
+ const registry = {
20
+ has: (path: string) => path in schema,
21
+ getNode: (path: string) => schema[path as keyof typeof schema]
22
+ } as unknown as SchemaRegistry;
23
+
24
+ state = new X32State(schema);
25
+
26
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
27
+
28
+ strategy = new StateAccessStrategy(state, logger, registry);
29
+ });
30
+
31
+ it('should handle get requests', () => {
32
+ state.set('/test/fader', 0.5);
33
+ const replies = strategy.execute({ address: '/test/fader', args: [] }, { address: '127.0.0.1', port: 10000 });
34
+ expect(replies).toHaveLength(1);
35
+ expect(replies[0].args[0]).toBe(0.5);
36
+ });
37
+
38
+ it('should handle set requests', () => {
39
+ const replies = strategy.execute({ address: '/test/fader', args: [0.8] }, { address: '127.0.0.1', port: 10000 });
40
+ expect(state.get('/test/fader')).toBe(0.8);
41
+ expect(replies).toHaveLength(0);
42
+ });
43
+
44
+ it('should validate types', () => {
45
+ // Sending string to float
46
+ strategy.execute({ address: '/test/fader', args: ['invalid'] }, { address: '127.0.0.1', port: 10000 });
47
+ expect(state.get('/test/fader')).toBe(0.0); // Should not change
48
+ });
49
+ });
@@ -0,0 +1,15 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { StaticResponseStrategy } from '../../../../src/domain/services/strategies/StaticResponseStrategy';
3
+ import { StaticResponseService } from '../../../../src/domain/services/StaticResponseService';
4
+
5
+ describe('StaticResponseStrategy', () => {
6
+ it('should return static response with replacements', () => {
7
+ const service = new StaticResponseService();
8
+ const strategy = new StaticResponseStrategy('10.0.0.1', 'MyMixer', 'X32', service);
9
+
10
+ // /status -> ["active", "{{ip}}", "{{name}}"]
11
+ const replies = strategy.execute({ address: '/status', args: [] }, { address: '127.0.0.1', port: 10000 });
12
+
13
+ expect(replies[0].args).toEqual(['active', '10.0.0.1', 'MyMixer']);
14
+ });
15
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { SubscriptionStrategy } from '../../../../src/domain/services/strategies/SubscriptionStrategy';
3
+ import { SubscriptionManager } from '../../../../src/domain/entities/SubscriptionManager';
4
+ import { X32State } from '../../../../src/domain/entities/X32State';
5
+ import { X32Node } from '../../../../src/domain/models/X32Node';
6
+
7
+ describe('SubscriptionStrategy', () => {
8
+ it('should handle /subscribe', () => {
9
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
10
+ const manager = new SubscriptionManager(logger);
11
+ const state = new X32State({ '/test': new X32Node('i', 123) });
12
+ const strategy = new SubscriptionStrategy(manager, state, logger);
13
+
14
+ const replies = strategy.execute({ address: '/subscribe', args: ['/test', 10] }, { address: '1.2.3.4', port: 1234 });
15
+
16
+ // Should add subscriber
17
+ expect(manager.getSubscribers()).toHaveLength(1);
18
+ // Should return current value
19
+ expect(replies[0].args[0]).toBe(123);
20
+ });
21
+
22
+ it('should handle /xremote', () => {
23
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
24
+ const manager = new SubscriptionManager(logger);
25
+ const state = new X32State({});
26
+ const strategy = new SubscriptionStrategy(manager, state, logger);
27
+
28
+ strategy.execute({ address: '/xremote', args: [] }, { address: '1.2.3.4', port: 1234 });
29
+ expect(manager.getSubscribers()).toHaveLength(1);
30
+ });
31
+
32
+ it('should handle /unsubscribe', () => {
33
+ const logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
34
+ const manager = new SubscriptionManager(logger);
35
+ const state = new X32State({});
36
+ const strategy = new SubscriptionStrategy(manager, state, logger);
37
+ const client = { address: '1.2.3.4', port: 1234 };
38
+
39
+ manager.addPathSubscriber(client, '/test');
40
+ expect(manager.getSubscribers()).toHaveLength(1);
41
+
42
+ strategy.execute({ address: '/unsubscribe', args: ['/test'] }, client);
43
+ expect(manager.getSubscribers()).toHaveLength(0);
44
+ });
45
+ });
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { OscCodec } from '../../../src/infrastructure/mappers/OscCodec';
3
+ import { SchemaRegistry } from '../../../src/domain/services/SchemaRegistry';
4
+ import { X32Node } from '../../../src/domain/models/X32Node';
5
+
6
+ describe('OscCodec', () => {
7
+ const registry = {
8
+ getNode: (path: string) => {
9
+ if (path === '/test/int') return new X32Node('i', 0);
10
+ if (path === '/test/float') return new X32Node('f', 0.0);
11
+ if (path === '/test/string') return new X32Node('s', '');
12
+ return undefined;
13
+ }
14
+ } as unknown as SchemaRegistry;
15
+
16
+ const codec = new OscCodec(registry);
17
+
18
+ it('should encode message with schema inference', () => {
19
+ // Int
20
+ const buf1 = codec.encode('/test/int', [123]);
21
+ expect(Buffer.isBuffer(buf1)).toBe(true);
22
+
23
+ // Float
24
+ const buf2 = codec.encode('/test/float', [0.5]);
25
+ expect(Buffer.isBuffer(buf2)).toBe(true);
26
+
27
+ // String
28
+ const buf3 = codec.encode('/test/string', ['hello']);
29
+ expect(Buffer.isBuffer(buf3)).toBe(true);
30
+ });
31
+
32
+ it('should handle raw typed args in encode', () => {
33
+ const buf = codec.encode('/any', [{ type: 'i', value: 10 }]);
34
+ expect(Buffer.isBuffer(buf)).toBe(true);
35
+ });
36
+
37
+ it('should handle buffers in encode', () => {
38
+ const buf = codec.encode('/any', [Buffer.alloc(4)]);
39
+ expect(Buffer.isBuffer(buf)).toBe(true);
40
+ });
41
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { InMemoryStateRepository } from '../../../src/infrastructure/repositories/InMemoryStateRepository';
3
+ import { SchemaRegistry } from '../../../src/domain/services/SchemaRegistry';
4
+ import { ILogger } from '../../../src/domain/ports/ILogger';
5
+
6
+ describe('InMemoryStateRepository', () => {
7
+ it('should initialize and provide state', () => {
8
+ const logger: ILogger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
9
+ const schemaRegistry = {
10
+ getSchema: vi.fn().mockReturnValue({})
11
+ } as unknown as SchemaRegistry;
12
+
13
+ const repo = new InMemoryStateRepository(logger, schemaRegistry);
14
+ const state = repo.getState();
15
+
16
+ expect(state).toBeDefined();
17
+ expect(repo.getState()).toBe(state); // Singleton check
18
+ });
19
+
20
+ it('should reset state', () => {
21
+ const logger: ILogger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
22
+ const schemaRegistry = { getSchema: vi.fn().mockReturnValue({}) } as unknown as SchemaRegistry;
23
+ const repo = new InMemoryStateRepository(logger, schemaRegistry);
24
+
25
+ // We can't easily see internal reset without mocking X32State,
26
+ // but we can verify it doesn't throw.
27
+ expect(() => repo.reset()).not.toThrow();
28
+ });
29
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { ConsoleLogger, LogLevel } from '../../../src/infrastructure/services/ConsoleLogger';
3
+ import { LogCategory } from '../../../src/domain/ports/ILogger';
4
+
5
+ describe('ConsoleLogger', () => {
6
+ let logger: ConsoleLogger;
7
+
8
+ beforeEach(() => {
9
+ logger = new ConsoleLogger();
10
+ vi.spyOn(console, 'log').mockImplementation(() => {});
11
+ vi.clearAllMocks();
12
+ });
13
+
14
+ it('should log messages at all levels', () => {
15
+ logger.setLevel(LogLevel.DEBUG);
16
+ logger.debug(LogCategory.SYSTEM, 'debug');
17
+ logger.info(LogCategory.SYSTEM, 'info');
18
+ logger.warn(LogCategory.SYSTEM, 'warn');
19
+ logger.error(LogCategory.SYSTEM, 'error');
20
+ expect(console.log).toHaveBeenCalledTimes(4);
21
+ });
22
+
23
+ it('should not log if level is higher', () => {
24
+ logger.setLevel(LogLevel.ERROR);
25
+ logger.info(LogCategory.SYSTEM, 'test info');
26
+ expect(console.log).not.toHaveBeenCalled();
27
+ });
28
+
29
+ it('should format message with color codes', () => {
30
+ logger.setLevel(LogLevel.INFO);
31
+ logger.info(LogCategory.SYSTEM, 'hello');
32
+ const call = vi.mocked(console.log).mock.calls[0][0];
33
+ expect(call).toContain('[SYSTEM ]');
34
+ expect(call).toContain('hello');
35
+ });
36
+
37
+ it('should handle data metadata', () => {
38
+ logger.info(LogCategory.SYSTEM, 'msg', { foo: 'bar' });
39
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('{"foo":"bar"}'));
40
+
41
+ // Buffer data
42
+ logger.info(LogCategory.SYSTEM, 'buf', Buffer.from('abc'));
43
+ expect(console.log).toHaveBeenCalledWith(expect.stringContaining('<Buffer 3b>'));
44
+ });
45
+
46
+ it('should enable and disable categories', () => {
47
+ logger.disableCategory(LogCategory.SYSTEM);
48
+ logger.info(LogCategory.SYSTEM, 'hidden');
49
+ expect(console.log).not.toHaveBeenCalled();
50
+
51
+ logger.enableCategory(LogCategory.SYSTEM);
52
+ logger.info(LogCategory.SYSTEM, 'shown');
53
+ expect(console.log).toHaveBeenCalled();
54
+ });
55
+
56
+ it('should singleton instance', () => {
57
+ const instance = ConsoleLogger.getInstance();
58
+ expect(instance).toBeInstanceOf(ConsoleLogger);
59
+ expect(ConsoleLogger.getInstance()).toBe(instance);
60
+ });
61
+
62
+ it('should hide messages matching patterns', () => {
63
+ process.env.HIDDEN_LOG = 'secret,private';
64
+ const loggerWithEnv = new ConsoleLogger();
65
+
66
+ loggerWithEnv.info(LogCategory.SYSTEM, 'this is secret');
67
+ expect(console.log).not.toHaveBeenCalled();
68
+
69
+ loggerWithEnv.info(LogCategory.SYSTEM, 'this is public');
70
+ expect(console.log).toHaveBeenCalled();
71
+
72
+ delete process.env.HIDDEN_LOG;
73
+ });
74
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { UdpNetworkGateway } from '../../../src/infrastructure/services/UdpNetworkGateway';
3
+ import { ILogger } from '../../../src/domain/ports/ILogger';
4
+ import { OscCodec } from '../../../src/infrastructure/mappers/OscCodec';
5
+ import * as dgram from 'dgram';
6
+
7
+ let messageCallback: Function;
8
+
9
+ vi.mock('dgram', () => ({
10
+ createSocket: vi.fn(() => ({
11
+ bind: vi.fn((port, ip, cb) => cb && cb()),
12
+ on: vi.fn((event, cb) => {
13
+ if (event === 'message') messageCallback = cb;
14
+ }),
15
+ send: vi.fn(),
16
+ close: vi.fn((cb) => cb && cb()),
17
+ }))
18
+ }));
19
+
20
+ describe('UdpNetworkGateway', () => {
21
+ let gateway: UdpNetworkGateway;
22
+ let logger: ILogger;
23
+ let codec: OscCodec;
24
+
25
+ beforeEach(() => {
26
+ logger = { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() };
27
+ codec = {
28
+ encode: vi.fn().mockReturnValue(Buffer.from('encoded')),
29
+ decode: vi.fn().mockReturnValue({ address: '/decoded', args: [] })
30
+ } as unknown as OscCodec;
31
+ gateway = new UdpNetworkGateway(logger, codec);
32
+ });
33
+
34
+ it('should start and bind socket', async () => {
35
+ await gateway.start(10023, '0.0.0.0');
36
+ expect(dgram.createSocket).toHaveBeenCalledWith('udp4');
37
+ });
38
+
39
+ it('should send packets', async () => {
40
+ await gateway.start(10023, '0.0.0.0');
41
+ gateway.send({ address: '1.2.3.4', port: 1234 }, '/test', [1]);
42
+ expect(codec.encode).toHaveBeenCalledWith('/test', [1]);
43
+ });
44
+
45
+ it('should receive and decode packets', async () => {
46
+ const spy = vi.fn();
47
+ gateway.onPacket(spy);
48
+ await gateway.start(10023, '0.0.0.0');
49
+
50
+ // Simulate incoming message
51
+ messageCallback(Buffer.from('raw'), { address: '5.6.7.8', port: 5678 });
52
+
53
+ expect(codec.decode).toHaveBeenCalled();
54
+ expect(spy).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ address: '5.6.7.8' }));
55
+ });
56
+
57
+ it('should stop', async () => {
58
+ await gateway.start(10023, '0.0.0.0');
59
+ await gateway.stop();
60
+ });
61
+ });