registry: RangeController, ModeController, ToggleController with resources, presets and semantics
The three generic controllers are described in full and leave the stub table (62 stubs remain). A declaration
takes an instance name, friendly names from text() and asset(), and the options of the interface: range, unit and
presets for a range; supportedModes and ordered for a mode; semantics() for all three, with actions mapped to
directives and states mapped to a value or a range of values.
Refused at device.add(), each with the endpoint, interface and instance in the message: no instance name or no
friendly name; an instance declared twice; a first friendly name or a phrase used by another capability of the
endpoint; a reserved word, an asset id that is not in the catalog; range.max not above range.min; a preset off the
precision grid or outside the range; fewer than two modes or a mode listed twice; a mapping to a directive the
interface does not have, to a payload that does not fit, to a mode that is not listed, to AdjustMode on modes
that are not ordered; SetEcoOn, SetEcoOff, EcoOn and EcoOff on anything but a toggle.
For 1.x callers: Alexa.RangeController lists rangeValue (1.5.2 sent supported: []); addSupportedModes() announces
a mode given as a string as { value } and notes it, and takes { ordered: false } as a second argument, the default
stays true as 1.5.2 sent it (device.add() defaults to false). Declared the 1.x way, the zoo fan and curtain give
the same JSON as before.
Tests: the zoo blind, whose range, unit and semantics were written by hand into the JSON of 1.5.2, is declared
with device.add() and equals that JSON plus the Alexa capability; five capability objects of Amazon's examples
are reproduced the same way. 27 examples of four pages added. npm test: 108 pass (was 85) in 10.5-11.3 s, also on
Node 18.20.8 and 20.20.2.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
c492ef74d1
commit
2558e21787
80 changed files with 3131 additions and 107 deletions
|
|
@ -30,16 +30,20 @@ test("a descriptor of tier 1 or 2 has an entry in test/fixtures/interfaces.js, a
|
|||
assert.deepEqual(described.map((descriptor) => descriptor.namespace), interfaces.map((entry) => entry.namespace).sort());
|
||||
});
|
||||
|
||||
for (const { namespace, page } of interfaces) {
|
||||
for (const { namespace, page, declared } 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);
|
||||
|
||||
for (const name of examples(page).filter((example) => example.startsWith("discovery"))) {
|
||||
for (const capability of capabilities(page, namespace, name)) {
|
||||
assert.equal(capability.version, descriptor.version, `${name}: version`);
|
||||
// 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`);
|
||||
const supported = capability.properties ? capability.properties.supported.map((property) => property.name) : [];
|
||||
for (const property of supported) assert.ok(property in descriptor.properties, `${name}: ${property}`);
|
||||
assert.deepEqual(supported, Object.keys(descriptor.properties), `${example}: properties`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -64,6 +68,19 @@ for (const { namespace, page } of interfaces) {
|
|||
});
|
||||
}
|
||||
|
||||
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");
|
||||
|
|
|
|||
104
test/registry/semantics.test.js
Normal file
104
test/registry/semantics.test.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
"use strict";
|
||||
// semantics(): the mappings of phrases to directives and of states to values, as discovery lists them.
|
||||
const { test } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { Actions, AlexaActions, ModeController, RangeController, SchemaError, States, ToggleController, semantics } = require("alex2node");
|
||||
const { doc } = require("../helpers/fixtures.js");
|
||||
|
||||
// The schema of the semantics option of an interface, and the message of the SchemaError it throws.
|
||||
const semanticsOf = (descriptor) => ({
|
||||
parse: (input) => descriptor.options.parse({ ...COMPLETE[descriptor.namespace], semantics: input }).semantics,
|
||||
});
|
||||
const COMPLETE = {
|
||||
"Alexa.RangeController": { range: { min: 0, max: 100, precision: 1 } },
|
||||
"Alexa.ModeController": {
|
||||
supportedModes: ["Up", "Down"].map((name) => ({ value: `Position.${name}`, friendlyNames: [{ "@type": "text", value: { text: name, locale: "en-US" } }] })),
|
||||
},
|
||||
"Alexa.ToggleController": {},
|
||||
};
|
||||
function refusal(descriptor, input) {
|
||||
try {
|
||||
semanticsOf(descriptor).parse(input);
|
||||
} catch (err) {
|
||||
assert.ok(err instanceof SchemaError, `threw ${err}`);
|
||||
return err.message;
|
||||
}
|
||||
return assert.fail("the semantics were accepted");
|
||||
}
|
||||
|
||||
test("semantics() builds the Semantics object of alexa-discovery-objects.html", () => {
|
||||
const built = semantics()
|
||||
.action(["Close", "Lower"], "SetRangeValue", { rangeValue: 0 })
|
||||
.action(["Open", "Raise"], "SetRangeValue", { rangeValue: 100 })
|
||||
.state("Closed", 0)
|
||||
.stateRange("Open", 1, 100);
|
||||
const { semantics: example } = doc("alexa-discovery-objects", "semantics");
|
||||
assert.deepEqual(semanticsOf(RangeController).parse(built), example);
|
||||
assert.deepEqual(built.actionMappings[0], doc("alexa-discovery-objects", "actionMapping"));
|
||||
assert.deepEqual(built.stateMappings[1], doc("alexa-discovery-objects", "stateMapping"));
|
||||
// What the schema returns is the plain object, and it takes that object as it takes the builder
|
||||
assert.equal(Object.getPrototypeOf(semanticsOf(RangeController).parse(built)), Object.prototype);
|
||||
assert.deepEqual(semanticsOf(RangeController).parse(example), example);
|
||||
});
|
||||
|
||||
test("a phrase and a state are given by name, by id or by the AlexaActions enum of 1.x", () => {
|
||||
const { actionMappings, stateMappings } = semantics()
|
||||
.action("Open", "TurnOn")
|
||||
.action("Alexa.Actions.Close", "TurnOff")
|
||||
.action([AlexaActions.Raise, Actions[3]], "TurnOn")
|
||||
.state(["Closed", States[0]], "OFF");
|
||||
assert.deepEqual(actionMappings.map((mapping) => mapping.actions), [
|
||||
["Alexa.Actions.Open"], ["Alexa.Actions.Close"], ["Alexa.Actions.Raise", "Alexa.Actions.Lower"],
|
||||
]);
|
||||
assert.deepEqual(stateMappings[0].states, ["Alexa.States.Closed", "Alexa.States.Open"]);
|
||||
// No payload given, none in discovery; an empty one is kept, as in the ToggleController examples
|
||||
assert.deepEqual(actionMappings[0].directive, { name: "TurnOn" });
|
||||
assert.deepEqual(semantics().action("Open", "TurnOn", {}).actionMappings[0].directive, { name: "TurnOn", payload: {} });
|
||||
assert.deepEqual(semanticsOf(ToggleController).parse(semantics().action("Open", "TurnOn")).actionMappings[0].directive, { name: "TurnOn" });
|
||||
});
|
||||
|
||||
test("a mapping is checked against the directives and the property of the interface", () => {
|
||||
const cases = [
|
||||
[RangeController, semantics().action("Open", "SetRange", { rangeValue: 100 }),
|
||||
'semantics.actionMappings[0].directive.name: expected a directive of the interface: SetRangeValue, AdjustRangeValue, got "SetRange"'],
|
||||
[RangeController, semantics().action("Open", "SetRangeValue", { rangeValue: "100" }),
|
||||
'semantics.actionMappings[0].directive.payload.rangeValue: expected a number, got "100"'],
|
||||
[RangeController, semantics().action("Open", "SetRangeValue"),
|
||||
"semantics.actionMappings[0].directive.payload.rangeValue: expected a number, got nothing"],
|
||||
[RangeController, semantics().action("Lower", "AdjustRangeValue", { rangeValueDelta: -10 }),
|
||||
"semantics.actionMappings[0].directive.payload.rangeValueDeltaDefault: expected true or false, got nothing"],
|
||||
[RangeController, semantics().action("Shut", "SetRangeValue", { rangeValue: 0 }),
|
||||
'semantics.actionMappings[0].actions[0]: expected a phrase of Alexa.Actions: Open, Close, Raise, Lower, got "Alexa.Actions.Shut"'],
|
||||
[RangeController, semantics().state("Ajar", 50),
|
||||
'semantics.stateMappings[0].states[0]: expected a state of Alexa.States: Open, Closed, Low, Empty, Full, Done, Stuck, got "Alexa.States.Ajar"'],
|
||||
[RangeController, semantics().state("Closed", "OFF"), 'semantics.stateMappings[0].value: expected a number, got "OFF"'],
|
||||
[RangeController, semantics().stateRange("Open", 100, 1), "semantics.stateMappings[0].range: the minimumValue 100 is above the maximumValue 1"],
|
||||
[ToggleController, semantics().state("Open", "on"), 'semantics.stateMappings[0].value: expected ON | OFF, got "on"'],
|
||||
[ToggleController, semantics().stateRange("Open", 1, 100), 'semantics.stateMappings[0].@type: expected StatesToValue, got "StatesToRange"'],
|
||||
[ModeController, semantics().state("Closed", 0), "semantics.stateMappings[0].value: expected a string of 1 or more characters, got 0"],
|
||||
[ToggleController, semantics(), "semantics: expected semantics() with at least one action or state, got {}"],
|
||||
[ToggleController, "open", 'semantics: expected semantics() with at least one action or state, got "open"'],
|
||||
[ToggleController, { actionMappings: [{ "@type": "ActionToDirective", actions: ["Alexa.Actions.Open"], directive: { name: "TurnOn" } }] },
|
||||
'semantics.actionMappings[0].@type: expected "ActionsToDirective", got "ActionToDirective"'],
|
||||
];
|
||||
for (const [descriptor, input, message] of cases) assert.equal(refusal(descriptor, input), message);
|
||||
});
|
||||
|
||||
test("the phrases and states of Alexa hunches are for a ToggleController", () => {
|
||||
const eco = semantics().action("SetEcoOn", "TurnOn").action("SetEcoOff", "TurnOff").state("EcoOn", "ON").state("EcoOff", "OFF");
|
||||
assert.deepEqual(semanticsOf(ToggleController).parse(eco), {
|
||||
actionMappings: [
|
||||
{ "@type": "ActionsToDirective", actions: ["Alexa.Actions.SetEcoOn"], directive: { name: "TurnOn" } },
|
||||
{ "@type": "ActionsToDirective", actions: ["Alexa.Actions.SetEcoOff"], directive: { name: "TurnOff" } },
|
||||
],
|
||||
stateMappings: [
|
||||
{ "@type": "StatesToValue", states: ["Alexa.States.EcoOn"], value: "ON" },
|
||||
{ "@type": "StatesToValue", states: ["Alexa.States.EcoOff"], value: "OFF" },
|
||||
],
|
||||
});
|
||||
assert.match(
|
||||
refusal(RangeController, semantics().action("SetEcoOn", "SetRangeValue", { rangeValue: 20 })),
|
||||
/^semantics\.actionMappings\[0\]\.actions\[0\]: expected a phrase of Alexa\.Actions: Open, Close, Raise, Lower, got "Alexa\.Actions\.SetEcoOn"$/
|
||||
);
|
||||
assert.match(refusal(ModeController, semantics().state("EcoOn", "Position.Up")), /expected a state of Alexa\.States: Open, Closed, Low, Empty, Full, Done, Stuck, got/);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue