registry: Lock, Color, ColorTemperature, Percentage, PowerLevel, Contact, Motion, Humidity
Eight descriptors written from their pages, each with its version, the value of its property and the payload of every directive. Seven replace a stub; Alexa.HumiditySensor is new, as AlexaInterfaceType.HUMIDITY_SENSOR. LockController is deferrable: defer() on a lock logs no warning. A 1.x declaration of LockController, MotionSensor, PercentageController or PowerLevelController now lists its property in discovery, where 1.5.2 listed none: the zoo's lock, motion sensor and heater come out as the JSON Alexa accepted. A payload that does not fit (hue 361, 900 K) is answered with INVALID_VALUE. 48 examples of the eight pages are saved as fixtures. 191 tests pass, 168 before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
feff828ec1
commit
af1489696d
111 changed files with 2971 additions and 45 deletions
82
test/dispatch/interfaces.test.js
Normal file
82
test/dispatch/interfaces.test.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
"use strict";
|
||||
// The interfaces of test/fixtures/interfaces.js, given the directives of their pages: the handler gets the payload as
|
||||
// the page prints it, and a payload that does not fit is answered with INVALID_VALUE without the handler.
|
||||
const { test } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { Alex2MQTT, LockController, MemoryPublisher, PowerLevelController, registry } = require("alex2node");
|
||||
const interfaces = require("../fixtures/interfaces.js");
|
||||
const { doc, examples } = require("../helpers/fixtures.js");
|
||||
const { directive } = require("../helpers/harness.js");
|
||||
|
||||
function bridgeWithout() {
|
||||
const sent = new MemoryPublisher();
|
||||
const logged = [];
|
||||
const bridge = new Alex2MQTT("u", "p", "root", false, { publisher: sent, log: (line) => logged.push(line) });
|
||||
const device = bridge.addDevice({ endpointId: "device-1", name: "Device", categories: ["OTHER"] });
|
||||
let count = 0;
|
||||
// The examples of a page share one message id and one correlation token
|
||||
const receive = ({ header, payload }) => {
|
||||
count += 1;
|
||||
const unique = { ...header, messageId: `message-${count}`, correlationToken: `ct-${count}` };
|
||||
const message = { header: unique, endpoint: { endpointId: "device-1" }, payload };
|
||||
return bridge.receive("root/device-1/alexaDirective", JSON.stringify(message));
|
||||
};
|
||||
/** What was published, as [the last part of the topic, the namespace and the name of the event, its payload]. */
|
||||
const answers = () => sent.published.map(({ topic, message: { event } }) => [topic.split("/").pop(), event.header.namespace, event.header.name, event.payload]);
|
||||
return { device, sent, logged, receive, answers };
|
||||
}
|
||||
|
||||
for (const { namespace, page, declared, refused } of interfaces.filter((entry) => entry.refused)) {
|
||||
test(`${namespace}: the directives of ${page}.html reach the handler, a payload that does not fit is answered with INVALID_VALUE`, async () => {
|
||||
const { device, receive, answers } = bridgeWithout();
|
||||
const seen = [];
|
||||
device.add(registry.get(namespace), declared[0].options).on("*", (ctx) => {
|
||||
seen.push([ctx.name, ctx.payload]);
|
||||
return ctx.respond();
|
||||
});
|
||||
|
||||
const printed = examples(page, ".directive").map((name) => doc(page, name).directive);
|
||||
for (const example of printed) await receive(example);
|
||||
assert.deepEqual(seen, printed.map(({ header, payload }) => [header.name, payload]));
|
||||
assert.deepEqual(answers(), printed.map(() => ["alexaResponce", "Alexa", "Response", {}]));
|
||||
|
||||
for (const [name, payload] of refused) await receive(directive(namespace, name, "device-1", "ct-1", payload));
|
||||
assert.deepEqual(
|
||||
answers().slice(printed.length),
|
||||
refused.map(([, , message]) => ["alexaResponce", "Alexa", "ErrorResponse", { type: "INVALID_VALUE", message }])
|
||||
);
|
||||
assert.equal(seen.length, printed.length, "the handler is not called with a payload that does not fit");
|
||||
});
|
||||
}
|
||||
|
||||
test("Alexa.LockController: a slow lock defers without a warning, and answers as the page does", async () => {
|
||||
const { device, sent, logged, receive, answers } = bridgeWithout();
|
||||
const lock = device.add(LockController);
|
||||
lock.on("Lock", async (ctx) => {
|
||||
await ctx.defer(15);
|
||||
return ctx.respond((s) => s.set(lock, "lockState", "LOCKED"));
|
||||
});
|
||||
const level = device.add(PowerLevelController);
|
||||
level.on("SetPowerLevel", async (ctx) => {
|
||||
await ctx.defer();
|
||||
return ctx.respond((s) => s.set(level, "powerLevel", ctx.payload.powerLevel));
|
||||
});
|
||||
|
||||
await receive(doc("alexa-lockcontroller", "Lock.directive").directive);
|
||||
const deferred = doc("alexa-lockcontroller", "Lock-slow.deferred");
|
||||
const answer = doc("alexa-lockcontroller", "Lock-slow.response");
|
||||
assert.deepEqual(answers(), [
|
||||
["alexaResponce", "Alexa", "DeferredResponse", deferred.event.payload],
|
||||
["deferredResponse", "Alexa", "Response", answer.event.payload],
|
||||
]);
|
||||
assert.equal("context" in sent.published[0].message, false);
|
||||
const [{ namespace, name, value }] = answer.context.properties;
|
||||
const reported = sent.published[1].message.context.properties.find((property) => property.namespace === namespace);
|
||||
assert.deepEqual([reported.name, reported.value], [name, value]);
|
||||
|
||||
// The page of the PowerLevelController has no DeferredResponse
|
||||
await receive(doc("alexa-powerlevelcontroller", "SetPowerLevel.directive").directive);
|
||||
assert.deepEqual(logged.filter((line) => line.includes("DeferredResponse")), [
|
||||
"warning: Amazon documents no DeferredResponse for Alexa.PowerLevelController, Alexa may not wait for the answer",
|
||||
]);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue