Alex2Node/test/registry/descriptors.test.js
David d7dfc5b122 registry: Speaker, StepSpeaker, EqualizerController, PlaybackController, PlaybackStateReporter, InputController, ChannelController
Seven descriptors written from their pages replace seven stubs. All are version 3; 1.5.2 announced Speaker,
StepSpeaker, EqualizerController and PlaybackStateReporter as version 1, and all but ChannelController without
a property. StepSpeaker has no properties object in discovery and PlaybackController one without fields, next
to supportedOperations; a directive for an operation the device did not list is answered with
INVALID_DIRECTIVE. The equalizer declares bands, modes or both under "configurations", a TV its inputs from
the 61 names of the page.

Not described: the lineup of ChannelController, whose discovery example the page folds away. The discovery
examples of InputController spell one key "friendNames"; the fixtures have friendlyNames, as its table does.
62 examples of the six pages are saved as fixtures. 241 tests pass, 215 before.

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

155 lines
9.4 KiB
JavaScript

"use strict";
// The registry: every descriptor is well formed, the ones described in full agree with the examples of their page,
// and every interface name of 1.x resolves.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const { registry, AlexaInterface, AlexaInterfaceType, DeclarationError, SchemaError } = require("alex2node");
const interfaces = require("../fixtures/interfaces.js");
const { doc, examples, capabilities, reported } = require("../helpers/fixtures.js");
const described = registry.list().filter((descriptor) => descriptor.tier !== 3);
const stubs = registry.list().filter((descriptor) => descriptor.tier === 3);
test("every descriptor has a namespace, a version, a kind and the page it was written from", () => {
const namespaces = registry.list().map((descriptor) => descriptor.namespace);
assert.deepEqual(namespaces, [...namespaces].sort(), "list() is ordered by namespace");
for (const descriptor of registry.list()) {
const { namespace } = descriptor;
assert.match(namespace, /^Alexa(\.[A-Za-z]+)*$/);
assert.match(descriptor.version, /^\d+(\.\d+)?$/, namespace);
assert.match(descriptor.doc, /^https:\/\/developer\.amazon\.com\/[\w\-./]+\.html$/, namespace);
assert.ok(["base", "controller", "sensor", "eventSource"].includes(descriptor.kind), namespace);
assert.ok([1, 2, 3].includes(descriptor.tier), namespace);
assert.equal(typeof descriptor.instanced, "boolean", namespace);
for (const [key, property] of Object.entries(descriptor.properties)) assert.equal(property.name, key, namespace);
for (const [key, directive] of Object.entries(descriptor.directives)) assert.equal(directive.name, key, namespace);
}
});
test("a descriptor of tier 1 or 2 has an entry in test/fixtures/interfaces.js, and every entry a descriptor", () => {
assert.deepEqual(described.map((descriptor) => descriptor.namespace), interfaces.map((entry) => entry.namespace).sort());
});
for (const { namespace, page, declared, unfit = [], chosen = false, folded = [] } of interfaces) {
test(`${namespace}: version, properties, directive payloads and property values of ${page}.html`, () => {
const descriptor = registry.get(namespace);
assert.ok(descriptor.doc.endsWith(`/${page}.html`), descriptor.doc);
// The discovery examples of the page, and of another page when the entry declares from one
const discovery = examples(page).filter((example) => example.startsWith("discovery")).map((example) => [page, example]);
for (const { page: other = page, example } of declared) discovery.push([other, example]);
for (const [from, example] of discovery) {
for (const capability of capabilities(from, namespace, example)) {
assert.equal(capability.version, descriptor.version, `${example}: version`);
assert.equal("instance" in capability, descriptor.instanced, `${example}: instance`);
// Without a properties object, or with one without fields as the PlaybackController has: no property
const supported = ((capability.properties || {}).supported || []).map((property) => property.name);
const known = Object.keys(descriptor.properties);
assert.deepEqual(supported, chosen ? known.filter((name) => supported.includes(name)) : known, `${example}: properties`);
}
}
// A page also shows the directives of the interface it is paired with: TurnOn on the page of WakeOnLANController
const directives = examples(page, ".directive").map((name) => doc(page, name).directive)
.filter((directive) => directive.header.namespace === namespace);
assert.deepEqual(
[...new Set(directives.map((directive) => directive.header.name))].sort(),
Object.keys(descriptor.directives).filter((name) => name !== "ReportState" && !folded.includes(name)).sort(),
"the page has an example of every directive"
);
for (const { header, payload } of directives) {
assert.deepEqual(descriptor.directives[header.name].payload.parse(payload, "payload"), payload);
}
const values = reported(page, namespace);
for (const { example, property } of values) {
const known = descriptor.properties[property.name];
assert.ok(known, `${example}: ${property.name} is not a property of the descriptor`);
assert.deepEqual(known.value.parse(property.value, property.name), property.value, example);
}
if (Object.keys(descriptor.properties).length > 0) assert.ok(values.length > 0, "the page reports no property of the interface");
for (const [name, value] of unfit) {
assert.throws(() => descriptor.properties[name].value.parse(value, name), SchemaError, `${name} ${JSON.stringify(value)}`);
}
});
}
test("the error types of an interface are in the error type table under its error namespace", () => {
const { ERROR_TYPES } = require("../../dist/cjs/registry/catalog.js");
const withErrors = registry.list().filter((descriptor) => descriptor.errorNamespace || descriptor.errorTypes);
// The generic controllers answer a safety problem under Alexa.Safety
for (const name of ["Alexa.ModeController", "Alexa.RangeController", "Alexa.ToggleController"]) {
assert.deepEqual(registry.get(name).errorTypes, ["OBSTACLE_DETECTED", "SAFETY_BEAM_BREACHED"], name);
}
for (const { namespace, errorNamespace, errorTypes } of withErrors) {
assert.ok(errorTypes.length > 0, namespace);
for (const type of errorTypes) assert.equal(ERROR_TYPES[type], errorNamespace, `${namespace}: ${type}`);
}
});
test("Alexa.EndpointHealth is version 3.1 and reports connectivity, with a reason when it has one", () => {
const { version, properties } = registry.get("Alexa.EndpointHealth");
assert.equal(version, "3.1");
assert.deepEqual(Object.keys(properties), ["connectivity"]);
const unreachable = doc("alexa-endpointhealth", "connectivity.property").value;
assert.deepEqual(properties.connectivity.value.parse(unreachable), { value: "UNREACHABLE", reason: "WIFI_BAD_PASSWORD" });
assert.deepEqual(properties.connectivity.value.parse({ value: "OK" }), { value: "OK" });
assert.throws(
() => properties.connectivity.value.parse({ value: "UNREACHABLE", reason: "UNPLUGGED" }, "connectivity"),
/^SchemaError: connectivity\.reason: expected WIFI_BAD_PASSWORD \| .* \| UNKNOWN, got "UNPLUGGED"$/
);
});
test("a payload or a value that does not fit is refused with its path", () => {
const { directives, properties } = registry.get("Alexa.BrightnessController");
assert.throws(
() => directives.SetBrightness.payload.parse({ brightness: "lots" }, "payload"),
/^SchemaError: payload\.brightness: expected an integer from 0 to 100, got "lots"$/
);
assert.throws(() => directives.AdjustBrightness.payload.parse({ brightnessDelta: 101 }, "payload"), /from -100 to 100/);
assert.throws(() => properties.brightness.value.parse(-1, "brightness"), /^SchemaError: brightness: expected an integer/);
assert.throws(() => registry.get("Alexa.PowerController").properties.powerState.value.parse("on", "powerState"), /expected ON \| OFF/);
});
test("registry.get: the namespace and the AlexaInterfaceType member give the same descriptor; an unknown name throws", () => {
assert.equal(registry.get(AlexaInterfaceType.POWER_CONTROLLER), registry.get("Alexa.PowerController"));
assert.equal(registry.has("Alexa.PowerController"), true);
assert.equal(registry.has("Alexa.TeleportController"), false);
assert.throws(
() => registry.get("Alexa.TeleportController"),
(err) => err instanceof DeclarationError && err.message === '"Alexa.TeleportController" is not an interface alex2node knows'
);
assert.throws(() => registry.get(AlexaInterfaceType.UNKNOWN), DeclarationError);
});
test("every AlexaInterfaceType is in the registry: described, or a stub with the version and properties of 1.5.2", () => {
const names = Object.values(AlexaInterfaceType).filter((name) => name !== AlexaInterfaceType.UNKNOWN);
// The 69 of 1.x and HUMIDITY_SENSOR
assert.equal(names.length, 70);
assert.deepEqual(names.filter((name) => !registry.has(name)), []);
// "Alexa" is the one interface that was never in the enum
assert.deepEqual(registry.list().map((descriptor) => descriptor.namespace).filter((name) => !names.includes(name)), ["Alexa"]);
for (const stub of stubs) {
assert.deepEqual(stub.directives, {}, stub.namespace);
assert.equal(stub.instanced, false, stub.namespace);
}
});
test("AlexaInterface.getVersion() and getProps() read the registry", () => {
// What a thermostat or a security panel lists is for its declaration to say: a 1.x declaration of the thermostat
// lists what 1.5.2 listed, one of the panel the arm state and no alarm
const chosen = {
"Alexa.SecurityPanelController": ["armState"],
"Alexa.ThermostatController": ["targetSetpoint", "lowerSetpoint", "upperSetpoint", "thermostatMode"],
"Alexa.ThermostatController.Schedule": ["scheduleEnabled"],
};
for (const descriptor of registry.list().filter(({ namespace }) => namespace !== "Alexa")) {
const capability = new AlexaInterface(descriptor.namespace);
assert.equal(capability.getVersion(), descriptor.version, descriptor.namespace);
assert.deepEqual(capability.getProps(), chosen[descriptor.namespace] ?? Object.keys(descriptor.properties), descriptor.namespace);
}
// 1.5.2 had no version for these two and put "UNKNOWN" in discovery
assert.equal(new AlexaInterface(AlexaInterfaceType.TIME_HOLD_CONTROLLER).getVersion(), "3");
assert.equal(new AlexaInterface(AlexaInterfaceType.CAMERA_LIVE_VIEW_CONTROLLER).getVersion(), "1.7");
});