"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"); assert.throws(() => power.on("TurnOff"), (err) => err instanceof DeclarationError && err.message === "lamp-1: Alexa.PowerController: on(\"TurnOff\") got undefined as the handler: pass a function that takes the context of the directive"); }); 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]); });