1.5.2: send() never rejects (status, error, scene, change report: resolves the topic or "" and reports the failure through the bridge's error event when listened to - 1.5.1 rejected, and an un-caught .send() killed the host on any broker hiccup); types ship (declaration: true, dist/*.d.ts tracked; addSupportedModes takes {value, modeResources}, ActionMapping payload optional, addHealthProp accepts EndpointHealth or the string); disconnect()/connect() re-binds devices to the new client; registerDevice returns the existing device on a duplicate endpointId (warning via the log hook, console.warn without one); ThermostatController discovery lists targetSetpoint and drops adaptiveRecoveryStatus; the UNSUPORTED INTERFACE TYPE stderr spam goes through the log hook. Examples: require("alex2node"), an error listener in each, EndpointHealth.OK, neutral endpoint ids, BlindControl reads correlationToken from the header, the thermostat reports Fahrenheit as Fahrenheit, ExamplePowerController is power-only again + new ExamplePowerControllerWithBrightness. readme (install from Forgejo, 1.5.2 changelog, table syntax), LICENSE (MIT); tests for the non-rejecting send, reconnect, duplicate, thermostat discovery and the shipped declaration signatures (8/8 on an in-process broker).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
717af632de
commit
275f00f8a7
35 changed files with 1011 additions and 176 deletions
|
|
@ -8,7 +8,9 @@ const assert = require("node:assert/strict");
|
|||
const net = require("node:net");
|
||||
const aedes = require("aedes");
|
||||
const mqtt = require("mqtt");
|
||||
const { Alex2MQTT, AlexaInterfaceType, DisplayCategory, PowerController, EndpointHealth, TemperatureSensorScale, DEFAULT_HOST } = require("../dist/index.js");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { Alex2MQTT, AlexaInterfaceType, DisplayCategory, PowerController, EndpointHealth, TemperatureSensorScale, DEFAULT_HOST, ActionMapping, AlexaActions, AlexaStatusMessage } = require("../dist/index.js");
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const until = async (fn, ms = 3000, what = "condition") => { const t0 = Date.now(); while (Date.now() - t0 < ms) { if (await fn()) return true; await sleep(15); } throw new Error(`timeout waiting for ${what}`); };
|
||||
|
|
@ -107,3 +109,86 @@ test("a dead broker: 'offline'/'error' events when listened to, and NO crash whe
|
|||
test("registerDevice before connect throws (unchanged from 1.4.0)", () => {
|
||||
assert.throws(() => new Alex2MQTT("u", "p", "root").registerDevice("x", "1", null), /connect before/);
|
||||
});
|
||||
|
||||
// 1.5.2
|
||||
const connected = (bridge) => { const p = new Promise((r) => bridge.once("connect", r)); bridge.connect(); return p; }; // resolves once subscribed to <root>/#
|
||||
const turnOn = (alexa, id, ct) => alexa.publish(`root/${id}/alexaDirective`, { header: { namespace: "Alexa.PowerController", name: "TurnOn", correlationToken: ct, payloadVersion: "3", messageId: ct }, endpoint: { endpointId: id }, payload: {} });
|
||||
const answered = (alexa, id, ct) => alexa.got.some((m) => m.topic === `root/${id}/alexaResponce` && m.payload.event.header.correlationToken === ct);
|
||||
|
||||
test("disconnect() then connect(): devices registered before still answer (1.5.1 left them on the closed client)", async () => {
|
||||
const b = await broker();
|
||||
const alexa = await watcher(b.url, "root/#");
|
||||
const bridge = new Alex2MQTT("u", "p", "root", false, { host: b.url });
|
||||
await connected(bridge);
|
||||
cleanups.push(() => bridge.disconnect());
|
||||
const lamp = bridge.registerDevice("Lamp", "lamp-1", DisplayCategory.LIGHT);
|
||||
lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
|
||||
lamp.on("Event", (d) => { lamp.getStatusMessage(d.header.correlationToken, true).addPowerControllerProp(PowerController.ON).send(); });
|
||||
turnOn(alexa, "lamp-1", "before");
|
||||
await until(() => answered(alexa, "lamp-1", "before"), 3000, "response before disconnect");
|
||||
await bridge.disconnect(); assert.equal(bridge.connected, false);
|
||||
await connected(bridge);
|
||||
assert.equal(bridge.getDevices().length, 1, "the devices stay registered");
|
||||
turnOn(alexa, "lamp-1", "after");
|
||||
await until(() => answered(alexa, "lamp-1", "after"), 3000, "response after disconnect() + connect()");
|
||||
});
|
||||
|
||||
test("registerDevice with an endpointId already registered returns the existing device and warns; thermostat discovery lists targetSetpoint", async () => {
|
||||
const b = await broker();
|
||||
const alexa = await watcher(b.url, "root/#");
|
||||
const logs = [];
|
||||
const bridge = new Alex2MQTT("u", "p", "root", false, { host: b.url, log: (m) => logs.push(m) });
|
||||
await connected(bridge);
|
||||
cleanups.push(() => bridge.disconnect());
|
||||
const a = bridge.registerDevice("Lamp A", "same", DisplayCategory.LIGHT); a.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
|
||||
const again = bridge.registerDevice("Lamp B", "same", DisplayCategory.SWITCH);
|
||||
assert.equal(again, a); 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");
|
||||
// without a log hook (the examples and the Quick Start set none) the warning falls back to console.warn
|
||||
const quiet = new Alex2MQTT("u", "p", "root-quiet", false, { host: b.url }); quiet.connect(); cleanups.push(() => quiet.disconnect());
|
||||
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");
|
||||
bridge.registerDevice("Thermostat", "thermo-1", DisplayCategory.THERMOSTAT).addCapability(AlexaInterfaceType.THERMOSTAT_CONTROLLER);
|
||||
alexa.publish("root/discover", {});
|
||||
await until(() => alexa.got.some((m) => m.topic === "root/discover_r"), 3000, "discovery");
|
||||
const disc = alexa.got.find((m) => m.topic === "root/discover_r").payload;
|
||||
assert.deepEqual(disc.map((d) => d.endpointId), ["same", "thermo-1"]);
|
||||
const thermo = disc[1].capabilities.find((c) => c.interface === "Alexa.ThermostatController");
|
||||
assert.deepEqual(thermo.properties.supported.map((p) => p.name), ["targetSetpoint", "lowerSetpoint", "upperSetpoint", "thermostatMode"]);
|
||||
assert.equal(thermo.version, "3.2");
|
||||
});
|
||||
|
||||
test("1.5.2 typings: dist ships declarations, ActionMapping's payload is optional, addHealthProp takes the enum or the string", () => {
|
||||
for (const f of ["index", "Alex2Node", "Device", "AlexaInterface", "AlexaStatusMessage", "AlexaErrorResponse", "ActionMapping", "DisplayCategory"]) assert.ok(fs.existsSync(path.join(__dirname, "..", "dist", `${f}.d.ts`)), `dist/${f}.d.ts`);
|
||||
// the declarations themselves (N-15 / NX-04 / N-04): a revert in src/ would rebuild narrower types and the runtime checks below would not notice
|
||||
const dts = (f) => fs.readFileSync(path.join(__dirname, "..", "dist", `${f}.d.ts`), "utf8");
|
||||
assert.match(dts("ActionMapping"), /constructor\(actions: AlexaActions\[\], directiveName: string, directivePayload\?: string\);/);
|
||||
assert.match(dts("AlexaStatusMessage"), /addHealthProp\(health: EndpointHealth \| `\$\{EndpointHealth\}`, uncertaintyInMs\?: number\): this;/);
|
||||
assert.match(dts("AlexaInterface"), /addSupportedModes\(modes: Array<string \| SupportedMode>\): void;/);
|
||||
assert.match(dts("index"), /export type \{ SupportedMode \} from "\.\/AlexaInterface";/);
|
||||
assert.deepEqual(new ActionMapping([AlexaActions.Close], "TurnOn").toJSON(), { "@type": "ActionsToDirective", actions: ["Alexa.Actions.Close"], directive: { name: "TurnOn" } });
|
||||
const health = (h) => new AlexaStatusMessage("ct", "root", "e", null).addHealthProp(h).toJSON().context.properties[0].value;
|
||||
assert.deepEqual(health(EndpointHealth.OK), { value: "OK" }); assert.deepEqual(health("UNREACHABLE"), { value: "UNREACHABLE" });
|
||||
});
|
||||
|
||||
test("1.5.2: a failed publish never rejects send() - it resolves \"\" and reaches the bridge's error listener", async () => {
|
||||
const b = await broker();
|
||||
const bridge = new Alex2MQTT("u", "p", "root-send", false, { host: b.url });
|
||||
const errors = []; bridge.on("error", (e) => errors.push(e.message));
|
||||
bridge.connect(); cleanups.push(() => bridge.disconnect());
|
||||
await new Promise((r) => bridge.once("connect", r));
|
||||
const lamp = bridge.registerDevice("Lamp", "lamp-send", null); lamp.addCapability(AlexaInterfaceType.POWER_CONTROLLER);
|
||||
await bridge.disconnect(); // the device still holds the ended client until the next connect(): every publish now fails
|
||||
const status = await lamp.getStatusMessage("ct", true).addPowerControllerProp(PowerController.ON).send();
|
||||
const error = await lamp.getErrorMessage("ct").send();
|
||||
const scene = await lamp.sendSceneResponse("ct", true);
|
||||
const change = await lamp.getChangeReport("PHYSICAL_INTERACTION").addPowerControllerProp(PowerController.OFF).send();
|
||||
assert.deepEqual([status, error, scene, change], ["", "", "", ""], "each resolves \"\" instead of rejecting");
|
||||
assert.ok(errors.length >= 4, `every failure reached the error listener: ${errors.join(" | ")}`);
|
||||
// and with NO listener the same failures are swallowed (an "error" event without a listener would throw)
|
||||
const quiet = new Alex2MQTT("u", "p", "root-send-quiet", false, { host: b.url }); quiet.connect(); cleanups.push(() => quiet.disconnect());
|
||||
await new Promise((r) => quiet.once("connect", r));
|
||||
const q = quiet.registerDevice("Q", "q-1", null); await quiet.disconnect();
|
||||
assert.equal(await q.getStatusMessage("ct", true).addPowerControllerProp(PowerController.ON).send(), "");
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue