@matterbridge/test-utils 3.10.7-dev-20260823-f911eb8

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 (57) hide show
  1. package/dist/buntest/export.d.ts +25 -0
  2. package/dist/buntest/export.d.ts.map +1 -0
  3. package/dist/buntest/export.js +26 -0
  4. package/dist/buntest/export.js.map +1 -0
  5. package/dist/buntest/matterTest.d.ts +351 -0
  6. package/dist/buntest/matterTest.d.ts.map +1 -0
  7. package/dist/buntest/matterTest.js +722 -0
  8. package/dist/buntest/matterTest.js.map +1 -0
  9. package/dist/buntest/setupTest.d.ts +84 -0
  10. package/dist/buntest/setupTest.d.ts.map +1 -0
  11. package/dist/buntest/setupTest.js +153 -0
  12. package/dist/buntest/setupTest.js.map +1 -0
  13. package/dist/export.d.ts +25 -0
  14. package/dist/export.d.ts.map +1 -0
  15. package/dist/export.js +25 -0
  16. package/dist/export.js.map +1 -0
  17. package/dist/flushAsync.d.ts +36 -0
  18. package/dist/flushAsync.d.ts.map +1 -0
  19. package/dist/flushAsync.js +43 -0
  20. package/dist/flushAsync.js.map +1 -0
  21. package/dist/jest/export.d.ts +25 -0
  22. package/dist/jest/export.d.ts.map +1 -0
  23. package/dist/jest/export.js +26 -0
  24. package/dist/jest/export.js.map +1 -0
  25. package/dist/jest/matterTest.d.ts +351 -0
  26. package/dist/jest/matterTest.d.ts.map +1 -0
  27. package/dist/jest/matterTest.js +722 -0
  28. package/dist/jest/matterTest.js.map +1 -0
  29. package/dist/jest/setupTest.d.ts +83 -0
  30. package/dist/jest/setupTest.d.ts.map +1 -0
  31. package/dist/jest/setupTest.js +154 -0
  32. package/dist/jest/setupTest.js.map +1 -0
  33. package/dist/logKeepAlives.d.ts +32 -0
  34. package/dist/logKeepAlives.d.ts.map +1 -0
  35. package/dist/logKeepAlives.js +69 -0
  36. package/dist/logKeepAlives.js.map +1 -0
  37. package/dist/matter/export.d.ts +24 -0
  38. package/dist/matter/export.d.ts.map +1 -0
  39. package/dist/matter/export.js +25 -0
  40. package/dist/matter/export.js.map +1 -0
  41. package/dist/matter/matterRequest.d.ts +257 -0
  42. package/dist/matter/matterRequest.d.ts.map +1 -0
  43. package/dist/matter/matterRequest.js +455 -0
  44. package/dist/matter/matterRequest.js.map +1 -0
  45. package/dist/vitest/export.d.ts +25 -0
  46. package/dist/vitest/export.d.ts.map +1 -0
  47. package/dist/vitest/export.js +26 -0
  48. package/dist/vitest/export.js.map +1 -0
  49. package/dist/vitest/matterTest.d.ts +351 -0
  50. package/dist/vitest/matterTest.d.ts.map +1 -0
  51. package/dist/vitest/matterTest.js +723 -0
  52. package/dist/vitest/matterTest.js.map +1 -0
  53. package/dist/vitest/setupTest.d.ts +62 -0
  54. package/dist/vitest/setupTest.d.ts.map +1 -0
  55. package/dist/vitest/setupTest.js +129 -0
  56. package/dist/vitest/setupTest.js.map +1 -0
  57. package/package.json +97 -0
@@ -0,0 +1,722 @@
1
+ /**
2
+ * @description This file contains the Bun Matter Test Environment.
3
+ * @file buntest/bunMatterTest.ts
4
+ * @author Luca Liguori
5
+ * @created 2026-04-19
6
+ * @version 1.0.0
7
+ * @license Apache-2.0
8
+ *
9
+ * Copyright 2025, 2026, 2027 Luca Liguori.
10
+ *
11
+ * Licensed under the Apache License, Version 2.0 (the "License");
12
+ * you may not use this file except in compliance with the License.
13
+ * You may obtain a copy of the License at
14
+ *
15
+ * http://www.apache.org/licenses/LICENSE-2.0
16
+ *
17
+ * Unless required by applicable law or agreed to in writing, software
18
+ * distributed under the License is distributed on an "AS IS" BASIS,
19
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
+ * See the License for the specific language governing permissions and
21
+ * limitations under the License.
22
+ */
23
+ // oxlint-disable-next-line import/no-unassigned-import
24
+ import '@matter/nodejs'; // Set up Node.js environment for matter.js
25
+ import { expect } from 'bun:test';
26
+ import path from 'node:path';
27
+ // @matter
28
+ import { Environment, Lifecycle, LogFormat as MatterLogFormat, LogLevel as MatterLogLevel } from '@matter/general';
29
+ import { Endpoint, ServerNode, ServerNodeStore } from '@matter/node';
30
+ import { BridgedDeviceBasicInformationServer } from '@matter/node/behaviors/bridged-device-basic-information';
31
+ import { DescriptorServer } from '@matter/node/behaviors/descriptor';
32
+ import { OnOffServer } from '@matter/node/behaviors/on-off';
33
+ import { MountedOnOffControlDevice } from '@matter/node/devices/mounted-on-off-control';
34
+ import { AggregatorEndpoint } from '@matter/node/endpoints';
35
+ import { DeviceTypeId, VendorId } from '@matter/types/datatype';
36
+ // @matterbridge
37
+ import { MATTER_STORAGE_DIR } from '@matterbridge/types';
38
+ import { inspectError } from '@matterbridge/utils/error';
39
+ // node-ansi-logger module
40
+ import { er, rs } from 'node-ansi-logger';
41
+ // local modules
42
+ import { flushAsync } from '../flushAsync.js';
43
+ import { HOMEDIR, log, NAME } from './setupTest.js';
44
+ export let environment;
45
+ export let server;
46
+ export let aggregator;
47
+ const noop = () => undefined;
48
+ /**
49
+ * Create a matter test environment for testing:
50
+ * - it will remove any existing home directory
51
+ * - setup the matter environment with name, debug logging and ANSI format
52
+ *
53
+ * @returns {Environment} - The default matter environment.
54
+ * @example
55
+ * ```typescript
56
+ * const NAME = 'BaseTest';
57
+ * const MATTER_PORT = 8000;
58
+ * const MATTER_CREATE_ONLY = true;
59
+ *
60
+ * // Setup the test environment
61
+ * await setupTest(NAME, false);
62
+ *
63
+ * // Setup the Matter test environment
64
+ * await createTestEnvironment();
65
+ *
66
+ * // Create the server node and aggregator
67
+ * await createServerNode(MATTER_PORT);
68
+ *
69
+ * // Start the server node if not in create-only mode
70
+ * if (!MATTER_CREATE_ONLY) await startServerNode();
71
+ *
72
+ * // Stop or flush the server node depending on the create-only mode
73
+ * if (MATTER_CREATE_ONLY) await flushServerNode();
74
+ * else await stopServerNode();
75
+ *
76
+ * // Destroy the Matter test environment
77
+ * await destroyTestEnvironment();
78
+ * ```
79
+ */
80
+ // oxlint-disable-next-line typescript/require-await
81
+ export async function createTestEnvironment() {
82
+ expect(NAME).toBeDefined();
83
+ expect(typeof NAME).toBe('string');
84
+ expect(NAME.length).toBeGreaterThanOrEqual(4); // avoid accidental deletion of short paths like "/" or "C:\"
85
+ // Setup the matter environment
86
+ environment = Environment.default;
87
+ environment.vars.set('log.level', MatterLogLevel.DEBUG);
88
+ environment.vars.set('log.format', MatterLogFormat.ANSI);
89
+ environment.vars.set('path.root', path.join(HOMEDIR, '.matterbridge', MATTER_STORAGE_DIR));
90
+ environment.vars.set('runtime.signals', false);
91
+ environment.vars.set('runtime.exitcode', false);
92
+ return environment;
93
+ }
94
+ /**
95
+ * Destroy the matter test environment
96
+ *
97
+ * @returns {Promise<void>} A promise that resolves when the test environment is destroyed.
98
+ * @example
99
+ * ```typescript
100
+ * const NAME = 'BaseTest';
101
+ * const MATTER_PORT = 8000;
102
+ * const MATTER_CREATE_ONLY = true;
103
+ *
104
+ * // Setup the test environment
105
+ * await setupTest(NAME, false);
106
+ *
107
+ * // Setup the Matter test environment
108
+ * await createTestEnvironment();
109
+ *
110
+ * // Create the server node and aggregator
111
+ * await createServerNode(MATTER_PORT);
112
+ *
113
+ * // Start the server node if not in create-only mode
114
+ * if (!MATTER_CREATE_ONLY) await startServerNode();
115
+ *
116
+ * // Stop or flush the server node depending on the create-only mode
117
+ * if (MATTER_CREATE_ONLY) await flushServerNode();
118
+ * else await stopServerNode();
119
+ *
120
+ * // Destroy the Matter test environment
121
+ * await destroyTestEnvironment();
122
+ * ```
123
+ */
124
+ export async function destroyTestEnvironment() {
125
+ // Nothing to clean up right now
126
+ }
127
+ /**
128
+ * Get a mocked PlatformMatterbridge.
129
+ *
130
+ * @returns {PlatformMatterbridge} An object representing the mocked PlatformMatterbridge.
131
+ */
132
+ export function getMatterbridge() {
133
+ const matterbridge = {
134
+ systemInformation: {
135
+ interfaceName: 'eth0',
136
+ macAddress: 'aa:bb:cc:dd:ee:ff',
137
+ ipv4Address: '192.168.68.100',
138
+ ipv6Address: 'fd78:cbf8:4939:46e2:51b2:2163:7f88:c33d',
139
+ nodeVersion: '24.16.0',
140
+ hostname: 'matterbridge',
141
+ user: 'bun',
142
+ osType: 'Linux',
143
+ osRelease: '6.8.0-xxxx',
144
+ osPlatform: 'linux',
145
+ osArch: 'arm64',
146
+ totalMemory: '0 B',
147
+ freeMemory: '0 B',
148
+ systemUptime: '0s',
149
+ processUptime: '0s',
150
+ cpuUsage: '0%',
151
+ processCpuUsage: '0%',
152
+ rss: '0 B',
153
+ heapTotal: '0 B',
154
+ heapUsed: '0 B',
155
+ },
156
+ uuid: '00000000-0000-0000-0000-000000000000',
157
+ rootDirectory: HOMEDIR,
158
+ homeDirectory: HOMEDIR,
159
+ matterbridgeDirectory: path.join(HOMEDIR, '.matterbridge'),
160
+ matterbridgePluginDirectory: path.join(HOMEDIR, 'Matterbridge'),
161
+ matterbridgeCertDirectory: path.join(HOMEDIR, '.mattercert'),
162
+ globalModulesDirectory: path.join(HOMEDIR, 'node_modules'),
163
+ matterbridgeVersion: '3.9.3',
164
+ matterbridgeLatestVersion: '3.9.3',
165
+ matterbridgeDevVersion: '3.9.3',
166
+ frontendVersion: '3.9.3',
167
+ bridgeMode: 'none',
168
+ restartMode: 'none',
169
+ virtualMode: 'mounted_switch',
170
+ aggregatorVendorId: VendorId(0xfff1),
171
+ aggregatorVendorName: 'Matterbridge',
172
+ aggregatorProductId: 0x8000,
173
+ aggregatorProductName: 'Matterbridge Bun',
174
+ };
175
+ return matterbridge;
176
+ }
177
+ /**
178
+ * Add a bridged endpoint
179
+ *
180
+ * @param {string} pluginName The name of the plugin.
181
+ * @param {Endpoint} device The device to add.
182
+ * @returns {Promise<boolean>} A promise that resolves to true if the endpoint was added successfully, or rejects with an error if the operation failed.
183
+ */
184
+ export const addBridgedEndpoint = async (pluginName, device) => {
185
+ try {
186
+ await aggregator.add(device);
187
+ return Promise.resolve(true);
188
+ }
189
+ catch (error) {
190
+ return Promise.reject(error);
191
+ }
192
+ };
193
+ /**
194
+ * Remove a bridged endpoint
195
+ *
196
+ * @param {string} pluginName The name of the plugin.
197
+ * @param {Endpoint} device The device to remove.
198
+ * @returns {Promise<boolean>} A promise that resolves to true if the endpoint was removed successfully, or rejects with an error if the operation failed.
199
+ */
200
+ export const removeBridgedEndpoint = async (pluginName, device) => {
201
+ try {
202
+ await device.delete();
203
+ return Promise.resolve(true);
204
+ }
205
+ catch (error) {
206
+ return Promise.reject(error);
207
+ }
208
+ };
209
+ /**
210
+ * Remove all bridged endpoints
211
+ *
212
+ * @param {string} _pluginName The name of the plugin.
213
+ * @param {number} _delay The delay before removing all endpoints.
214
+ * @returns {Promise<boolean>} A promise that resolves to true if all endpoints were removed successfully, or rejects with an error if the operation failed.
215
+ */
216
+ export const removeAllBridgedEndpoints = async (_pluginName, _delay = 0) => {
217
+ try {
218
+ for (const device of aggregator.parts) {
219
+ await device.delete();
220
+ }
221
+ return Promise.resolve(true);
222
+ }
223
+ catch (error) {
224
+ return Promise.reject(error);
225
+ }
226
+ };
227
+ /**
228
+ * Add a virtual endpoint
229
+ *
230
+ * @param {string} pluginName The name of the plugin.
231
+ * @param {string} name The name of the virtual endpoint.
232
+ * @param {'light' | 'outlet' | 'switch' | 'mounted_switch'} type The type of the virtual endpoint.
233
+ * @param {() => Promise<void>} callback The callback to execute when the virtual endpoint is triggered.
234
+ * @returns {Promise<boolean>} A promise that resolves to true if the virtual endpoint was added successfully, or rejects with an error if the operation failed.
235
+ */
236
+ export const addVirtualEndpoint = async (pluginName, name, type, callback) => {
237
+ try {
238
+ const device = new Endpoint(MountedOnOffControlDevice.with(BridgedDeviceBasicInformationServer), {
239
+ id: name.replaceAll(' ', '') + ':' + type,
240
+ bridgedDeviceBasicInformation: {
241
+ vendorId: VendorId(0xfff1),
242
+ vendorName: 'Matterbridge',
243
+ productName: 'Matterbridge Virtual Device',
244
+ nodeLabel: name.slice(0, 32),
245
+ softwareVersion: 20000,
246
+ softwareVersionString: '2.0.0',
247
+ },
248
+ onOff: { onOff: false },
249
+ });
250
+ // Set up an event listener for when the `onOff` state changes.
251
+ device.events.onOff.onOff$Changed.on((value) => {
252
+ // If the `onOff` state becomes true, turn off the virtual device and execute the callback.
253
+ if (value) {
254
+ void callback().catch(/* istanbul ignore next */ noop);
255
+ void device.setStateOf(OnOffServer, { onOff: false }).catch(/* istanbul ignore next */ noop);
256
+ }
257
+ });
258
+ // Add the created device to the aggregator.
259
+ await aggregator.add(device);
260
+ // Add the OnOffPlugInUnit to MountedOnOffControlDevice (Matter 1.4.2 specs added this (new case of superset) for legacy controllers to recognize the mounted switch).
261
+ await device.act(async (agent) => {
262
+ const descriptor = await agent.load(DescriptorServer);
263
+ descriptor.addDeviceTypes('OnOffPlugInUnit');
264
+ });
265
+ // Initially set the state of the virtual device's `OnOffServer` to false (off).
266
+ await device.setStateOf(OnOffServer, { onOff: false });
267
+ return Promise.resolve(true);
268
+ }
269
+ catch (error) {
270
+ return Promise.reject(error);
271
+ }
272
+ };
273
+ /**
274
+ * Inject matterbridge platform for testing.
275
+ *
276
+ * @param {object} platform The platform to inject.
277
+ *
278
+ * @example
279
+ * ```typescript
280
+ * platform = new MatterbridgePlatform(matterbridge, log, config);
281
+ * // Inject in the platform the Matterbridge environment
282
+ * addMatterbridge(platform);
283
+ * ```
284
+ */
285
+ export function addMatterbridge(platform) {
286
+ expect(platform).toBeDefined();
287
+ // Setup the platform MatterNode helpers
288
+ // @ts-expect-error - setMatterNode is intentionally private
289
+ platform.setMatterNode(addBridgedEndpoint, removeBridgedEndpoint, removeAllBridgedEndpoints, addVirtualEndpoint);
290
+ }
291
+ /**
292
+ * Flush (await) the lazy endpoint number persistence mechanism used by matter.js.
293
+ *
294
+ * Background:
295
+ * assignNumber() batches persistence (store.saveNumber + updating __nextNumber__) via an internal promise (#numbersPersisted).
296
+ * Calling endpointStores.close() waits for the current batch only. If new endpoints were added in the same macrotask
297
+ * cycle additional micro/macro turns might be needed to ensure the batch started. We defensively yield macrotasks
298
+ * (setImmediate) and then await close() multiple rounds.
299
+ *
300
+ * @param {ServerNode} targetServer The server whose endpoint numbering persistence should be flushed.
301
+ * @param {number} rounds Number of macrotask + close cycles to run (3 is usually sufficient).
302
+ * @param {number} ticks Number of macrotask (setImmediate) turns to yield after stopping the server to allow asynchronous work to complete before returning (default 1).
303
+ * @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
304
+ * @param {number} pause Duration in ms to wait between cycles (default 10ms) to allow any follow-up work scheduled by close() to run.
305
+ * @returns {Promise<void>} Resolves when pending number persistence batches have completed.
306
+ */
307
+ export async function flushAllEndpointNumberPersistence(targetServer, rounds = 3, ticks = 1, microTurns = 1, pause = 10) {
308
+ const nodeStore = targetServer.env.get(ServerNodeStore);
309
+ for (let i = 0; i < rounds; i++) {
310
+ await flushAsync(ticks, microTurns, pause);
311
+ await nodeStore.endpointStores.close();
312
+ }
313
+ }
314
+ /**
315
+ * Get the root ServerNode for a given Endpoint by traversing up the owner chain until the root endpoint is reached.
316
+ *
317
+ * @param {Endpoint} endpoint The endpoint to find the root server for.
318
+ * @returns {ServerNode<ServerNode.RootEndpoint>} The root ServerNode of the given endpoint.
319
+ */
320
+ function getRootServerNode(endpoint) {
321
+ let current = endpoint;
322
+ while (current.owner) {
323
+ current = current.owner;
324
+ }
325
+ // oxlint-disable-next-line typescript/no-unsafe-type-assertion
326
+ return current;
327
+ }
328
+ /**
329
+ * Collect all endpoints in the server endpoint tree (root -> descendants).
330
+ *
331
+ * @param {Endpoint} root Root endpoint (typically the ServerNode root endpoint cast as Endpoint).
332
+ * @returns {Endpoint[]} Flat array including the root and every descendant once.
333
+ */
334
+ function collectAllEndpoints(root) {
335
+ const list = [];
336
+ const walk = (ep) => {
337
+ list.push(ep);
338
+ for (const child of ep.parts) {
339
+ walk(child);
340
+ }
341
+ };
342
+ walk(root);
343
+ return list;
344
+ }
345
+ /**
346
+ * Assert that every endpoint attached to the server has an assigned and (batch-)persisted endpoint number.
347
+ *
348
+ * This waits for any outstanding number persistence batch (endpointStores.close()), then traverses the endpoint
349
+ * graph and asserts:
350
+ * - Root endpoint: number is 0 (allowing undefined to coerce to 0 via nullish coalescing check).
351
+ * - All other endpoints: number > 0.
352
+ *
353
+ * @param {ServerNode} targetServer The server whose endpoint numbers are verified.
354
+ * @returns {Promise<void>} Resolves when assertions complete.
355
+ */
356
+ export async function assertAllEndpointNumbersPersisted(targetServer) {
357
+ const nodeStore = targetServer.env.get(ServerNodeStore);
358
+ // Ensure any pending persistence finished (flush any in-flight batch promise)
359
+ await nodeStore.endpointStores.close();
360
+ const all = collectAllEndpoints(targetServer);
361
+ for (const ep of all) {
362
+ const store = nodeStore.storeForEndpoint(ep);
363
+ if (ep.maybeNumber === 0) {
364
+ expect(store.number ?? 0).toBe(0); // root
365
+ }
366
+ else {
367
+ expect(store.number).toBeGreaterThan(0);
368
+ }
369
+ }
370
+ return all.length;
371
+ }
372
+ /**
373
+ * Close the server node stores to flush any pending endpoint number persistence.
374
+ *
375
+ * @param {ServerNode} targetServer The server whose endpoint stores should be closed.
376
+ * @returns {Promise<void>} Resolves when the stores have been closed.
377
+ */
378
+ export async function closeServerNodeStores(targetServer) {
379
+ const resolvedTargetServer = targetServer ?? server;
380
+ await resolvedTargetServer?.env.get(ServerNodeStore)?.endpointStores.close();
381
+ }
382
+ /**
383
+ * Create a matter server node for testing.
384
+ *
385
+ * @param {number} port TCP port to listen on.
386
+ * @param {DeviceTypeId} deviceType Device type identifier for the server node. Defaults to the bridge device type.
387
+ * @param {number} ticks Number of macrotask (setImmediate) turns to yield after starting the server to allow asynchronous work to complete before returning (default 1).
388
+ * @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
389
+ * @param {number} pause Final timer delay in ms to wait after microtask draining to allow any follow-up work scheduled by the server start process to run before returning (default 10ms).
390
+ * @returns {Promise<[ServerNode<ServerNode.RootEndpoint>, Endpoint<AggregatorEndpoint>]>} Resolves to an array containing the created ServerNode and its AggregatorNode.
391
+ * @example
392
+ * ```typescript
393
+ * const NAME = 'BaseTest';
394
+ * const MATTER_PORT = 8000;
395
+ * const MATTER_CREATE_ONLY = true;
396
+ *
397
+ * // Setup the test environment
398
+ * await setupTest(NAME, false);
399
+ *
400
+ * // Setup the Matter test environment
401
+ * await createTestEnvironment();
402
+ *
403
+ * // Create the server node and aggregator
404
+ * await createServerNode(MATTER_PORT);
405
+ *
406
+ * // Start the server node if not in create-only mode
407
+ * if (!MATTER_CREATE_ONLY) await startServerNode();
408
+ *
409
+ * // Stop or flush the server node depending on the create-only mode
410
+ * if (MATTER_CREATE_ONLY) await flushServerNode();
411
+ * else await stopServerNode();
412
+ *
413
+ * // Destroy the Matter test environment
414
+ * await destroyTestEnvironment();
415
+ * ```
416
+ */
417
+ export async function createServerNode(port, deviceType = DeviceTypeId(0x000e), ticks = 1, microTurns = 1, pause = 10) {
418
+ const { randomBytes } = await import('node:crypto');
419
+ const random = randomBytes(8).toString('hex');
420
+ // Create the server node
421
+ server = await ServerNode.create({
422
+ id: NAME + 'ServerNode',
423
+ // Provide the environment
424
+ environment,
425
+ // Provide Node announcement settings
426
+ productDescription: {
427
+ name: NAME + 'ServerNode',
428
+ deviceType: DeviceTypeId(deviceType),
429
+ vendorId: VendorId(0xfff1),
430
+ productId: 0x8000,
431
+ },
432
+ // Provide defaults for the BasicInformation cluster on the Root endpoint
433
+ basicInformation: {
434
+ vendorId: VendorId(0xfff1),
435
+ vendorName: 'Matterbridge',
436
+ productId: 0x8000,
437
+ productName: ('Matterbridge ' + NAME).slice(0, 32), // truncate to 32 chars to satisfy BasicInformation cluster constraints
438
+ productLabel: ('Label ' + NAME).slice(0, 64), // truncate to 64 chars to satisfy BasicInformation cluster constraints
439
+ nodeLabel: (NAME + 'ServerNode').slice(0, 32), // truncate to 32 chars to satisfy BasicInformation cluster constraints
440
+ hardwareVersion: 1,
441
+ softwareVersion: 1,
442
+ reachable: true,
443
+ serialNumber: 'SN' + random,
444
+ uniqueId: 'UI' + random,
445
+ },
446
+ // Provide Network relevant configuration like the port
447
+ network: {
448
+ listeningAddressIpv4: undefined,
449
+ listeningAddressIpv6: undefined,
450
+ port,
451
+ tcp: true,
452
+ transportPreference: 'udp',
453
+ },
454
+ // Provide the certificate for the device
455
+ operationalCredentials: {
456
+ certification: undefined,
457
+ },
458
+ });
459
+ expect(server).toBeDefined();
460
+ expect(server.lifecycle.isReady).toBeTruthy();
461
+ // Create the aggregator node
462
+ aggregator = new Endpoint(AggregatorEndpoint, {
463
+ id: NAME + 'AggregatorNode',
464
+ });
465
+ expect(aggregator).toBeDefined();
466
+ // Add the aggregator to the server
467
+ await server.add(aggregator);
468
+ expect(server.parts.has(aggregator.id)).toBeTruthy();
469
+ expect(server.parts.has(aggregator)).toBeTruthy();
470
+ expect(aggregator.lifecycle.isReady).toBeTruthy();
471
+ expect(server.lifecycle.isOnline).toBeFalsy();
472
+ // Ensure the queue is empty and pause 100ms to allow any pending work to complete before returning the server and aggregator
473
+ await flushAsync(ticks, microTurns, pause);
474
+ return [server, aggregator];
475
+ }
476
+ /**
477
+ * Start a matter server node for testing.
478
+ *
479
+ * @param {number} ticks Number of macrotask (setImmediate) turns to yield after starting the server to allow asynchronous work to complete before returning (default 1).
480
+ * @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
481
+ * @param {number} pause Final timer delay in ms to wait after microtask draining to allow any follow-up work scheduled by the server start process to run before returning (default 10ms).
482
+ * @returns {Promise<[ServerNode<ServerNode.RootEndpoint>, Endpoint<AggregatorEndpoint>]>} Resolves to an array containing the created ServerNode and its AggregatorNode.
483
+ * @example
484
+ * ```typescript
485
+ * const NAME = 'BaseTest';
486
+ * const MATTER_PORT = 8000;
487
+ * const MATTER_CREATE_ONLY = true;
488
+ *
489
+ * // Setup the test environment
490
+ * await setupTest(NAME, false);
491
+ *
492
+ * // Setup the Matter test environment
493
+ * await createTestEnvironment();
494
+ *
495
+ * // Create the server node and aggregator
496
+ * await createServerNode(MATTER_PORT);
497
+ *
498
+ * // Start the server node if not in create-only mode
499
+ * if (!MATTER_CREATE_ONLY) await startServerNode();
500
+ *
501
+ * // Stop or flush the server node depending on the create-only mode
502
+ * if (MATTER_CREATE_ONLY) await flushServerNode();
503
+ * else await stopServerNode();
504
+ *
505
+ * // Destroy the Matter test environment
506
+ * await destroyTestEnvironment();
507
+ * ```
508
+ */
509
+ export async function startServerNode(ticks = 1, microTurns = 1, pause = 10) {
510
+ // Create the server node
511
+ if (!server || !aggregator) {
512
+ // istanbul ignore next
513
+ throw new Error('Server node and aggregator must be created before starting the server. Call createServerNode() first.');
514
+ }
515
+ // Wait for the server to be online
516
+ await new Promise((resolve, reject) => {
517
+ // oxlint-disable-next-line typescript/require-await
518
+ server.lifecycle.online.on(async () => {
519
+ resolve();
520
+ });
521
+ server.start().catch((err) => reject(err));
522
+ });
523
+ // Check if the server is online
524
+ expect(server.lifecycle.isReady).toBeTruthy();
525
+ expect(server.lifecycle.isOnline).toBeTruthy();
526
+ expect(server.lifecycle.isCommissioned).toBeFalsy();
527
+ expect(server.lifecycle.isPartsReady).toBeTruthy();
528
+ expect(server.lifecycle.hasId).toBeTruthy();
529
+ expect(server.lifecycle.hasNumber).toBeTruthy();
530
+ expect(aggregator.lifecycle.isReady).toBeTruthy();
531
+ expect(aggregator.lifecycle.isInstalled).toBeTruthy();
532
+ expect(aggregator.lifecycle.isPartsReady).toBeTruthy();
533
+ expect(aggregator.lifecycle.hasId).toBeTruthy();
534
+ expect(aggregator.lifecycle.hasNumber).toBeTruthy();
535
+ // Ensure the queue is empty
536
+ await flushAsync(ticks, microTurns, pause);
537
+ return [server, aggregator];
538
+ }
539
+ /**
540
+ * Stop a matter server node.
541
+ *
542
+ * @param {number} ticks Number of macrotask (setImmediate) turns to yield after stopping the server to allow asynchronous work to complete before returning (default 1).
543
+ * @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
544
+ * @param {number} pause Final timer delay in ms to wait after microtask draining to allow any follow-up work scheduled by the server stop process to run before returning (default 10ms).
545
+ * @returns {Promise<void>} Resolves when the server has stopped.
546
+ * @example
547
+ * ```typescript
548
+ * const NAME = 'BaseTest';
549
+ * const MATTER_PORT = 8000;
550
+ * const MATTER_CREATE_ONLY = true;
551
+ *
552
+ * // Setup the test environment
553
+ * await setupTest(NAME, false);
554
+ *
555
+ * // Setup the Matter test environment
556
+ * await createTestEnvironment();
557
+ *
558
+ * // Create the server node and aggregator
559
+ * await createServerNode(MATTER_PORT);
560
+ *
561
+ * // Start the server node if not in create-only mode
562
+ * if (!MATTER_CREATE_ONLY) await startServerNode();
563
+ *
564
+ * // Stop or flush the server node depending on the create-only mode
565
+ * if (MATTER_CREATE_ONLY) await flushServerNode();
566
+ * else await stopServerNode();
567
+ *
568
+ * // Destroy the Matter test environment
569
+ * await destroyTestEnvironment();
570
+ * ```
571
+ */
572
+ export async function stopServerNode(ticks = 1, microTurns = 1, pause = 10) {
573
+ // Flush any pending endpoint number persistence
574
+ await flushAllEndpointNumberPersistence(server);
575
+ // Ensure all endpoint numbers are persisted
576
+ await assertAllEndpointNumbersPersisted(server);
577
+ // Stop the server
578
+ expect(server).toBeDefined();
579
+ expect(server.lifecycle.isReady).toBeTruthy();
580
+ expect(server.lifecycle.isOnline).toBeTruthy();
581
+ // Close the server to release UDP sockets and other network resources created
582
+ // by ServerNode.create() (mDNS udp4/udp6 sockets), which would otherwise keep
583
+ // the Bun process alive indefinitely after all tests finish.
584
+ await server.close();
585
+ expect(server.lifecycle.isReady).toBeFalsy();
586
+ expect(server.lifecycle.isOnline).toBeFalsy();
587
+ // Ensure the queue is empty
588
+ await flushAsync(ticks, microTurns, pause);
589
+ }
590
+ /**
591
+ * Flush any pending endpoint number persistence.
592
+ *
593
+ * @param {number} ticks Number of macrotask (setImmediate) turns to yield after stopping the server to allow asynchronous work to complete before returning (default 1).
594
+ * @param {number} microTurns Number of microtask drains (Promise.resolve chains) to perform after macrotask yielding to allow asynchronous work to complete before returning (default 1).
595
+ * @param {number} pause Final timer delay in ms to wait after microtask draining to allow any follow-up work scheduled by the server stop process to run before returning (default 10ms).
596
+ * @returns {Promise<void>} Resolves when the server has stopped.
597
+ * @example
598
+ * ```typescript
599
+ * const NAME = 'BaseTest';
600
+ * const MATTER_PORT = 8000;
601
+ * const MATTER_CREATE_ONLY = true;
602
+ *
603
+ * // Setup the test environment
604
+ * await setupTest(NAME, false);
605
+ *
606
+ * // Setup the Matter test environment
607
+ * await createTestEnvironment();
608
+ *
609
+ * // Create the server node and aggregator
610
+ * await createServerNode(MATTER_PORT);
611
+ *
612
+ * // Start the server node if not in create-only mode
613
+ * if (!MATTER_CREATE_ONLY) await startServerNode();
614
+ *
615
+ * // Stop or flush the server node depending on the create-only mode
616
+ * if (MATTER_CREATE_ONLY) await flushServerNode();
617
+ * else await stopServerNode();
618
+ *
619
+ * // Destroy the Matter test environment
620
+ * await destroyTestEnvironment();
621
+ * ```
622
+ */
623
+ export async function flushServerNode(ticks = 1, microTurns = 1, pause = 10) {
624
+ // Flush any pending endpoint number persistence
625
+ await flushAllEndpointNumberPersistence(server);
626
+ // Ensure all endpoint numbers are persisted
627
+ await assertAllEndpointNumbersPersisted(server);
628
+ // Stop the server
629
+ expect(server).toBeDefined();
630
+ expect(server.lifecycle.isReady).toBeTruthy();
631
+ expect(server.lifecycle.isOnline).toBeFalsy();
632
+ // Close the server to release UDP sockets and other network resources created
633
+ // by ServerNode.create() (mDNS udp4/udp6 sockets), which would otherwise keep
634
+ // the Bun process alive indefinitely after all tests finish.
635
+ await server.close();
636
+ expect(server.lifecycle.isReady).toBeFalsy();
637
+ expect(server.lifecycle.isOnline).toBeFalsy();
638
+ // Ensure the queue is empty
639
+ await flushAsync(ticks, microTurns, pause);
640
+ }
641
+ /**
642
+ * Add a device (endpoint) to a matter server node or an aggregator.
643
+ *
644
+ * @param {ServerNode<ServerNode.RootEndpoint> | Endpoint<AggregatorEndpoint>} owner The server or aggregator to add the device to.
645
+ * @param {Endpoint} device The device to add.
646
+ * @param {number} rounds The number of rounds to perform after addition (default 3).
647
+ * @param {number} pause The pause time in milliseconds after addition (default 10ms).
648
+ * @returns {Promise<void>} Resolves when the device has been added and is ready.
649
+ * @example
650
+ * ```typescript
651
+ * expect(await addDevice(aggregator, device)).toBeTruthy();
652
+ * ```
653
+ */
654
+ export async function addDevice(owner, device, rounds = 3, pause = 10) {
655
+ expect(owner).toBeDefined();
656
+ expect(device).toBeDefined();
657
+ expect(owner.lifecycle.isReady).toBeTruthy();
658
+ expect(owner.construction.status).toBe(Lifecycle.Status.Active);
659
+ expect(owner.lifecycle.isPartsReady).toBeTruthy();
660
+ const rootServerNode = getRootServerNode(owner);
661
+ await flushAllEndpointNumberPersistence(rootServerNode, rounds, pause);
662
+ // istanbul ignore next
663
+ try {
664
+ await owner.add(device);
665
+ }
666
+ catch (error) {
667
+ inspectError(log, `Error adding device ${device.maybeId}.${device.maybeNumber}`, error);
668
+ process.stderr.write(`${er}Error adding device ${device.maybeId}.${device.maybeNumber}${rs}\n`);
669
+ return false;
670
+ }
671
+ await device.construction.ready;
672
+ expect(owner.parts.has(device)).toBeTruthy();
673
+ expect(owner.lifecycle.isPartsReady).toBeTruthy();
674
+ expect(device.lifecycle.isReady).toBeTruthy();
675
+ expect(device.lifecycle.isInstalled).toBeTruthy();
676
+ expect(device.lifecycle.hasId).toBeTruthy();
677
+ expect(device.lifecycle.hasNumber).toBeTruthy();
678
+ expect(device.construction.status).toBe(Lifecycle.Status.Active);
679
+ await flushAllEndpointNumberPersistence(rootServerNode, rounds, pause);
680
+ return true;
681
+ }
682
+ /**
683
+ * Delete a device (endpoint) from a matter server node or an aggregator.
684
+ *
685
+ * @param {ServerNode<ServerNode.RootEndpoint> | Endpoint<AggregatorEndpoint>} owner The server or aggregator to remove the device from.
686
+ * @param {Endpoint} device The device to remove.
687
+ * @param {number} rounds The number of rounds to perform after deletion (default 3).
688
+ * @param {number} pause The pause time in milliseconds after deletion (default 10ms).
689
+ * @returns {Promise<void>} Resolves when the device has been removed and is no longer ready.
690
+ * @example
691
+ * ```typescript
692
+ * expect(await deleteDevice(aggregator, device)).toBeTruthy();
693
+ * ```
694
+ */
695
+ export async function deleteDevice(owner, device, rounds = 3, pause = 10) {
696
+ expect(owner).toBeDefined();
697
+ expect(device).toBeDefined();
698
+ expect(owner.lifecycle.isReady).toBeTruthy();
699
+ expect(owner.construction.status).toBe(Lifecycle.Status.Active);
700
+ expect(owner.lifecycle.isPartsReady).toBeTruthy();
701
+ const rootServerNode = getRootServerNode(owner);
702
+ await flushAllEndpointNumberPersistence(rootServerNode, rounds, pause);
703
+ // istanbul ignore next
704
+ try {
705
+ await device.delete();
706
+ }
707
+ catch (error) {
708
+ inspectError(log, `Error deleting device ${device.maybeId}.${device.maybeNumber}`, error);
709
+ process.stderr.write(`${er}Error deleting device ${device.maybeId}.${device.maybeNumber}${rs}\n`);
710
+ return false;
711
+ }
712
+ expect(owner.parts.has(device)).toBeFalsy();
713
+ expect(owner.lifecycle.isPartsReady).toBeTruthy();
714
+ expect(device.lifecycle.isReady).toBeFalsy();
715
+ expect(device.lifecycle.isInstalled).toBeFalsy();
716
+ expect(device.lifecycle.hasId).toBeTruthy();
717
+ expect(device.lifecycle.hasNumber).toBeTruthy();
718
+ expect(device.construction.status).toBe(Lifecycle.Status.Destroyed);
719
+ await flushAllEndpointNumberPersistence(rootServerNode, rounds, pause);
720
+ return true;
721
+ }
722
+ //# sourceMappingURL=matterTest.js.map