errors: typed helpers for every documented error type
AlexaErrors has a helper for each of the 73 types of the table on alexa-errorresponse.html, named after the type in camel case: 63 take the message only, 10 take the payload fields their page documents. A field the page requires (reason, currentDeviceMode, currentChargeState, resourceType) is a required argument, so leaving it out does not compile; from JavaScript the helper throws a TypeError that lists the values. validRange is optional, as the page says, and has both ends when given. temperatureOutOfRange and setpointsTooClose stay as shorter names. The helpers are compared with the 15 examples of the four error pages. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
4a3dd81bcf
commit
94b13f733a
36 changed files with 874 additions and 138 deletions
19
test/fixtures/alexa-docs/alexa-errorresponse/ENDPOINT_CONTROL_UNAVAILABLE.error.json
vendored
Normal file
19
test/fixtures/alexa-docs/alexa-errorresponse/ENDPOINT_CONTROL_UNAVAILABLE.error.json
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa",
|
||||
"name": "ErrorResponse",
|
||||
"messageId": "Unique identifier, preferably a version 4 UUID",
|
||||
"correlationToken": "Opaque correlation token that matches the request",
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"endpoint": {
|
||||
"endpointId": "Endpoint ID"
|
||||
},
|
||||
"payload": {
|
||||
"type": "ENDPOINT_CONTROL_UNAVAILABLE",
|
||||
"message": "Device has no connectivity package enabled.",
|
||||
"reason": "NO_CONNECTIVITY_PACKAGE_ENABLED"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
test/fixtures/alexa-docs/alexa-errorresponse/ENDPOINT_LOW_POWER.error.json
vendored
Normal file
19
test/fixtures/alexa-docs/alexa-errorresponse/ENDPOINT_LOW_POWER.error.json
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa",
|
||||
"name": "ErrorResponse",
|
||||
"messageId": "Unique identifier, preferably a version 4 UUID",
|
||||
"correlationToken": "Opaque correlation token that matches the request",
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"endpoint": {
|
||||
"endpointId": "Endpoint ID"
|
||||
},
|
||||
"payload": {
|
||||
"type": "ENDPOINT_LOW_POWER",
|
||||
"message": "The lock battery is low",
|
||||
"percentageState": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
19
test/fixtures/alexa-docs/alexa-errorresponse/NOT_SUPPORTED_IN_CURRENT_MODE.error.json
vendored
Normal file
19
test/fixtures/alexa-docs/alexa-errorresponse/NOT_SUPPORTED_IN_CURRENT_MODE.error.json
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa",
|
||||
"name": "ErrorResponse",
|
||||
"messageId": "Unique identifier, preferably a version 4 UUID",
|
||||
"correlationToken": "Opaque correlation token that matches the request",
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"endpoint": {
|
||||
"endpointId": "Endpoint ID"
|
||||
},
|
||||
"payload": {
|
||||
"type": "NOT_SUPPORTED_IN_CURRENT_MODE",
|
||||
"message": "The device is in demand response mode.",
|
||||
"currentDeviceMode": "DEMAND_RESPONSE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa",
|
||||
"name": "ErrorResponse",
|
||||
"messageId": "Unique identifier, preferably a version 4 UUID",
|
||||
"correlationToken": "Opaque correlation token that matches the request",
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"endpoint": {
|
||||
"endpointId": "Endpoint ID"
|
||||
},
|
||||
"payload": {
|
||||
"type": "NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE ",
|
||||
"message": "The device has been charging",
|
||||
"currentChargeState": "ALREADY_CHARGED_TO_REQUIRED_LEVEL",
|
||||
"currentChargeLevelInPercentage": 75
|
||||
}
|
||||
}
|
||||
}
|
||||
28
test/fixtures/alexa-docs/alexa-errorresponse/TEMPERATURE_VALUE_OUT_OF_RANGE.error.json
vendored
Normal file
28
test/fixtures/alexa-docs/alexa-errorresponse/TEMPERATURE_VALUE_OUT_OF_RANGE.error.json
vendored
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa",
|
||||
"name": "ErrorResponse",
|
||||
"messageId": "Unique identifier, preferably a version 4 UUID",
|
||||
"correlationToken": "Opaque correlation token that matches the request",
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"endpoint": {
|
||||
"endpointId": "Endpoint ID"
|
||||
},
|
||||
"payload": {
|
||||
"type": "TEMPERATURE_VALUE_OUT_OF_RANGE",
|
||||
"message": "The requested temperature of -15 is out of range.",
|
||||
"validRange": {
|
||||
"minimumValue": {
|
||||
"value": 15,
|
||||
"scale": "CELSIUS"
|
||||
},
|
||||
"maximumValue": {
|
||||
"value": 30,
|
||||
"scale": "CELSIUS"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
test/fixtures/alexa-docs/alexa-errorresponse/VALUE_OUT_OF_RANGE.error.json
vendored
Normal file
22
test/fixtures/alexa-docs/alexa-errorresponse/VALUE_OUT_OF_RANGE.error.json
vendored
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa",
|
||||
"name": "ErrorResponse",
|
||||
"messageId": "Unique identifier, preferably a version 4 UUID",
|
||||
"correlationToken": "Opaque correlation token that matches the request",
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"endpoint": {
|
||||
"endpointId": "Endpoint ID"
|
||||
},
|
||||
"payload": {
|
||||
"type": "VALUE_OUT_OF_RANGE",
|
||||
"message": "The percent value cannot exceed 100.",
|
||||
"validRange": {
|
||||
"minimumValue": 0,
|
||||
"maximumValue": 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
test/fixtures/alexa-docs/alexa-safety-errorresponse/OBSTACLE_DETECTED.error.json
vendored
Normal file
18
test/fixtures/alexa-docs/alexa-safety-errorresponse/OBSTACLE_DETECTED.error.json
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa.Safety",
|
||||
"name": "ErrorResponse",
|
||||
"messageId": "Unique identifier, preferably a version 4 UUID",
|
||||
"correlationToken": "Opaque correlation token that matches the request",
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"endpoint": {
|
||||
"endpointId": "Endpoint ID"
|
||||
},
|
||||
"payload": {
|
||||
"type": "OBSTACLE_DETECTED",
|
||||
"message": "There is an obstacle in the way."
|
||||
}
|
||||
}
|
||||
}
|
||||
18
test/fixtures/alexa-docs/alexa-safety-errorresponse/SAFETY_BEAM_BREACHED.error.json
vendored
Normal file
18
test/fixtures/alexa-docs/alexa-safety-errorresponse/SAFETY_BEAM_BREACHED.error.json
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"event": {
|
||||
"header": {
|
||||
"namespace": "Alexa.Safety",
|
||||
"name": "ErrorResponse",
|
||||
"messageId": "Unique identifier, preferably a version 4 UUID",
|
||||
"correlationToken": "Opaque correlation token that matches the request",
|
||||
"payloadVersion": "3"
|
||||
},
|
||||
"endpoint": {
|
||||
"endpointId": "Endpoint ID"
|
||||
},
|
||||
"payload": {
|
||||
"type": "SAFETY_BEAM_BREACHED",
|
||||
"message": "The safety beam was breached."
|
||||
}
|
||||
}
|
||||
}
|
||||
18
test/fixtures/types.ts
vendored
18
test/fixtures/types.ts
vendored
|
|
@ -156,6 +156,24 @@ const changedNames: string[] = report.event.payload.change.properties.map((prope
|
|||
messages.deferredResponse({ endpointId: "lamp-1", correlationToken: "ct" }).context;
|
||||
// @ts-expect-error the range is two numbers
|
||||
AlexaErrors.valueOutOfRange("too high", { minimumValue: 1 });
|
||||
// @ts-expect-error a temperature has a scale
|
||||
AlexaErrors.temperatureValueOutOfRange("too cold", { minimumValue: 15, maximumValue: 30 });
|
||||
// @ts-expect-error the reason is required
|
||||
AlexaErrors.endpointControlUnavailable("no connectivity package");
|
||||
// @ts-expect-error the mode is one of the page's
|
||||
AlexaErrors.notSupportedInCurrentMode("the lamp shows a color", "PARTY");
|
||||
// @ts-expect-error the state of the battery is required
|
||||
AlexaErrors.notSupportedWithCurrentBatteryChargeState("charging");
|
||||
// @ts-expect-error the resource is required
|
||||
AlexaErrors.insufficientResource("the tank is empty");
|
||||
// @ts-expect-error a type without fields takes the message only
|
||||
AlexaErrors.thermostatIsOff("the thermostat is off", { validRange: { minimumValue: 1, maximumValue: 2 } });
|
||||
// @ts-expect-error not a type of the table
|
||||
AlexaErrors.thermostatIsOn("the thermostat is on");
|
||||
const plainErrors: string[] = [
|
||||
AlexaErrors.endpointUnreachable("the lamp does not answer"), AlexaErrors.obstacleDetected("something is in the way"),
|
||||
AlexaErrors.valueOutOfRange("too high"), AlexaErrors.maintenanceRequired("the bin is full", "EMPTY_BIN"),
|
||||
].map((error) => error.type);
|
||||
const errorNamespace: string = AlexaErrors.setpointsTooClose("too close", { value: 2, scale: "CELSIUS" }).namespace;
|
||||
|
||||
// Handlers: the directives of the interface by name, the payload of each typed by its descriptor
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@
|
|||
const { test } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { AlexaError, AlexaErrors, AlexaErrorType, messages } = require("alex2node");
|
||||
const { ERROR_TYPES } = require("../../dist/cjs/registry/catalog.js");
|
||||
const { doc, examples } = require("../helpers/fixtures");
|
||||
|
||||
const celsius = (value) => ({ value, scale: "CELSIUS" });
|
||||
const camelCase = (type) => type.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
||||
|
||||
test("the namespace follows the type: the interface that documents it, Alexa for the rest", () => {
|
||||
const namespaces = Object.fromEntries(
|
||||
|
|
@ -37,7 +42,6 @@ test("AlexaError: an Error with the type, the message for Alexa, the fields of t
|
|||
});
|
||||
|
||||
test("the helpers: the type, the namespace and the payload fields as the pages spell them", () => {
|
||||
const celsius = (value) => ({ value, scale: "CELSIUS" });
|
||||
const cases = [
|
||||
[AlexaErrors.valueOutOfRange("m", { minimumValue: 1, maximumValue: 10 }),
|
||||
"Alexa", "VALUE_OUT_OF_RANGE", { validRange: { minimumValue: 1, maximumValue: 10 } }],
|
||||
|
|
@ -60,6 +64,10 @@ test("the helpers: the type, the namespace and the payload fields as the pages s
|
|||
[AlexaErrors.bypassNeeded("m", [{ friendlyName: "Front door" }]),
|
||||
"Alexa.SecurityPanelController", "BYPASS_NEEDED", { endpointsNeedingBypass: [{ friendlyName: "Front door" }] }],
|
||||
[AlexaErrors.of("OBSTACLE_DETECTED", "m"), "Alexa.Safety", "OBSTACLE_DETECTED", {}],
|
||||
[AlexaErrors.valueOutOfRange("m"), "Alexa", "VALUE_OUT_OF_RANGE", {}],
|
||||
[AlexaErrors.temperatureValueOutOfRange("m"), "Alexa", "TEMPERATURE_VALUE_OUT_OF_RANGE", {}],
|
||||
[AlexaErrors.requestedSetpointsTooClose("m"), "Alexa.ThermostatController", "REQUESTED_SETPOINTS_TOO_CLOSE", {}],
|
||||
[AlexaErrors.bypassNeeded("m"), "Alexa.SecurityPanelController", "BYPASS_NEEDED", {}],
|
||||
];
|
||||
for (const [error, namespace, type, extra] of cases) {
|
||||
assert.deepEqual(
|
||||
|
|
@ -77,3 +85,92 @@ test("errorResponse: the namespace of the type, or the one the caller names", ()
|
|||
});
|
||||
assert.equal(messages.errorResponse({ ...fields, namespace: "Alexa" }).event.header.namespace, "Alexa");
|
||||
});
|
||||
|
||||
test("every type of the table has a helper under its name in camel case, which takes the message", () => {
|
||||
const types = Object.keys(ERROR_TYPES);
|
||||
assert.equal(types.length, 73);
|
||||
// The fields these types cannot do without
|
||||
const fields = {
|
||||
NOT_SUPPORTED_IN_CURRENT_MODE: ["ECO"],
|
||||
ENDPOINT_CONTROL_UNAVAILABLE: ["UNKNOWN"],
|
||||
NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE: ["CURRENTLY_CHARGING"],
|
||||
INSUFFICIENT_RESOURCE: ["WATER"],
|
||||
};
|
||||
for (const type of types) {
|
||||
const helper = AlexaErrors[camelCase(type)];
|
||||
assert.equal(typeof helper, "function", `AlexaErrors.${camelCase(type)}`);
|
||||
const error = helper("what went wrong", ...(fields[type] || []));
|
||||
assert.ok(error instanceof AlexaError, type);
|
||||
assert.deepEqual(
|
||||
{ type: error.type, alexaMessage: error.alexaMessage, namespace: error.namespace },
|
||||
{ type, alexaMessage: "what went wrong", namespace: ERROR_TYPES[type] }
|
||||
);
|
||||
if (!fields[type]) assert.deepEqual(error.extra, {}, type);
|
||||
}
|
||||
// of() and the two shorter names are the only helpers that are not named after a type
|
||||
assert.deepEqual(
|
||||
Object.keys(AlexaErrors).filter((name) => !types.some((type) => camelCase(type) === name)).sort(),
|
||||
["of", "setpointsTooClose", "temperatureOutOfRange"]
|
||||
);
|
||||
});
|
||||
|
||||
test("the helpers build the examples of the error pages", () => {
|
||||
const open = [
|
||||
{ friendlyName: "side window sensor", endpointId: "Endpoint ID" },
|
||||
{ friendlyName: "front door sensor", endpointId: "Endpoint ID" },
|
||||
{ friendlyName: "water sensor" },
|
||||
];
|
||||
// What the helper of each example is called with after the message
|
||||
const fields = {
|
||||
ENDPOINT_CONTROL_UNAVAILABLE: ["NO_CONNECTIVITY_PACKAGE_ENABLED"],
|
||||
ENDPOINT_LOW_POWER: [5],
|
||||
NOT_SUPPORTED_IN_CURRENT_MODE: ["DEMAND_RESPONSE"],
|
||||
NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE: ["ALREADY_CHARGED_TO_REQUIRED_LEVEL", 75],
|
||||
TEMPERATURE_VALUE_OUT_OF_RANGE: [{ minimumValue: celsius(15), maximumValue: celsius(30) }],
|
||||
VALUE_OUT_OF_RANGE: [{ minimumValue: 0, maximumValue: 100 }],
|
||||
REQUESTED_SETPOINTS_TOO_CLOSE: [celsius(2)],
|
||||
BYPASS_NEEDED: [open],
|
||||
};
|
||||
const pages = [
|
||||
"alexa-errorresponse", "alexa-safety-errorresponse", "alexa-securitypanelcontroller-errorresponse",
|
||||
"alexa-thermostatcontroller-errorresponse",
|
||||
];
|
||||
let compared = 0;
|
||||
for (const page of pages) {
|
||||
for (const example of examples(page, ".error")) {
|
||||
const { header, endpoint, payload } = doc(page, example).event;
|
||||
// The page prints the type of NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE with a space after it
|
||||
const type = payload.type.trim();
|
||||
const error = AlexaErrors[camelCase(type)](payload.message, ...(fields[type] || []));
|
||||
const { event } = messages.errorResponse({
|
||||
endpointId: endpoint.endpointId,
|
||||
messageId: header.messageId,
|
||||
correlationToken: header.correlationToken,
|
||||
type: error.type,
|
||||
message: error.alexaMessage,
|
||||
extra: error.extra,
|
||||
namespace: error.namespace,
|
||||
});
|
||||
assert.deepEqual(event.header, header, `${page} ${example}`);
|
||||
assert.deepEqual(event.payload, { ...payload, type }, `${page} ${example}`);
|
||||
compared += 1;
|
||||
}
|
||||
}
|
||||
assert.equal(compared, 15);
|
||||
});
|
||||
|
||||
test("a helper called without a field it needs, or with a value the page does not have, says what to pass", () => {
|
||||
assert.throws(
|
||||
() => AlexaErrors.endpointControlUnavailable("no connection"),
|
||||
{ name: "TypeError", message: /^AlexaErrors\.endpointControlUnavailable\(\) got undefined as reason: pass one of DEEP_SLEEP_MODE, / }
|
||||
);
|
||||
assert.throws(() => AlexaErrors.notSupportedInCurrentMode("m", "PARTY"), /got "PARTY" as currentDeviceMode: pass one of ASLEEP, COLOR, /);
|
||||
assert.throws(() => AlexaErrors.notSupportedWithCurrentBatteryChargeState("m"), /got undefined as currentChargeState/);
|
||||
assert.throws(() => AlexaErrors.insufficientResource("m", "SOAP"), /got "SOAP" as resourceType: pass one of WATER/);
|
||||
assert.throws(() => AlexaErrors.maintenanceRequired("m", "CLEAN"), /got "CLEAN" as maintenanceAction: pass one of EMPTY_BIN/);
|
||||
assert.throws(
|
||||
() => AlexaErrors.valueOutOfRange("m", { minimumValue: 0 }),
|
||||
/^TypeError: AlexaErrors\.valueOutOfRange\(\) got a validRange without maximumValue: pass both ends, or no range$/
|
||||
);
|
||||
assert.throws(() => AlexaErrors.temperatureOutOfRange("m", { maximumValue: celsius(30) }), /temperatureValueOutOfRange\(\) got a validRange without minimumValue/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue