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>
165 lines
7.7 KiB
JavaScript
165 lines
7.7 KiB
JavaScript
"use strict";
|
|
// The builders of src/messages/build.ts. Given a messageId and the times, a builder returns the same object every
|
|
// time, so each test compares the whole message.
|
|
const { test } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const { messages, MessageError } = require("alex2node");
|
|
const { UUID_V4 } = require("../helpers/harness.js");
|
|
|
|
const AT = "2026-09-28T10:00:00.000Z";
|
|
const messageId = "0c2bd0a4-6c1c-4b7a-9d3e-5f0a3a1b2c3d";
|
|
const powerState = messages.property("Alexa.PowerController", "powerState", "ON", { timeOfSample: AT });
|
|
const brightness = messages.property("Alexa.BrightnessController", "brightness", 40, { timeOfSample: AT, uncertaintyInMilliseconds: 500 });
|
|
|
|
test("response: the event and the state of the endpoint as its context", () => {
|
|
assert.deepEqual(messages.response({ endpointId: "lamp-1", correlationToken: "ct", messageId, context: [powerState, brightness] }), {
|
|
event: {
|
|
header: { namespace: "Alexa", name: "Response", messageId, correlationToken: "ct", payloadVersion: "3" },
|
|
endpoint: { endpointId: "lamp-1" },
|
|
payload: {},
|
|
},
|
|
context: {
|
|
properties: [
|
|
{ namespace: "Alexa.PowerController", name: "powerState", value: "ON", timeOfSample: AT, uncertaintyInMilliseconds: 0 },
|
|
{ namespace: "Alexa.BrightnessController", name: "brightness", value: 40, timeOfSample: AT, uncertaintyInMilliseconds: 500 },
|
|
],
|
|
},
|
|
});
|
|
});
|
|
|
|
test("response: nothing to report is an empty list of properties; an interface names its own response", () => {
|
|
const armed = messages.response({
|
|
namespace: "Alexa.SecurityPanelController", name: "Arm.Response", endpointId: "panel", correlationToken: "ct", messageId,
|
|
payload: { exitDelayInSeconds: 30 },
|
|
});
|
|
assert.deepEqual(armed, {
|
|
event: {
|
|
header: { namespace: "Alexa.SecurityPanelController", name: "Arm.Response", messageId, correlationToken: "ct", payloadVersion: "3" },
|
|
endpoint: { endpointId: "panel" },
|
|
payload: { exitDelayInSeconds: 30 },
|
|
},
|
|
context: { properties: [] },
|
|
});
|
|
});
|
|
|
|
test("stateReport: a Response by the name of StateReport", () => {
|
|
assert.deepEqual(messages.stateReport({ endpointId: "lamp-1", correlationToken: "ct", messageId, context: [powerState] }), {
|
|
event: {
|
|
header: { namespace: "Alexa", name: "StateReport", messageId, correlationToken: "ct", payloadVersion: "3" },
|
|
endpoint: { endpointId: "lamp-1" },
|
|
payload: {},
|
|
},
|
|
context: { properties: [powerState] },
|
|
});
|
|
});
|
|
|
|
test("deferredResponse: no context key, the estimate only when given", () => {
|
|
const event = {
|
|
header: { namespace: "Alexa", name: "DeferredResponse", messageId, correlationToken: "ct", payloadVersion: "3" },
|
|
endpoint: { endpointId: "lamp-1" },
|
|
};
|
|
assert.deepEqual(messages.deferredResponse({ endpointId: "lamp-1", correlationToken: "ct", messageId }), {
|
|
event: { ...event, payload: {} },
|
|
});
|
|
assert.deepEqual(
|
|
messages.deferredResponse({ endpointId: "lamp-1", correlationToken: "ct", messageId, estimatedDeferralInSeconds: 20 }),
|
|
{ event: { ...event, payload: { estimatedDeferralInSeconds: 20 } } }
|
|
);
|
|
});
|
|
|
|
test("errorResponse: the event alone, the fields of the type next to type and message", () => {
|
|
const answer = messages.errorResponse({
|
|
endpointId: "lamp-1", correlationToken: "ct", messageId,
|
|
type: "VALUE_OUT_OF_RANGE", message: "too high", extra: { validRange: { minimumValue: 1, maximumValue: 10 } },
|
|
});
|
|
assert.deepEqual(answer, {
|
|
event: {
|
|
header: { namespace: "Alexa", name: "ErrorResponse", messageId, correlationToken: "ct", payloadVersion: "3" },
|
|
endpoint: { endpointId: "lamp-1" },
|
|
payload: { type: "VALUE_OUT_OF_RANGE", message: "too high", validRange: { minimumValue: 1, maximumValue: 10 } },
|
|
},
|
|
});
|
|
});
|
|
|
|
test("changeReport: what changed in payload.change, the rest in the context, no correlationToken", () => {
|
|
const report = messages.changeReport({ endpointId: "lamp-1", messageId, cause: "APP_INTERACTION", changed: [powerState], context: [brightness] });
|
|
assert.deepEqual(report, {
|
|
event: {
|
|
header: { namespace: "Alexa", name: "ChangeReport", messageId, payloadVersion: "3" },
|
|
endpoint: { endpointId: "lamp-1" },
|
|
payload: { change: { cause: { type: "APP_INTERACTION" }, properties: [powerState] } },
|
|
},
|
|
context: { properties: [brightness] },
|
|
});
|
|
});
|
|
|
|
test("changeReport: a property that changed is not repeated in the context; instances are told apart", () => {
|
|
const lift = (instance, value) => messages.property("Alexa.RangeController", "rangeValue", value, { instance, timeOfSample: AT });
|
|
const report = messages.changeReport({
|
|
endpointId: "blind", messageId,
|
|
changed: [lift("Blind.Lift", 40)],
|
|
context: [lift("Blind.Lift", 30), lift("Blind.Tilt", 5)],
|
|
});
|
|
assert.equal(report.event.payload.change.cause.type, "PHYSICAL_INTERACTION");
|
|
assert.deepEqual(report.context.properties, [lift("Blind.Tilt", 5)]);
|
|
});
|
|
|
|
test("changeReport: without a changed property it is refused, the error names the endpoint and what to do", () => {
|
|
assert.throws(
|
|
() => messages.changeReport({ endpointId: "lamp-1", changed: [], context: [brightness] }),
|
|
(err) => {
|
|
assert.ok(err instanceof MessageError);
|
|
assert.equal(err.endpointId, "lamp-1");
|
|
assert.match(err.message, /^lamp-1: a ChangeReport needs at least one property that changed/);
|
|
assert.match(err.message, /send no report when nothing changed/);
|
|
return true;
|
|
}
|
|
);
|
|
});
|
|
|
|
test("sceneEvent: ActivationStarted and DeactivationStarted with the cause and the time", () => {
|
|
const started = messages.sceneEvent({ endpointId: "scene-1", correlationToken: "ct", messageId, activated: true, timestamp: AT });
|
|
assert.deepEqual(started, {
|
|
event: {
|
|
header: { namespace: "Alexa.SceneController", name: "ActivationStarted", messageId, correlationToken: "ct", payloadVersion: "3" },
|
|
endpoint: { endpointId: "scene-1" },
|
|
payload: { cause: { type: "VOICE_INTERACTION" }, timestamp: AT },
|
|
},
|
|
context: {},
|
|
});
|
|
const stopped = messages.sceneEvent({
|
|
endpointId: "scene-1", correlationToken: "ct", messageId, activated: false, cause: "APP_INTERACTION", timestamp: new Date(AT),
|
|
});
|
|
assert.equal(stopped.event.header.name, "DeactivationStarted");
|
|
assert.deepEqual(stopped.event.payload, { cause: { type: "APP_INTERACTION" }, timestamp: AT });
|
|
});
|
|
|
|
test("doorbellPress: an event without a correlationToken and without a context", () => {
|
|
assert.deepEqual(messages.doorbellPress({ endpointId: "door", messageId, timestamp: AT }), {
|
|
event: {
|
|
header: { namespace: "Alexa.DoorbellEventSource", name: "DoorbellPress", messageId, payloadVersion: "3" },
|
|
endpoint: { endpointId: "door" },
|
|
payload: { cause: { type: "PHYSICAL_INTERACTION" }, timestamp: AT },
|
|
},
|
|
});
|
|
});
|
|
|
|
test("simpleEvent: the instance in the header, payloadVersion 1.0", () => {
|
|
assert.deepEqual(messages.simpleEvent({ endpointId: "button", instance: "Button.Front", id: "Button.SinglePush.1", messageId, timestamp: AT }), {
|
|
event: {
|
|
header: { namespace: "Alexa.SimpleEventSource", name: "Event", instance: "Button.Front", messageId, payloadVersion: "1.0" },
|
|
endpoint: { endpointId: "button" },
|
|
payload: { id: "Button.SinglePush.1", timestamp: AT },
|
|
},
|
|
});
|
|
});
|
|
|
|
test("left out, the messageId is a new version 4 UUID and the time is now", () => {
|
|
const before = Date.now();
|
|
const first = messages.doorbellPress({ endpointId: "door" });
|
|
const second = messages.doorbellPress({ endpointId: "door" });
|
|
assert.match(first.event.header.messageId, UUID_V4);
|
|
assert.notEqual(first.event.header.messageId, second.event.header.messageId);
|
|
const at = Date.parse(first.event.payload.timestamp);
|
|
assert.ok(at >= before && at <= Date.now(), first.event.payload.timestamp);
|
|
});
|