Alex2Node/test/device/raise.test.js
David 3a07c861b1 registry: SceneController events, DoorbellEventSource, SimpleEventSource, TimeHoldController, InventoryLevelSensor, WakeOnLANController
Six descriptors written from their pages replace the last stubs of tiers 1 and 2. A scene answers Activate and
Deactivate through ctx.respond() with ActivationStarted and DeactivationStarted, the time and the cause filled
in. device.raise(descriptor, name, payload) publishes DoorbellPress and the Event of a button on <root>/event
with the endpoint and a new messageId; it throws a MessageError for an interface or instance the device did
not declare, an event that answers a directive, a payload that does not fit and a message over 16000 bytes.
TurnOn of a device with WakeOnLANController is deferred without the warning.

On the wire: a doorbell has no properties object and proactivelyReported on the capability; SimpleEventSource
is version 1.0, InventoryLevelSensor and WakeOnLANController version 3 (1.5.2: 1). A scene declared without
options is announced as before. Alex2MQTT has no topic yet for the WakeUp event.
24 examples of the six pages are saved as fixtures. 267 tests pass, 241 before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 21:01:53 +00:00

145 lines
7.9 KiB
JavaScript

"use strict";
// device.raise(): an event nobody asked for goes to <root>/event, and one that would not arrive is refused.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const {
Alex2MQTT, AlexaInterfaceType, DoorbellEventSource, MemoryPublisher, MessageError, PowerController, SceneController,
SimpleEventSource, asset, topics,
} = require("alex2node");
const { endpoint } = require("../helpers/endpoint.js");
const { doc } = require("../helpers/fixtures.js");
const { UUID_V4 } = require("../helpers/harness.js");
function doorbell() {
const sent = new MemoryPublisher();
const errors = [];
const bridge = new Alex2MQTT("u", "p", "root", false, { publisher: sent, log: () => {} });
bridge.on("error", (err) => errors.push(err));
const device = bridge.addDevice({ endpointId: "door-1", name: "Front door", categories: ["DOORBELL"] });
device.add(DoorbellEventSource);
return { bridge, device, sent, errors };
}
const button = (name, events) => ({
instance: `Remote.${name}`,
friendlyNames: [asset(`Alexa.Button.${name}`)],
supportedEvents: events.map((push) => ({ id: `Button.${push}.1`, friendlyNames: [asset(`Alexa.Button.${push}`)] })),
});
const refused = (raise, problem) => assert.throws(raise, (err) => err instanceof MessageError && err.problem === problem);
test("raise() publishes the event on <root>/event, with the endpoint, a new messageId, the time and the usual cause", async () => {
const { device, sent } = doorbell();
const printed = doc("alexa-doorbelleventsource", "DoorbellPress.event").event;
const before = Date.now();
assert.deepEqual(await device.raise(DoorbellEventSource, "DoorbellPress"), { ok: true, topic: "root/event" });
await device.raise(DoorbellEventSource, "DoorbellPress", printed.payload);
await device.raise(DoorbellEventSource, "DoorbellPress", { cause: { type: "APP_INTERACTION" } }, { messageId: "press-3" });
assert.equal(topics.event("root"), "root/event");
assert.deepEqual(sent.published.map(({ topic }) => topic), ["root/event", "root/event", "root/event"]);
const [first, second, third] = sent.published.map(({ message }) => message.event);
for (const event of [first, second, third]) {
assert.deepEqual(event.endpoint, { endpointId: "door-1" });
const { messageId, ...header } = event.header;
assert.deepEqual(header, { namespace: printed.header.namespace, name: printed.header.name, payloadVersion: printed.header.payloadVersion });
}
assert.match(first.header.messageId, UUID_V4);
assert.match(second.header.messageId, UUID_V4);
assert.notEqual(first.header.messageId, second.header.messageId);
assert.equal(third.header.messageId, "press-3");
assert.deepEqual(first.payload.cause, { type: "PHYSICAL_INTERACTION" });
assert.ok(Date.parse(first.payload.timestamp) >= before && Date.parse(first.payload.timestamp) <= Date.now(), first.payload.timestamp);
assert.deepEqual(second.payload, printed.payload);
assert.deepEqual(third.payload.cause, { type: "APP_INTERACTION" });
});
test("raise() for an instance: the Event of a button has the instance in its header and the version of the interface", async () => {
const sent = new MemoryPublisher();
const remote = endpoint("remote-1", "Remote", ["REMOTE"]);
remote.publisher = sent;
const top = remote.add(SimpleEventSource, button("TopButton", ["SinglePush", "DoublePush"]));
remote.add(SimpleEventSource, button("BottomButton", ["SinglePush"]));
const printed = doc("alexa-simpleeventsource", "Event.event").event;
await remote.raise(SimpleEventSource, "Event", printed.payload, { instance: top.instance, messageId: "push-1" });
assert.deepEqual(sent.published, [{
topic: "root/event",
message: {
event: {
header: { namespace: "Alexa.SimpleEventSource", name: "Event", instance: "Remote.TopButton", messageId: "push-1", payloadVersion: "1.0" },
endpoint: { endpointId: "remote-1" },
payload: printed.payload,
},
},
}]);
assert.equal(printed.header.payloadVersion, "1.0");
const raised = "remote-1: Alexa.SimpleEventSource.Event was not raised: ";
assert.throws(() => remote.raise(SimpleEventSource, "Event", { id: "Button.SinglePush.1" }), {
name: "MessageError",
message: `${raised}the device did not declare Alexa.SimpleEventSource. Declare it with add() first, and pass the instance that raises the event`,
});
assert.throws(() => remote.raise(SimpleEventSource, "Event", { id: "Button.SinglePush.1" }, { instance: "Remote.LeftButton" }), {
message: `${raised}the device did not declare the instance "Remote.LeftButton" of Alexa.SimpleEventSource. Declare it with add() first`,
});
assert.throws(() => remote.raise(SimpleEventSource, "Event", {}, { instance: top.instance }), {
message: `${raised}payload.id: expected a string of 1 or more characters, got nothing`,
});
assert.equal(sent.published.length, 1);
});
test("raise() refuses an event the device has no capability for, and one that would not arrive", () => {
const { device, sent } = doorbell();
device.add(PowerController);
const scene = endpoint("scene-1", "Evening", ["SCENE_TRIGGER"]);
scene.setDescription("Evening scene");
scene.publisher = sent;
scene.add(SceneController);
const was = (name) => `${name} was not raised: `;
refused(() => scene.raise(DoorbellEventSource, "DoorbellPress"), `${was("Alexa.DoorbellEventSource.DoorbellPress")}the device did not declare Alexa.DoorbellEventSource. Declare it with add() first`);
refused(() => device.raise(DoorbellEventSource, "Knock"), `${was("Alexa.DoorbellEventSource.Knock")}it is not an event of the interface, which has DoorbellPress`);
refused(() => device.raise(PowerController, "TurnedOn"), `${was("Alexa.PowerController.TurnedOn")}it is not an event of the interface, which has none`);
refused(
() => scene.raise(SceneController, "ActivationStarted"),
`${was("Alexa.SceneController.ActivationStarted")}it answers a directive. Send it with respond() in the handler of the directive`
);
refused(
() => device.raise(DoorbellEventSource, "DoorbellPress", { timestamp: "just now" }),
`${was("Alexa.DoorbellEventSource.DoorbellPress")}payload.timestamp: expected a UTC time like 2017-08-30T01:18:21Z, got "just now"`
);
refused(
() => device.raise(DoorbellEventSource, "DoorbellPress", { cause: { type: "KNOCK" } }),
`${was("Alexa.DoorbellEventSource.DoorbellPress")}payload.cause.type: expected APP_INTERACTION | PERIODIC_POLL | PHYSICAL_INTERACTION | RULE_TRIGGER | VOICE_INTERACTION, got "KNOCK"`
);
refused(
() => device.raise(DoorbellEventSource, "DoorbellPress", { picture: "x".repeat(topics.EVENT_BYTES) }),
`${was("Alexa.DoorbellEventSource.DoorbellPress")}it is 16296 bytes as JSON, Alex2MQTT takes 16000`
);
assert.deepEqual(sent.published, []);
});
test("raise() of a doorbell declared the 1.x way, of a device on no bridge and with a broker that is gone", async () => {
const sent = new MemoryPublisher();
const bell = endpoint("door-2", "Back door", ["DOORBELL"]);
bell.addCapability(AlexaInterfaceType.DOORBELL_EVENT_SOURCE);
const reported = [];
bell.onPublishError = (err) => reported.push(err.message);
const nowhere = await bell.raise(DoorbellEventSource, "DoorbellPress");
assert.equal(nowhere.ok, false);
assert.deepEqual(reported, ["nothing was published to root/event: the device is on no bridge, register it with addDevice() or registerDevice()"]);
bell.publisher = sent;
assert.equal((await bell.raise(DoorbellEventSource, "DoorbellPress")).ok, true);
assert.equal(sent.published[0].message.event.endpoint.endpointId, "door-2");
const { device, sent: broker, errors } = doorbell();
broker.failWith = new Error("the broker is gone");
const result = await device.raise(DoorbellEventSource, "DoorbellPress");
assert.deepEqual([result.ok, result.topic, result.error.message], [false, "root/event", "the broker is gone"]);
assert.deepEqual(errors.map((err) => err.message), ["the broker is gone"]);
});