src/messages builds Response, StateReport, DeferredResponse, ErrorResponse, ChangeReport and the scene, doorbell and simple events from plain values; messageId and times are parameters, so a test compares whole objects. StateBuilder collects properties checked by the descriptors, AlexaError and AlexaErrors carry the payload fields of an error type. AlexaStatusMessage and AlexaErrorResponse move to src/compat and are written on the builders. On the wire, against 1.5.2: a DeferredResponse has no context key, a ChangeReport has no correlationToken, the namespace of an ErrorResponse follows its type, and a ChangeReport without a changed property is not published: send() resolves "" and the bridge reports an error that names the endpoint. 143 tests pass (108 before). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
79 lines
4.8 KiB
JavaScript
79 lines
4.8 KiB
JavaScript
"use strict";
|
|
// AlexaError, the helpers of AlexaErrors and the namespace an ErrorResponse goes under.
|
|
const { test } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const { AlexaError, AlexaErrors, AlexaErrorType, messages } = require("alex2node");
|
|
|
|
test("the namespace follows the type: the interface that documents it, Alexa for the rest", () => {
|
|
const namespaces = Object.fromEntries(
|
|
["ENDPOINT_UNREACHABLE", "THERMOSTAT_IS_OFF", "UNAUTHORIZED", "OBSTACLE_DETECTED", "SAFETY_BEAM_BREACHED", "NOT_A_TYPE", "constructor"]
|
|
.map((type) => [type, messages.errorNamespace(type)])
|
|
);
|
|
assert.deepEqual(namespaces, {
|
|
ENDPOINT_UNREACHABLE: "Alexa",
|
|
THERMOSTAT_IS_OFF: "Alexa.ThermostatController",
|
|
UNAUTHORIZED: "Alexa.SecurityPanelController",
|
|
OBSTACLE_DETECTED: "Alexa.Safety",
|
|
SAFETY_BEAM_BREACHED: "Alexa.Safety",
|
|
NOT_A_TYPE: "Alexa",
|
|
constructor: "Alexa",
|
|
});
|
|
});
|
|
|
|
test("every type of the 1.x enum has a namespace that starts with Alexa", () => {
|
|
for (const type of Object.values(AlexaErrorType)) assert.match(messages.errorNamespace(type), /^Alexa(\.[A-Za-z]+)*$/, type);
|
|
});
|
|
|
|
test("AlexaError: an Error with the type, the message for Alexa, the fields of the payload and the namespace", () => {
|
|
const error = AlexaErrors.of("THERMOSTAT_IS_OFF", "the thermostat is off");
|
|
assert.ok(error instanceof AlexaError && error instanceof Error);
|
|
assert.equal(error.name, "AlexaError");
|
|
assert.equal(error.message, "THERMOSTAT_IS_OFF: the thermostat is off");
|
|
assert.deepEqual(
|
|
{ type: error.type, alexaMessage: error.alexaMessage, extra: error.extra, namespace: error.namespace },
|
|
{ type: "THERMOSTAT_IS_OFF", alexaMessage: "the thermostat is off", extra: {}, namespace: "Alexa.ThermostatController" }
|
|
);
|
|
assert.equal(new AlexaError("THERMOSTAT_IS_OFF", "off", {}, "Alexa").namespace, "Alexa");
|
|
});
|
|
|
|
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 } }],
|
|
[AlexaErrors.temperatureOutOfRange("m", { minimumValue: celsius(15), maximumValue: celsius(30) }),
|
|
"Alexa", "TEMPERATURE_VALUE_OUT_OF_RANGE", { validRange: { minimumValue: celsius(15), maximumValue: celsius(30) } }],
|
|
[AlexaErrors.notSupportedInCurrentMode("m", "COLOR"), "Alexa", "NOT_SUPPORTED_IN_CURRENT_MODE", { currentDeviceMode: "COLOR" }],
|
|
[AlexaErrors.endpointLowPower("m", 5), "Alexa", "ENDPOINT_LOW_POWER", { percentageState: 5 }],
|
|
[AlexaErrors.endpointLowPower("m"), "Alexa", "ENDPOINT_LOW_POWER", {}],
|
|
[AlexaErrors.endpointControlUnavailable("m", "DEEP_SLEEP_MODE"), "Alexa", "ENDPOINT_CONTROL_UNAVAILABLE", { reason: "DEEP_SLEEP_MODE" }],
|
|
[AlexaErrors.notSupportedWithCurrentBatteryChargeState("m", "NOT_CONNECTED_TO_POWER", 75),
|
|
"Alexa", "NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE",
|
|
{ currentChargeState: "NOT_CONNECTED_TO_POWER", currentChargeLevelInPercentage: 75 }],
|
|
[AlexaErrors.notSupportedWithCurrentBatteryChargeState("m", "FULLY_CHARGED"),
|
|
"Alexa", "NOT_SUPPORTED_WITH_CURRENT_BATTERY_CHARGE_STATE", { currentChargeState: "FULLY_CHARGED" }],
|
|
[AlexaErrors.insufficientResource("m", "WATER"), "Alexa", "INSUFFICIENT_RESOURCE", { resourceType: "WATER" }],
|
|
[AlexaErrors.maintenanceRequired("m", "EMPTY_BIN"), "Alexa", "MAINTENANCE_REQUIRED", { maintenanceAction: "EMPTY_BIN" }],
|
|
[AlexaErrors.maintenanceRequired("m"), "Alexa", "MAINTENANCE_REQUIRED", {}],
|
|
[AlexaErrors.setpointsTooClose("m", celsius(2)),
|
|
"Alexa.ThermostatController", "REQUESTED_SETPOINTS_TOO_CLOSE", { minimumTemperatureDelta: celsius(2) }],
|
|
[AlexaErrors.bypassNeeded("m", [{ friendlyName: "Front door" }]),
|
|
"Alexa.SecurityPanelController", "BYPASS_NEEDED", { endpointsNeedingBypass: [{ friendlyName: "Front door" }] }],
|
|
[AlexaErrors.of("OBSTACLE_DETECTED", "m"), "Alexa.Safety", "OBSTACLE_DETECTED", {}],
|
|
];
|
|
for (const [error, namespace, type, extra] of cases) {
|
|
assert.deepEqual(
|
|
{ namespace: error.namespace, type: error.type, alexaMessage: error.alexaMessage, extra: error.extra },
|
|
{ namespace, type, alexaMessage: "m", extra },
|
|
type
|
|
);
|
|
}
|
|
});
|
|
|
|
test("errorResponse: the namespace of the type, or the one the caller names", () => {
|
|
const fields = { endpointId: "thermostat", correlationToken: "ct", messageId: "m", type: "THERMOSTAT_IS_OFF", message: "off" };
|
|
assert.deepEqual(messages.errorResponse(fields).event.header, {
|
|
namespace: "Alexa.ThermostatController", name: "ErrorResponse", messageId: "m", correlationToken: "ct", payloadVersion: "3",
|
|
});
|
|
assert.equal(messages.errorResponse({ ...fields, namespace: "Alexa" }).event.header.namespace, "Alexa");
|
|
});
|