Alex2Node/test/discovery.test.js
David 58869d44c1 test: split the 1.5.2 suite into a harness and per-topic files
test/helpers/harness.js holds the aedes broker, the Alexa-side watcher, until() and the cleanups, plus directive(),
connected() and setup(), which the old file spelled out in each test. test/alex2node.test.js (194 lines, 8 tests)
becomes connection, discovery, directives, change-report and typings.

The 50-line round-trip test is cut at its topic boundaries and the duplicate-endpointId test no longer carries the
thermostat discovery check, so a failure names a case: 8 tests become 15, every 1.5.2 assertion kept. Four checks
are stricter: the "directive" event is compared whole, the StateReport is checked for its token and properties, the
Celsius conversion covers lowerSetpoint as well as temperature, and a duplicate endpointId is listed once.

No change to src/ or dist/. npm test: 15 pass in 0.9 s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 13:45:38 +00:00

104 lines
5.2 KiB
JavaScript

"use strict";
// Discovery: a message on <root>/discover is answered on <root>/discover_r with one JSON array, an endpoint object
// per registered device.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const { Alex2MQTT, AlexaInterfaceType, DisplayCategory } = require("../dist/index.js");
const { setup, connected, until } = require("./helpers/harness.js");
// Ask as Alex2MQTT does and return the endpoints of the one answer.
async function discover(alexa, root) {
alexa.publish(`${root}/discover`, { namespace: "Alexa.Discovery", name: "Discover" });
await until(() => alexa.on(`${root}/discover_r`).length === 1, 3000, "discovery reply");
return alexa.on(`${root}/discover_r`)[0];
}
test("discovery answers with every registered device and its capabilities", async () => {
const { alexa, bridge } = await setup(Alex2MQTT, "root");
const discovered = [];
bridge.on("discover", (n) => discovered.push(n));
const lamp = bridge.registerDevice("Dining Room Light", "5020AA", DisplayCategory.LIGHT);
lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER, { proactivelyReported: true });
lamp.addCapability(AlexaInterfaceType.BRIGHTNESS_CONTROLLER);
const scene = bridge.registerDevice("Evening", "scene-1", DisplayCategory.SCENE_TRIGGER);
scene.addCapability(AlexaInterfaceType.SCENE_CONTROLLER);
const endpoints = await discover(alexa, "root");
assert.equal(endpoints.length, 2);
assert.equal(endpoints[0].friendlyName, "Dining Room Light");
assert.deepEqual(endpoints[0].displayCategories, ["LIGHT"]);
const power = endpoints[0].capabilities.find((c) => c.interface === "Alexa.PowerController");
assert.equal(power.properties.proactivelyReported, true);
assert.deepEqual(power.properties.supported, [{ name: "powerState" }]);
// Alexa.SceneController has no properties block
const activation = endpoints[1].capabilities.find((c) => c.interface === "Alexa.SceneController");
assert.equal(activation.supportsDeactivation, true);
assert.equal(activation.version, "3");
assert.equal(activation.properties, undefined);
await until(() => discovered.length === 1, 3000, "the discover event");
assert.deepEqual(discovered, [2]);
assert.notEqual(bridge.lastDiscoveryAt, null);
});
test("unregisterDevice: the device is gone from the next discovery answer", async () => {
const { alexa, bridge } = await setup(Alex2MQTT, "root");
const lamp = bridge.registerDevice("Dining Room Light", "5020AA", DisplayCategory.LIGHT);
lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
const scene = bridge.registerDevice("Evening", "scene-1", DisplayCategory.SCENE_TRIGGER);
scene.addCapability(AlexaInterfaceType.SCENE_CONTROLLER);
assert.equal(bridge.unregisterDevice("scene-1"), true);
assert.equal(bridge.unregisterDevice("scene-1"), false);
assert.equal(bridge.getDevices().length, 1);
const endpoints = await discover(alexa, "root");
assert.deepEqual(endpoints.map((e) => e.endpointId), ["5020AA"]);
});
test("registerDevice with an endpointId already registered returns the existing device and warns", async () => {
const logs = [];
const { url, alexa, bridge } = await setup(Alex2MQTT, "root", { log: (m) => logs.push(m) });
const first = bridge.registerDevice("Lamp A", "same", DisplayCategory.LIGHT);
first.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
const again = bridge.registerDevice("Lamp B", "same", DisplayCategory.SWITCH);
assert.equal(again, first);
assert.equal(again.name, "Lamp A");
assert.equal(bridge.getDevices().length, 1);
assert.ok(logs.some((m) => /warning: registerDevice\("same"\)/.test(m)), "warned through the log hook");
const endpoints = await discover(alexa, "root");
assert.deepEqual(endpoints.map((e) => e.endpointId), ["same"]);
// Without a log hook (the examples and the Quick Start set none) the warning falls back to console.warn.
const quiet = await connected(new Alex2MQTT("u", "p", "root-quiet", false, { host: url }));
const warned = [];
const realWarn = console.warn;
console.warn = (...args) => warned.push(args.join(" "));
try {
quiet.registerDevice("Q", "same", null);
assert.equal(quiet.registerDevice("Q again", "same", null).name, "Q");
} finally {
console.warn = realWarn;
}
assert.ok(warned.some((m) => /warning: registerDevice\("same"\)/.test(m)), "console.warn without a log hook");
});
test("ThermostatController discovery: version 3.2, targetSetpoint listed with both setpoints and the mode", async () => {
const { alexa, bridge } = await setup(Alex2MQTT, "root");
const lamp = bridge.registerDevice("Lamp", "lamp-1", DisplayCategory.LIGHT);
lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
const thermostat = bridge.registerDevice("Thermostat", "thermo-1", DisplayCategory.THERMOSTAT);
thermostat.addCapability(AlexaInterfaceType.THERMOSTAT_CONTROLLER);
const endpoints = await discover(alexa, "root");
assert.deepEqual(endpoints.map((e) => e.endpointId), ["lamp-1", "thermo-1"]);
const capability = endpoints[1].capabilities.find((c) => c.interface === "Alexa.ThermostatController");
assert.equal(capability.version, "3.2");
assert.deepEqual(
capability.properties.supported.map((p) => p.name),
["targetSetpoint", "lowerSetpoint", "upperSetpoint", "thermostatMode"]
);
});