"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); });