Alex2Node/test/dispatch/dispatch.test.js
David feff828ec1 dispatch: typed handlers, respond/defer/error, automatic ErrorResponse, watchdog
src/dispatcher.ts routes a directive to capability.on(name | "*"), device.onDirective() or
device.onReportState(), with the payload checked by the descriptor of the interface. The
DirectiveContext answers with respond/report/defer/error; respond() and report() start from
device.state(). A handler or 1.x listener that throws or rejects is answered with INTERNAL_ERROR
(an AlexaError with itself) and reported through "error", never as an unhandled rejection.
Undeclared interface, unknown directive, AdjustMode on an unordered mode and a missing handler
get INVALID_DIRECTIVE, a bad payload INVALID_VALUE; a device with an "Event" or "ReportState"
listener keeps the answer to itself. Every answer passes the dispatcher: the first one per
correlationToken is published, a second is refused. No answer within answerWithinMs (6500,
0 = off, unref'd timer) sends INTERNAL_ERROR and emits "unanswered". A messageId that arrived
in the last 60 s is dropped. New: "unknownEndpoint", options answerUnknownEndpoints, publisher,
timers, and bridge.receive() to run a bridge without a broker. 168 tests pass (18 new).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-28 19:49:41 +00:00

442 lines
24 KiB
JavaScript

"use strict";
// Dispatch without a broker: a bridge that publishes to a MemoryPublisher and is given its messages by receive().
// The watchdog runs on timers the test fires by hand.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const {
Alex2MQTT, AlexaErrors, AlexaInterfaceType, DeclarationError, MemoryPublisher, ModeController, PowerController,
PowerState, RangeController, BrightnessController, asset, text,
} = require("alex2node");
const { directive } = require("../helpers/harness.js");
// Timers that wait for fire()
function manualTimers() {
const pending = [];
return {
pending,
setTimeout(run, ms) {
const timer = { run, ms };
pending.push(timer);
return timer;
},
clearTimeout(timer) {
const i = pending.indexOf(timer);
if (i !== -1) pending.splice(i, 1);
},
fire() {
for (const timer of pending.splice(0)) timer.run();
},
};
}
function bridgeWithout(options = {}) {
const sent = new MemoryPublisher();
const timers = manualTimers();
const logged = [];
const bridge = new Alex2MQTT("u", "p", "root", false, { publisher: sent, timers, log: (line) => logged.push(line), ...options });
const errors = [];
bridge.on("error", (err) => errors.push(err));
/** What was published, as [the last part of the topic, the name of the event, its payload]. */
const answers = () => sent.published.map(({ topic, message }) => [topic.split("/").pop(), message.event.header.name, message.event.payload]);
const receive = (message) => bridge.receive(`root/${message.endpoint.endpointId}/alexaDirective`, JSON.stringify(message));
return { bridge, sent, timers, logged, errors, answers, receive };
}
const instanced = (message, instance) => ({ ...message, header: { ...message.header, instance } });
const nextTurn = () => new Promise((resolve) => setImmediate(resolve));
// Blinds with a lift from 0 to 100 and a tilt in three unordered modes
function blinds(bridge) {
const device = bridge.addDevice({ endpointId: "blinds-1", name: "Blinds", categories: ["INTERIOR_BLIND"] });
const lift = device.add(RangeController, {
instance: "Blind.Lift", friendlyNames: [asset("Alexa.Setting.Opening")], range: { min: 0, max: 100, precision: 1 },
});
const mode = (value) => ({ value, friendlyNames: [text(value, "en-US")] });
const tilt = device.add(ModeController, {
instance: "Blind.Tilt", friendlyNames: [text("tilt", "en-US")], supportedModes: [mode("Up"), mode("Flat"), mode("Down")],
});
return { device, lift, tilt };
}
test("a directive reaches the handler of its capability with a checked payload, and respond() answers with the state of the device under what the handler set", async () => {
const { bridge, sent, timers, receive } = bridgeWithout();
const { device, lift, tilt } = blinds(bridge);
device.state((s) => s.set(lift, "rangeValue", 10).set(tilt, "mode", "Flat").health("OK"));
const seen = [];
lift.on("SetRangeValue", (ctx) => {
seen.push([ctx.endpointId, ctx.namespace, ctx.name, ctx.instance, ctx.correlationToken, ctx.payload, ctx.answered]);
return ctx.respond((s) => s.set(lift, "rangeValue", ctx.payload.rangeValue));
});
await receive(instanced(directive("Alexa.RangeController", "SetRangeValue", "blinds-1", "ct-1", { rangeValue: 7 }), "Blind.Lift"));
assert.deepEqual(seen, [["blinds-1", "Alexa.RangeController", "SetRangeValue", "Blind.Lift", "ct-1", { rangeValue: 7 }, false]]);
assert.equal(sent.published.length, 1);
const [{ topic, message }] = sent.published;
assert.equal(topic, "root/blinds-1/alexaResponce");
assert.deepEqual([message.event.header.namespace, message.event.header.name, message.event.header.correlationToken], ["Alexa", "Response", "ct-1"]);
assert.deepEqual(message.context.properties.map((p) => [p.name, p.instance, p.value]), [
["mode", "Blind.Tilt", "Flat"],
["connectivity", undefined, { value: "OK" }],
["rangeValue", "Blind.Lift", 7],
]);
assert.deepEqual(timers.pending, [], "an answered directive leaves no timer");
});
test("a directive no handler can be called for is answered with INVALID_DIRECTIVE", async () => {
const { bridge, answers, receive, errors } = bridgeWithout();
const { lift, tilt } = blinds(bridge);
lift.on("SetRangeValue", (ctx) => ctx.respond());
tilt.on("*", (ctx) => ctx.respond());
const range = (name, payload) => directive("Alexa.RangeController", name, "blinds-1", `ct-${name}`, payload);
await receive(directive("Alexa.PowerController", "TurnOn", "blinds-1", "ct-undeclared"));
await receive(instanced(range("SetRangeValue", { rangeValue: 7 }), "Blind.Height"));
await receive(instanced(range("SetRange", { rangeValue: 7 }), "Blind.Lift"));
await receive(instanced(range("AdjustRangeValue", { rangeValueDelta: 5, rangeValueDeltaDefault: false }), "Blind.Lift"));
await receive(instanced(directive("Alexa.ModeController", "AdjustMode", "blinds-1", "ct-unordered", { modeDelta: 1 }), "Blind.Tilt"));
assert.deepEqual(answers(), [
["alexaResponce", "ErrorResponse", { type: "INVALID_DIRECTIVE", message: "Alexa.PowerController is not declared on blinds-1" }],
["alexaResponce", "ErrorResponse", { type: "INVALID_DIRECTIVE", message: "Alexa.RangeController \"Blind.Height\" is not declared on blinds-1" }],
["alexaResponce", "ErrorResponse", { type: "INVALID_DIRECTIVE", message: "SetRange is not a directive of Alexa.RangeController" }],
["alexaResponce", "ErrorResponse", {
type: "INVALID_DIRECTIVE",
message: "blinds-1 has no handler for Alexa.RangeController.AdjustRangeValue: register one with on(\"AdjustRangeValue\", handler) of the capability",
}],
["alexaResponce", "ErrorResponse", { type: "INVALID_DIRECTIVE", message: "Alexa.ModeController \"Blind.Tilt\" as blinds-1 declares it does not take AdjustMode" }],
]);
assert.deepEqual(errors, [], "the sender was wrong, not the bridge");
});
test("a payload the interface does not describe is answered with INVALID_VALUE and the handler is not called", async () => {
const { bridge, answers, receive } = bridgeWithout();
const lamp = bridge.addDevice({ endpointId: "lamp-1", name: "Lamp", categories: ["LIGHT"] });
let called = 0;
lamp.add(BrightnessController).on("SetBrightness", () => { called += 1; });
await receive(directive("Alexa.BrightnessController", "SetBrightness", "lamp-1", "ct-1", { brightness: "lots" }));
assert.equal(called, 0);
const [[, name, payload]] = answers();
assert.equal(name, "ErrorResponse");
assert.equal(payload.type, "INVALID_VALUE");
assert.match(payload.message, /^payload\.brightness: expected .*got "lots"$/);
});
test("on(): \"*\" and onDirective() take what has no handler of its own; a name that is no directive is refused", async () => {
const { bridge, receive } = bridgeWithout();
const lamp = bridge.addDevice({ endpointId: "lamp-1", name: "Lamp", categories: ["LIGHT"] });
const power = lamp.add(PowerController);
lamp.add(BrightnessController);
const called = [];
const by = (who) => (ctx) => { called.push(`${who} ${ctx.name}`); return ctx.respond(); };
power.on("TurnOn", by("TurnOn")).on("*", by("*"));
lamp.onDirective(by("device"));
await receive(directive("Alexa.PowerController", "TurnOn", "lamp-1", "ct-1"));
await receive(directive("Alexa.PowerController", "TurnOff", "lamp-1", "ct-2"));
await receive(directive("Alexa.BrightnessController", "SetBrightness", "lamp-1", "ct-3", { brightness: 40 }));
assert.deepEqual(called, ["TurnOn TurnOn", "* TurnOff", "device SetBrightness"]);
assert.throws(() => power.on("Toggle", by("Toggle")), (err) => err instanceof DeclarationError
&& err.message === "lamp-1: Alexa.PowerController: Toggle is not a directive of the interface, which has TurnOn, TurnOff");
});
test("a handler that throws or rejects answers INTERNAL_ERROR, the bridge reports what it threw, and nothing is left unhandled", async () => {
const unhandled = [];
const note = (reason) => unhandled.push(reason);
process.on("unhandledRejection", note);
const { bridge, answers, receive, errors } = bridgeWithout();
const lamp = bridge.addDevice({ endpointId: "lamp-1", name: "Lamp", categories: ["LIGHT"] });
const power = lamp.add(PowerController);
power.on("TurnOn", () => { throw new TypeError("relay is undefined"); });
power.on("TurnOff", async () => { await nextTurn(); throw new Error("the relay did not answer"); });
await receive(directive("Alexa.PowerController", "TurnOn", "lamp-1", "ct-1"));
await receive(directive("Alexa.PowerController", "TurnOff", "lamp-1", "ct-2"));
await nextTurn();
process.off("unhandledRejection", note);
assert.deepEqual(answers(), [
["alexaResponce", "ErrorResponse", { type: "INTERNAL_ERROR", message: "relay is undefined" }],
["alexaResponce", "ErrorResponse", { type: "INTERNAL_ERROR", message: "the relay did not answer" }],
]);
assert.ok(errors[0] instanceof TypeError, "the error as it was thrown");
assert.deepEqual(errors.map((err) => err.message), ["relay is undefined", "the relay did not answer"]);
assert.deepEqual(unhandled, []);
});
test("an AlexaError a handler throws is the answer, under the namespace of its type; ctx.error() sends the same", async () => {
const { bridge, sent, receive, errors } = bridgeWithout();
const { lift } = blinds(bridge);
const outOfRange = AlexaErrors.valueOutOfRange("the lift goes from 0 to 100", { minimumValue: 0, maximumValue: 100 });
lift.on("SetRangeValue", (ctx) => {
if (ctx.payload.rangeValue > 100) throw outOfRange;
if (ctx.payload.rangeValue === 50) return ctx.error(AlexaErrors.of("OBSTACLE_DETECTED", "something is in the way"));
return ctx.error("ENDPOINT_UNREACHABLE", "the motor is unplugged", { since: "today" });
});
const set = (rangeValue) => receive(instanced(directive("Alexa.RangeController", "SetRangeValue", "blinds-1", `ct-${rangeValue}`, { rangeValue }), "Blind.Lift"));
await set(101);
await set(50);
await set(1);
assert.deepEqual(sent.published.map(({ message }) => [message.event.header.namespace, message.event.header.correlationToken, message.event.payload]), [
["Alexa", "ct-101", { type: "VALUE_OUT_OF_RANGE", message: "the lift goes from 0 to 100", validRange: { minimumValue: 0, maximumValue: 100 } }],
["Alexa.Safety", "ct-50", { type: "OBSTACLE_DETECTED", message: "something is in the way" }],
["Alexa", "ct-1", { type: "ENDPOINT_UNREACHABLE", message: "the motor is unplugged", since: "today" }],
]);
assert.deepEqual(errors, [], "an error the handler chose is not a fault of the bridge");
});
test("defer(): a DeferredResponse without a context now, the answer on deferredResponse, a second defer() refused", async () => {
const { bridge, sent, answers, receive, timers, logged, errors } = bridgeWithout();
const lamp = bridge.addDevice({ endpointId: "lamp-1", name: "Lamp", categories: ["LIGHT"], endpointHealth: false });
const results = [];
lamp.add(PowerController).on("TurnOn", async (ctx) => {
results.push(await ctx.defer(5), [ctx.deferred, ctx.answered]);
assert.deepEqual(timers.pending, [], "the watchdog waits for the first answer only");
results.push(await ctx.defer());
results.push(await ctx.respond((s) => s.set(PowerController, "powerState", "ON")), [ctx.deferred, ctx.answered]);
});
await receive(directive("Alexa.PowerController", "TurnOn", "lamp-1", "ct-1"));
await receive(directive("Alexa.PowerController", "TurnOn", "lamp-1", "ct-2"));
assert.deepEqual(answers().slice(0, 2), [
["alexaResponce", "DeferredResponse", { estimatedDeferralInSeconds: 5 }],
["deferredResponse", "Response", {}],
]);
assert.equal("context" in sent.published[0].message, false);
assert.equal(sent.published.length, 4);
assert.deepEqual(results.slice(0, 5).map((result) => (Array.isArray(result) ? result : result.ok)), [true, [true, false], false, true, [true, true]]);
assert.equal(
results[2].error.message,
"nothing was published to root/lamp-1/alexaResponce: Alexa.PowerController.TurnOn for lamp-1 was deferred before, and Alex2MQTT takes one answer"
);
assert.deepEqual(errors.map((err) => err.message), [results[2].error.message, results[7].error.message]);
const warnings = logged.filter((line) => line.startsWith("warning: Amazon documents no DeferredResponse"));
assert.deepEqual(warnings, ["warning: Amazon documents no DeferredResponse for Alexa.PowerController, Alexa may not wait for the answer"]);
});
test("the watchdog: a directive nobody answers gets INTERNAL_ERROR and the bridge emits \"unanswered\"; a late answer is refused", async () => {
const { bridge, answers, receive, timers, errors } = bridgeWithout();
const lamp = bridge.addDevice({ endpointId: "lamp-1", name: "Lamp", categories: ["LIGHT"] });
let late;
lamp.add(PowerController).on("TurnOn", (ctx) => { late = ctx; });
const unanswered = [];
bridge.on("unanswered", (info) => unanswered.push(info));
await receive(directive("Alexa.PowerController", "TurnOn", "lamp-1", "ct-1"));
assert.deepEqual(answers(), []);
assert.deepEqual(timers.pending.map((timer) => timer.ms), [6500], "before Alex2MQTT gives up at 7 s");
timers.fire();
await nextTurn();
assert.deepEqual(answers(), [["alexaResponce", "ErrorResponse", { type: "INTERNAL_ERROR", message: "no answer within 6.5 s" }]]);
assert.deepEqual(unanswered, [{ endpointId: "lamp-1", namespace: "Alexa.PowerController", name: "TurnOn", correlationToken: "ct-1" }]);
assert.match(errors[0].message, /^lamp-1: Alexa\.PowerController\.TurnOn got no answer within 6\.5 s/);
const result = await late.respond();
assert.equal(result.ok, false);
assert.match(result.error.message, /was answered before/);
assert.equal(answers().length, 1);
});
test("answerWithinMs: the time of the watchdog, 0 for none, anything else refused", async () => {
const quick = bridgeWithout({ answerWithinMs: 200 });
const never = bridgeWithout({ answerWithinMs: 0 });
for (const { bridge, receive } of [quick, never]) {
bridge.addDevice({ endpointId: "lamp-1", name: "Lamp", categories: ["LIGHT"] }).add(PowerController).on("TurnOn", () => {});
await receive(directive("Alexa.PowerController", "TurnOn", "lamp-1", "ct-1"));
}
assert.deepEqual(quick.timers.pending.map((timer) => timer.ms), [200]);
assert.deepEqual(never.timers.pending, []);
assert.throws(
() => bridgeWithout({ answerWithinMs: -1 }),
{ message: "answerWithinMs is -1: pass the milliseconds a handler has to answer in, or 0 for no limit" }
);
});
test("the timer of the watchdog does not keep the process alive", () => {
const script = `
const { Alex2MQTT, MemoryPublisher, PowerController } = require(${JSON.stringify(path.join(__dirname, "..", ".."))});
const bridge = new Alex2MQTT("u", "p", "root", false, { publisher: new MemoryPublisher(), answerWithinMs: 60000 });
bridge.addDevice({ endpointId: "lamp-1", name: "Lamp", categories: ["LIGHT"] }).add(PowerController).on("TurnOn", () => {});
const header = { namespace: "Alexa.PowerController", name: "TurnOn", messageId: "m-1", correlationToken: "ct-1", payloadVersion: "3" };
bridge.receive("root/lamp-1/alexaDirective", JSON.stringify({ header, endpoint: { endpointId: "lamp-1" }, payload: {} }));
`;
const run = spawnSync(process.execPath, ["-e", script], { encoding: "utf8", timeout: 10000 });
assert.equal(run.status, 0, `the process did not end by itself: ${run.error || run.stderr}`);
});
test("a directive that arrives twice is handled once, by its messageId", async () => {
const { bridge, answers, receive, logged } = bridgeWithout();
const called = [];
for (const endpointId of ["lamp-1", "lamp-2"]) {
const lamp = bridge.addDevice({ endpointId, name: `Lamp ${endpointId.slice(-1)}`, categories: ["LIGHT"] });
lamp.add(PowerController).on("TurnOn", (ctx) => { called.push(endpointId); return ctx.respond(); });
}
const turnOn = directive("Alexa.PowerController", "TurnOn", "lamp-1", "ct-1");
await receive(turnOn);
await receive(turnOn);
// The messageId is new to the other endpoint
await receive({ ...turnOn, endpoint: { endpointId: "lamp-2" } });
assert.deepEqual(called, ["lamp-1", "lamp-2"]);
assert.equal(answers().length, 2);
assert.ok(logged.includes("the directive message-ct-1 for lamp-1 is dropped: it arrived before"));
});
test("a directive for an endpoint the bridge does not have: \"unknownEndpoint\", and NO_SUCH_ENDPOINT only when asked for", async () => {
const silent = bridgeWithout();
const answering = bridgeWithout({ answerUnknownEndpoints: true });
const unknown = [];
silent.bridge.on("unknownEndpoint", (info) => unknown.push(info));
await silent.receive(directive("Alexa.PowerController", "TurnOn", "lamp-9", "ct-1"));
await answering.receive(directive("Alexa.PowerController", "TurnOn", "lamp-9", "ct-1"));
assert.deepEqual(unknown, [{ endpointId: "lamp-9", namespace: "Alexa.PowerController", name: "TurnOn" }]);
assert.deepEqual(silent.answers(), []);
assert.deepEqual(silent.timers.pending, []);
assert.deepEqual(answering.answers(), [
["alexaResponce", "ErrorResponse", { type: "NO_SUCH_ENDPOINT", message: "lamp-9 is not an endpoint of this bridge" }],
]);
});
test("what is not a directive is dropped: nothing published, nothing thrown", async () => {
const { bridge, answers, errors, logged, timers } = bridgeWithout();
bridge.addDevice({ endpointId: "lamp-1", name: "Lamp", categories: ["LIGHT"] });
await bridge.receive("root/lamp-1/alexaDirective", "{ not JSON");
await bridge.receive("root/lamp-1/alexaDirective", "null");
await bridge.receive("root/lamp-1/alexaDirective", JSON.stringify({ payload: {} }));
assert.deepEqual([answers(), errors, timers.pending], [[], [], []]);
assert.deepEqual(logged.filter((line) => line.includes("dropped")), [
"the directive for lamp-1 is dropped: it is not JSON",
"the directive for lamp-1 is dropped: it has no header",
"the directive for lamp-1 is dropped: it has no header",
]);
});
test("ReportState: answered from state() of the device, by onReportState(), or with INVALID_DIRECTIVE when there is neither", async () => {
const { bridge, sent, answers, receive } = bridgeWithout();
const lamp = bridge.addDevice({ endpointId: "lamp-1", name: "Lamp", categories: ["LIGHT"] });
lamp.add(PowerController);
const stateOf = () => sent.published[sent.published.length - 1].message.context.properties.map((p) => [p.name, p.value]);
await receive(directive("Alexa", "ReportState", "lamp-1", "ct-1"));
assert.deepEqual(answers()[0].slice(0, 2), ["alexaResponce", "ErrorResponse"]);
assert.equal(answers()[0][2].type, "INVALID_DIRECTIVE");
lamp.state((s) => s.set(PowerController, "powerState", "OFF").health("OK"));
await receive(directive("Alexa", "ReportState", "lamp-1", "ct-2"));
assert.deepEqual(answers()[1], ["alexaResponce", "StateReport", {}]);
assert.deepEqual(stateOf(), [["powerState", "OFF"], ["connectivity", { value: "OK" }]]);
lamp.onReportState((ctx) => ctx.report((s) => s.health("UNREACHABLE")));
await receive(directive("Alexa", "ReportState", "lamp-1", "ct-3"));
assert.deepEqual(stateOf(), [["powerState", "OFF"], ["connectivity", { value: "UNREACHABLE" }]]);
lamp.capability("Alexa.PowerController").on("TurnOn", (ctx) => ctx.report());
await receive(directive("Alexa.PowerController", "TurnOn", "lamp-1", "ct-4"));
assert.deepEqual(answers()[3][2], {
type: "INTERNAL_ERROR",
message: "report() answers ReportState with a StateReport: answer Alexa.PowerController.TurnOn with respond()",
});
});
// The device of a 1.x caller: declared by name, one listener for every directive, the answer an AlexaStatusMessage
function legacyLamp(bridge, onEvent) {
const lamp = bridge.registerDevice("Lamp", "lamp-1", ["LIGHT"]);
lamp.addCapability("Alexa.PowerController", { proactivelyReported: true });
lamp.addCapability(AlexaInterfaceType.ENDPOINT_HEALTH);
lamp.on("Event", onEvent);
lamp.on("ReportState", (request) => lamp.getStatusMessage(request.header.correlationToken).addPowerControllerProp(PowerState.OFF).send());
return lamp;
}
test("1.x: \"Event\" and \"ReportState\" listeners get every directive and their AlexaStatusMessage is the answer", async () => {
const { bridge, answers, receive, timers, errors } = bridgeWithout();
const got = [];
const lamp = legacyLamp(bridge, (request, namespace) => {
got.push([namespace, request.header.name, request.payload]);
if (namespace !== "Alexa.PowerController") {
const error = lamp.getErrorMessage(request.header.correlationToken);
error.setErrorMessage("INVALID_DIRECTIVE", "a lamp has no thermostat");
return error.send();
}
return lamp.getStatusMessage(request.header.correlationToken, true).addPowerControllerProp(PowerState.ON).send();
});
await receive(directive("Alexa.PowerController", "TurnOn", "lamp-1", "ct-1"));
await receive(directive("Alexa.ThermostatController", "SetTargetTemperature", "lamp-1", "ct-2", { targetSetpoint: { value: 20, scale: "CELSIUS" } }));
await receive(directive("Alexa", "ReportState", "lamp-1", "ct-3"));
assert.deepEqual(got, [
["Alexa.PowerController", "TurnOn", {}],
["Alexa.ThermostatController", "SetTargetTemperature", { targetSetpoint: { value: 20, scale: "CELSIUS" } }],
]);
assert.deepEqual(answers(), [
["alexaResponce", "Response", {}],
["alexaResponce", "ErrorResponse", { type: "INVALID_DIRECTIVE", message: "a lamp has no thermostat" }],
["alexaResponce", "StateReport", {}],
]);
assert.deepEqual([timers.pending, errors], [[], []], "each answer stopped its watchdog");
});
test("1.x: a deferred answer, DeferredResponse first and the Response on deferredResponse later", async () => {
const { bridge, answers, receive, timers, errors } = bridgeWithout();
let finish;
const lamp = legacyLamp(bridge, (request) => {
const token = request.header.correlationToken;
lamp.getStatusMessage(token, false, true).addEstimatedDeferralTime(20).send(false).catch(() => {});
finish = () => lamp.getStatusMessage(token, true).addPowerControllerProp(PowerState.ON).send(true);
});
await receive(directive("Alexa.PowerController", "TurnOn", "lamp-1", "ct-1"));
assert.deepEqual(timers.pending, []);
assert.equal(await finish(), "root/lamp-1/deferredResponse");
assert.equal(await finish(), "", "the second answer is not published");
assert.deepEqual(answers(), [
["alexaResponce", "DeferredResponse", { estimatedDeferralInSeconds: 20 }],
["deferredResponse", "Response", {}],
]);
assert.match(errors[0].message, /Alexa\.PowerController\.TurnOn for lamp-1 was answered before/);
});
test("1.x: a listener that stays silent, throws or rejects gets no INVALID_DIRECTIVE but the answer of the watchdog or INTERNAL_ERROR", async () => {
const unhandled = [];
const note = (reason) => unhandled.push(reason);
process.on("unhandledRejection", note);
const { bridge, answers, receive, timers, errors } = bridgeWithout();
legacyLamp(bridge, async (request) => {
if (request.header.name === "TurnOff") throw new Error("the relay did not answer");
});
await receive(directive("Alexa.SceneController", "Activate", "lamp-1", "ct-1"));
assert.deepEqual(answers(), [], "the listener owns the answer");
timers.fire();
await receive(directive("Alexa.PowerController", "TurnOff", "lamp-1", "ct-2"));
await nextTurn();
process.off("unhandledRejection", note);
assert.deepEqual(answers(), [
["alexaResponce", "ErrorResponse", { type: "INTERNAL_ERROR", message: "no answer within 6.5 s" }],
["alexaResponce", "ErrorResponse", { type: "INTERNAL_ERROR", message: "the relay did not answer" }],
]);
assert.equal(errors.length, 2);
assert.deepEqual(unhandled, []);
});
test("1.x and a handler on one device: the listener is called first, and the first answer is the one published", async () => {
const { bridge, answers, receive, errors } = bridgeWithout();
const order = [];
const lamp = legacyLamp(bridge, (request) => {
order.push("listener");
return lamp.getStatusMessage(request.header.correlationToken, true).addPowerControllerProp(PowerState.ON).send();
});
let second;
lamp.capability("Alexa.PowerController").on("TurnOn", async (ctx) => {
order.push("handler");
second = await ctx.respond();
});
await receive(directive("Alexa.PowerController", "TurnOn", "lamp-1", "ct-1"));
assert.deepEqual(order, ["listener", "handler"]);
assert.equal(answers().length, 1);
assert.equal(second.ok, false);
assert.deepEqual(errors.map((err) => err.message), [second.error.message]);
});