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>
This commit is contained in:
David 2026-09-28 20:54:53 +00:00
parent c59cf3ff10
commit d7dfc5b122
113 changed files with 3948 additions and 47 deletions

View file

@ -42,7 +42,8 @@ for (const { namespace, page, declared, unfit = [], chosen = false, folded = []
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`);
const supported = capability.properties ? capability.properties.supported.map((property) => property.name) : [];
// 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`);
}

View file

@ -0,0 +1,108 @@
"use strict";
// The interfaces of a TV and a speaker: what the examples of their pages do not show. The examples themselves are
// run by descriptors.test.js, device/discovery.test.js and dispatch/interfaces.test.js.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const {
Alex2MQTT, AlexaInterfaceType, DeclarationError, EqualizerController, InputController, Inputs, MemoryPublisher,
PlaybackController, PlaybackStateReporter, Speaker, StepSpeaker,
} = require("alex2node");
const { endpoint } = require("../helpers/endpoint.js");
const { directive } = require("../helpers/harness.js");
const tv = () => endpoint("tv-1", "TV", ["TV"]);
const refused = (descriptor, options, message) => assert.throws(
() => tv().add(descriptor, options),
(err) => err instanceof DeclarationError && err.message === `tv-1: ${descriptor.namespace}: ${message}`
);
test("1.x declarations: the version and the properties are those of the pages, where 1.5.2 had version 1 and no property", () => {
const T = AlexaInterfaceType;
const announced = (type) => {
const { version, properties } = tv().addCapability(type).getJSON();
return [version, properties && properties.supported];
};
assert.deepEqual(announced(T.SPEAKER), ["3", [{ name: "volume" }, { name: "muted" }]]);
assert.deepEqual(announced(T.STEP_SPEAKER), ["3", undefined]);
assert.deepEqual(announced(T.EQUALIZER_CONTROLLER), ["3", [{ name: "bands" }, { name: "mode" }]]);
assert.deepEqual(announced(T.PLAYBACK_CONTROLLER), ["3", undefined]);
assert.deepEqual(announced(T.PLAYBACK_STATE_REPORTER), ["3", [{ name: "playbackState" }]]);
assert.deepEqual(announced(T.INPUT_CONTROLLER), ["3", [{ name: "input" }]]);
assert.deepEqual(announced(T.CHANNEL_CONTROLLER), ["3", [{ name: "channel" }]]);
});
test("1.x declarations of an equalizer and of inputs have no way to say what the device has: check() says it", () => {
const device = tv();
device.addCapability(AlexaInterfaceType.EQUALIZER_CONTROLLER);
device.addCapability(AlexaInterfaceType.INPUT_CONTROLLER);
device.addCapability(AlexaInterfaceType.SPEAKER);
assert.deepEqual(device.check(), [
"tv-1: Alexa.EqualizerController: needs bands, modes or both: declare what the equalizer has",
"tv-1: Alexa.InputController: inputs: expected a list with 1 or more entries, got nothing",
]);
});
test("Speaker and StepSpeaker: neither has options, and the StepSpeaker has no properties object", () => {
assert.deepEqual(tv().add(StepSpeaker).toJSON(), { type: "AlexaInterface", interface: "Alexa.StepSpeaker", version: "3" });
refused(Speaker, { maximum: 11 }, "maximum: not an option of the interface");
assert.deepEqual(Object.keys(PlaybackStateReporter.directives), []);
});
test("EqualizerController: a device with bands, with modes or with both lists the property of what it has", () => {
const bands = { supported: ["BASS", "TREBLE"], range: { minimum: -10, maximum: 10 } };
const onlyBands = tv().add(EqualizerController, { bands }).toJSON();
assert.deepEqual(onlyBands.properties.supported, [{ name: "bands" }]);
assert.deepEqual(onlyBands.configurations, { bands: { supported: [{ name: "BASS" }, { name: "TREBLE" }], range: bands.range } });
const onlyModes = tv().add(EqualizerController, { modes: ["NIGHT", "TV"] }).toJSON();
assert.deepEqual(onlyModes.properties.supported, [{ name: "mode" }]);
assert.deepEqual(onlyModes.configurations, { modes: { supported: [{ name: "NIGHT" }, { name: "TV" }] } });
assert.equal("configuration" in onlyModes, false, "the interface spells it configurations");
refused(EqualizerController, {}, "needs bands, modes or both: declare what the equalizer has");
refused(EqualizerController, { bands: { ...bands, range: { minimum: 6, maximum: -6 } } }, "bands.range.maximum -6 is not above bands.range.minimum 6");
refused(EqualizerController, { bands: { ...bands, supported: ["BASS", "BASS"] } }, "BASS is listed twice");
refused(EqualizerController, { modes: ["CINEMA"] }, 'modes[0]: expected MOVIE | MUSIC | NIGHT | SPORT | TV, got "CINEMA"');
});
test("PlaybackController: all eight operations unless the declaration lists some, and only those reach the handler", async () => {
assert.deepEqual(tv().add(PlaybackController).toJSON().supportedOperations, Object.keys(PlaybackController.directives));
refused(
PlaybackController,
{ supportedOperations: ["Eject"] },
'supportedOperations[0]: expected Play | Pause | Stop | Next | Previous | FastForward | Rewind | StartOver, got "Eject"'
);
const sent = new MemoryPublisher();
const bridge = new Alex2MQTT("u", "p", "root", false, { publisher: sent, log: () => {} });
const player = bridge.addDevice({ endpointId: "player-1", name: "Player", categories: ["STREAMING_DEVICE"] });
const state = player.add(PlaybackStateReporter);
const seen = [];
player.add(PlaybackController, { supportedOperations: ["Play", "Pause"] }).on("*", (ctx) => {
seen.push(ctx.name);
return ctx.respond((s) => s.set(state, "playbackState", { state: ctx.name === "Play" ? "PLAYING" : "PAUSED" }));
});
for (const [name, token] of [["Play", "ct-1"], ["Rewind", "ct-2"], ["Pause", "ct-3"]]) {
await bridge.receive("root/player-1/alexaDirective", JSON.stringify(directive("Alexa.PlaybackController", name, "player-1", token)));
}
assert.deepEqual(seen, ["Play", "Pause"]);
const answers = sent.published.map(({ message: { event, context } }) => [
event.header.name,
context ? context.properties.filter(({ name }) => name === "playbackState").map(({ value }) => value.state) : event.payload.type,
]);
assert.deepEqual(answers, [["Response", ["PLAYING"]], ["ErrorResponse", "INVALID_DIRECTIVE"], ["Response", ["PAUSED"]]]);
});
test("InputController: the inputs are on the capability, each name and friendly name stands for one input", () => {
assert.equal(Inputs.length, 61);
assert.equal(new Set(Inputs).size, 61);
const inputs = [{ name: "HDMI 1", friendlyNames: ["Console"] }, { name: "TUNER" }];
assert.deepEqual(tv().add(InputController, { inputs }).toJSON().inputs, inputs);
refused(InputController, { inputs: [] }, "inputs: expected a list with 1 or more entries, got []");
refused(InputController, { inputs: [{ name: "SCART" }] }, 'inputs[0].name: expected an input like HDMI 1, got "SCART"');
refused(InputController, { inputs: [{ name: "TV" }, { name: "TV" }] }, "TV is listed twice, a name stands for one input");
refused(
InputController,
{ inputs: [{ name: "HDMI 1", friendlyNames: ["Console"] }, { name: "HDMI 2", friendlyNames: ["Console"] }] },
"Console is listed twice, a name stands for one input"
);
});