From 4a3dd81bcf93fb33bbbba12cfe38c8a14ee7c45d Mon Sep 17 00:00:00 2001 From: David Date: Mon, 28 Sep 2026 21:08:10 +0000 Subject: [PATCH] messages: DoorbellPress is sent with the empty context of its page proactiveEvent() built every proactive event without a context, so doorbellPress() and device.raise(DoorbellEventSource, "DoorbellPress") left out the "context": {} that the example of alexa-doorbelleventsource.html has. The Event of Alexa.SimpleEventSource is printed without one, so the event descriptor says which it is (emptyContext) and raise() passes it on to the builder. A raised DoorbellPress is now { event, context: {} }, 13 bytes longer; the Event of a button is unchanged. The tests compare the keys and the context of the built message with the fixture of the page, for the builder, the descriptor and raise(). 267 tests pass. Co-Authored-By: Claude Fable 5.1 --- dist/cjs/device/Device.js | 1 + dist/cjs/messages/build.js | 11 +++++++++-- dist/cjs/registry/interfaces/DoorbellEventSource.js | 2 +- dist/esm/device/Device.js | 1 + dist/esm/messages/build.d.ts | 8 +++++++- dist/esm/messages/build.js | 11 +++++++++-- dist/esm/messages/types.d.ts | 2 ++ dist/esm/registry/interfaces/DoorbellEventSource.js | 2 +- dist/esm/registry/types.d.ts | 2 ++ dist/types/messages/build.d.ts | 8 +++++++- dist/types/messages/types.d.ts | 2 ++ dist/types/registry/types.d.ts | 2 ++ src/device/Device.ts | 1 + src/messages/build.ts | 13 +++++++++++-- src/messages/types.ts | 2 ++ src/registry/interfaces/DoorbellEventSource.ts | 2 +- src/registry/types.ts | 2 ++ test/device/raise.test.js | 13 ++++++++++--- test/messages/build.test.js | 11 ++++++++--- test/registry/events.test.js | 8 +++++++- 20 files changed, 86 insertions(+), 18 deletions(-) diff --git a/dist/cjs/device/Device.js b/dist/cjs/device/Device.js index aec0758..486c70c 100644 --- a/dist/cjs/device/Device.js +++ b/dist/cjs/device/Device.js @@ -181,6 +181,7 @@ class Device extends events_1.EventEmitter { instance, payloadVersion: event.payloadVersion ?? capability.descriptor.version, payload: checked, + emptyContext: event.emptyContext, }); const bytes = Buffer.byteLength(JSON.stringify(message)); if (bytes > topics.EVENT_BYTES) diff --git a/dist/cjs/messages/build.js b/dist/cjs/messages/build.js index 161e227..6689aee 100644 --- a/dist/cjs/messages/build.js +++ b/dist/cjs/messages/build.js @@ -113,7 +113,11 @@ function sceneEvent(fields) { context: {}, }; } -/** An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. */ +/** + * An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. The + * pages differ on the context, so the caller says which it is: DoorbellPress is printed with an empty one + * (alexa-doorbelleventsource.html), the Event of Alexa.SimpleEventSource without. + */ function proactiveEvent(fields) { const { namespace, name, instance, messageId, payloadVersion, payload } = fields; return { @@ -122,13 +126,16 @@ function proactiveEvent(fields) { endpoint: { endpointId: fields.endpointId }, payload, }, + ...(fields.emptyContext ? { context: {} } : {}), }; } /** Somebody rang (alexa-doorbelleventsource.html). */ function doorbellPress(fields) { const { endpointId, messageId } = fields; const payload = { cause: { type: fields.cause ?? "PHYSICAL_INTERACTION" }, timestamp: (0, property_js_1.isoTime)(fields.timestamp) }; - return proactiveEvent({ endpointId, messageId, namespace: "Alexa.DoorbellEventSource", name: "DoorbellPress", payload }); + return proactiveEvent({ + endpointId, messageId, namespace: "Alexa.DoorbellEventSource", name: "DoorbellPress", payload, emptyContext: true, + }); } /** An event of a button or a sensor that routines start on (alexa-simpleeventsource.html). */ function simpleEvent(fields) { diff --git a/dist/cjs/registry/interfaces/DoorbellEventSource.js b/dist/cjs/registry/interfaces/DoorbellEventSource.js index 661f6e9..d339bc8 100644 --- a/dist/cjs/registry/interfaces/DoorbellEventSource.js +++ b/dist/cjs/registry/interfaces/DoorbellEventSource.js @@ -18,7 +18,7 @@ exports.DoorbellEventSource = (0, types_js_1.defineInterface)({ properties: {}, directives: {}, events: { - DoorbellPress: { name: "DoorbellPress", payload: (0, events_js_1.happened)("PHYSICAL_INTERACTION"), topic: "proactive" }, + DoorbellPress: { name: "DoorbellPress", payload: (0, events_js_1.happened)("PHYSICAL_INTERACTION"), topic: "proactive", emptyContext: true }, }, // The capability is the announcement that the endpoint raises the event: proactivelyReported whatever was declared discovery: () => ({ properties: false, topLevel: { proactivelyReported: true } }), diff --git a/dist/esm/device/Device.js b/dist/esm/device/Device.js index 14f11e3..1a49bbd 100644 --- a/dist/esm/device/Device.js +++ b/dist/esm/device/Device.js @@ -146,6 +146,7 @@ class Device extends EventEmitter { instance, payloadVersion: event.payloadVersion ?? capability.descriptor.version, payload: checked, + emptyContext: event.emptyContext, }); const bytes = Buffer.byteLength(JSON.stringify(message)); if (bytes > topics.EVENT_BYTES) diff --git a/dist/esm/messages/build.d.ts b/dist/esm/messages/build.d.ts index bf44eff..5f327e5 100644 --- a/dist/esm/messages/build.d.ts +++ b/dist/esm/messages/build.d.ts @@ -88,8 +88,14 @@ export interface ProactiveEventFields extends Envelope { /** Default: "3". */ payloadVersion?: string; payload: Record; + /** true: the message has "context": {}, as the page of the event prints it. Default: no context. */ + emptyContext?: boolean; } -/** An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. */ +/** + * An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. The + * pages differ on the context, so the caller says which it is: DoorbellPress is printed with an empty one + * (alexa-doorbelleventsource.html), the Event of Alexa.SimpleEventSource without. + */ export declare function proactiveEvent(fields: ProactiveEventFields): ProactiveEventMessage; export interface DoorbellPressFields extends Envelope { /** Default: "PHYSICAL_INTERACTION", somebody pressed the button. */ diff --git a/dist/esm/messages/build.js b/dist/esm/messages/build.js index 9159b1d..65527b8 100644 --- a/dist/esm/messages/build.js +++ b/dist/esm/messages/build.js @@ -99,7 +99,11 @@ export function sceneEvent(fields) { context: {}, }; } -/** An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. */ +/** + * An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. The + * pages differ on the context, so the caller says which it is: DoorbellPress is printed with an empty one + * (alexa-doorbelleventsource.html), the Event of Alexa.SimpleEventSource without. + */ export function proactiveEvent(fields) { const { namespace, name, instance, messageId, payloadVersion, payload } = fields; return { @@ -108,13 +112,16 @@ export function proactiveEvent(fields) { endpoint: { endpointId: fields.endpointId }, payload, }, + ...(fields.emptyContext ? { context: {} } : {}), }; } /** Somebody rang (alexa-doorbelleventsource.html). */ export function doorbellPress(fields) { const { endpointId, messageId } = fields; const payload = { cause: { type: fields.cause ?? "PHYSICAL_INTERACTION" }, timestamp: isoTime(fields.timestamp) }; - return proactiveEvent({ endpointId, messageId, namespace: "Alexa.DoorbellEventSource", name: "DoorbellPress", payload }); + return proactiveEvent({ + endpointId, messageId, namespace: "Alexa.DoorbellEventSource", name: "DoorbellPress", payload, emptyContext: true, + }); } /** An event of a button or a sensor that routines start on (alexa-simpleeventsource.html). */ export function simpleEvent(fields) { diff --git a/dist/esm/messages/types.d.ts b/dist/esm/messages/types.d.ts index aa644f3..dc4bfb0 100644 --- a/dist/esm/messages/types.d.ts +++ b/dist/esm/messages/types.d.ts @@ -73,4 +73,6 @@ export interface SceneEventMessage { /** An event a device raises by itself: DoorbellPress, the Event of Alexa.SimpleEventSource. */ export interface ProactiveEventMessage { event: Event; + /** Only with an event whose page prints it: DoorbellPress has an empty one, the Event of a button has none. */ + context?: {}; } diff --git a/dist/esm/registry/interfaces/DoorbellEventSource.js b/dist/esm/registry/interfaces/DoorbellEventSource.js index 20739d8..cb531ad 100644 --- a/dist/esm/registry/interfaces/DoorbellEventSource.js +++ b/dist/esm/registry/interfaces/DoorbellEventSource.js @@ -15,7 +15,7 @@ export const DoorbellEventSource = defineInterface({ properties: {}, directives: {}, events: { - DoorbellPress: { name: "DoorbellPress", payload: happened("PHYSICAL_INTERACTION"), topic: "proactive" }, + DoorbellPress: { name: "DoorbellPress", payload: happened("PHYSICAL_INTERACTION"), topic: "proactive", emptyContext: true }, }, // The capability is the announcement that the endpoint raises the event: proactivelyReported whatever was declared discovery: () => ({ properties: false, topLevel: { proactivelyReported: true } }), diff --git a/dist/esm/registry/types.d.ts b/dist/esm/registry/types.d.ts index 2daf117..82aaea2 100644 --- a/dist/esm/registry/types.d.ts +++ b/dist/esm/registry/types.d.ts @@ -66,6 +66,8 @@ export interface EventDescriptor { payload: Schema; /** response: the answer to a directive. proactive: sent without one. */ topic: "response" | "proactive"; + /** A proactive event that the page prints with "context": {}, DoorbellPress. Default: sent without a context. */ + emptyContext?: boolean; } /** What an interface adds to the capability object in discovery, next to the fields every capability has. */ export interface CapabilityExtras { diff --git a/dist/types/messages/build.d.ts b/dist/types/messages/build.d.ts index bf44eff..5f327e5 100644 --- a/dist/types/messages/build.d.ts +++ b/dist/types/messages/build.d.ts @@ -88,8 +88,14 @@ export interface ProactiveEventFields extends Envelope { /** Default: "3". */ payloadVersion?: string; payload: Record; + /** true: the message has "context": {}, as the page of the event prints it. Default: no context. */ + emptyContext?: boolean; } -/** An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. */ +/** + * An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. The + * pages differ on the context, so the caller says which it is: DoorbellPress is printed with an empty one + * (alexa-doorbelleventsource.html), the Event of Alexa.SimpleEventSource without. + */ export declare function proactiveEvent(fields: ProactiveEventFields): ProactiveEventMessage; export interface DoorbellPressFields extends Envelope { /** Default: "PHYSICAL_INTERACTION", somebody pressed the button. */ diff --git a/dist/types/messages/types.d.ts b/dist/types/messages/types.d.ts index aa644f3..dc4bfb0 100644 --- a/dist/types/messages/types.d.ts +++ b/dist/types/messages/types.d.ts @@ -73,4 +73,6 @@ export interface SceneEventMessage { /** An event a device raises by itself: DoorbellPress, the Event of Alexa.SimpleEventSource. */ export interface ProactiveEventMessage { event: Event; + /** Only with an event whose page prints it: DoorbellPress has an empty one, the Event of a button has none. */ + context?: {}; } diff --git a/dist/types/registry/types.d.ts b/dist/types/registry/types.d.ts index 2daf117..82aaea2 100644 --- a/dist/types/registry/types.d.ts +++ b/dist/types/registry/types.d.ts @@ -66,6 +66,8 @@ export interface EventDescriptor { payload: Schema; /** response: the answer to a directive. proactive: sent without one. */ topic: "response" | "proactive"; + /** A proactive event that the page prints with "context": {}, DoorbellPress. Default: sent without a context. */ + emptyContext?: boolean; } /** What an interface adds to the capability object in discovery, next to the fields every capability has. */ export interface CapabilityExtras { diff --git a/src/device/Device.ts b/src/device/Device.ts index b3169cf..24cb789 100644 --- a/src/device/Device.ts +++ b/src/device/Device.ts @@ -228,6 +228,7 @@ class Device extends EventEmitter { instance, payloadVersion: event.payloadVersion ?? capability.descriptor.version, payload: checked, + emptyContext: event.emptyContext, }); const bytes = Buffer.byteLength(JSON.stringify(message)); if (bytes > topics.EVENT_BYTES) return refuse(`it is ${bytes} bytes as JSON, Alex2MQTT takes ${topics.EVENT_BYTES}`); diff --git a/src/messages/build.ts b/src/messages/build.ts index 1744dc9..2d962e5 100644 --- a/src/messages/build.ts +++ b/src/messages/build.ts @@ -182,9 +182,15 @@ export interface ProactiveEventFields extends Envelope { /** Default: "3". */ payloadVersion?: string; payload: Record; + /** true: the message has "context": {}, as the page of the event prints it. Default: no context. */ + emptyContext?: boolean; } -/** An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. */ +/** + * An event a device raises by itself, of any interface. The header has no correlationToken: nobody asked. The + * pages differ on the context, so the caller says which it is: DoorbellPress is printed with an empty one + * (alexa-doorbelleventsource.html), the Event of Alexa.SimpleEventSource without. + */ export function proactiveEvent(fields: ProactiveEventFields): ProactiveEventMessage { const { namespace, name, instance, messageId, payloadVersion, payload } = fields; return { @@ -193,6 +199,7 @@ export function proactiveEvent(fields: ProactiveEventFields): ProactiveEventMess endpoint: { endpointId: fields.endpointId }, payload, }, + ...(fields.emptyContext ? { context: {} } : {}), }; } @@ -207,7 +214,9 @@ export interface DoorbellPressFields extends Envelope { export function doorbellPress(fields: DoorbellPressFields): ProactiveEventMessage { const { endpointId, messageId } = fields; const payload = { cause: { type: fields.cause ?? "PHYSICAL_INTERACTION" }, timestamp: isoTime(fields.timestamp) }; - return proactiveEvent({ endpointId, messageId, namespace: "Alexa.DoorbellEventSource", name: "DoorbellPress", payload }); + return proactiveEvent({ + endpointId, messageId, namespace: "Alexa.DoorbellEventSource", name: "DoorbellPress", payload, emptyContext: true, + }); } export interface SimpleEventFields extends Envelope { diff --git a/src/messages/types.ts b/src/messages/types.ts index 173064d..da420f9 100644 --- a/src/messages/types.ts +++ b/src/messages/types.ts @@ -70,4 +70,6 @@ export interface SceneEventMessage { /** An event a device raises by itself: DoorbellPress, the Event of Alexa.SimpleEventSource. */ export interface ProactiveEventMessage { event: Event; + /** Only with an event whose page prints it: DoorbellPress has an empty one, the Event of a button has none. */ + context?: {}; } diff --git a/src/registry/interfaces/DoorbellEventSource.ts b/src/registry/interfaces/DoorbellEventSource.ts index 461ad96..bace421 100644 --- a/src/registry/interfaces/DoorbellEventSource.ts +++ b/src/registry/interfaces/DoorbellEventSource.ts @@ -16,7 +16,7 @@ export const DoorbellEventSource = defineInterface({ properties: {}, directives: {}, events: { - DoorbellPress: { name: "DoorbellPress", payload: happened("PHYSICAL_INTERACTION"), topic: "proactive" }, + DoorbellPress: { name: "DoorbellPress", payload: happened("PHYSICAL_INTERACTION"), topic: "proactive", emptyContext: true }, }, // The capability is the announcement that the endpoint raises the event: proactivelyReported whatever was declared discovery: () => ({ properties: false, topLevel: { proactivelyReported: true } }), diff --git a/src/registry/types.ts b/src/registry/types.ts index 4aa144d..3841f42 100644 --- a/src/registry/types.ts +++ b/src/registry/types.ts @@ -61,6 +61,8 @@ export interface EventDescriptor { payload: Schema; /** response: the answer to a directive. proactive: sent without one. */ topic: "response" | "proactive"; + /** A proactive event that the page prints with "context": {}, DoorbellPress. Default: sent without a context. */ + emptyContext?: boolean; } /** What an interface adds to the capability object in discovery, next to the fields every capability has. */ diff --git a/test/device/raise.test.js b/test/device/raise.test.js index 239c918..5f17130 100644 --- a/test/device/raise.test.js +++ b/test/device/raise.test.js @@ -30,7 +30,8 @@ const refused = (raise, problem) => assert.throws(raise, (err) => err instanceof test("raise() publishes the event on /event, with the endpoint, a new messageId, the time and the usual cause", async () => { const { device, sent } = doorbell(); - const printed = doc("alexa-doorbelleventsource", "DoorbellPress.event").event; + const example = doc("alexa-doorbelleventsource", "DoorbellPress.event"); + const printed = example.event; const before = Date.now(); assert.deepEqual(await device.raise(DoorbellEventSource, "DoorbellPress"), { ok: true, topic: "root/event" }); @@ -39,6 +40,10 @@ test("raise() publishes the event on /event, with the endpoint, a new mess assert.equal(topics.event("root"), "root/event"); assert.deepEqual(sent.published.map(({ topic }) => topic), ["root/event", "root/event", "root/event"]); + for (const { message } of sent.published) { + assert.deepEqual(Object.keys(message).sort(), Object.keys(example).sort()); + assert.deepEqual(message.context, example.context); + } const [first, second, third] = sent.published.map(({ message }) => message.event); for (const event of [first, second, third]) { assert.deepEqual(event.endpoint, { endpointId: "door-1" }); @@ -62,7 +67,8 @@ test("raise() for an instance: the Event of a button has the instance in its hea remote.publisher = sent; const top = remote.add(SimpleEventSource, button("TopButton", ["SinglePush", "DoublePush"])); remote.add(SimpleEventSource, button("BottomButton", ["SinglePush"])); - const printed = doc("alexa-simpleeventsource", "Event.event").event; + const example = doc("alexa-simpleeventsource", "Event.event"); + const printed = example.event; await remote.raise(SimpleEventSource, "Event", printed.payload, { instance: top.instance, messageId: "push-1" }); assert.deepEqual(sent.published, [{ @@ -76,6 +82,7 @@ test("raise() for an instance: the Event of a button has the instance in its hea }, }]); assert.equal(printed.header.payloadVersion, "1.0"); + assert.deepEqual(Object.keys(sent.published[0].message), Object.keys(example)); const raised = "remote-1: Alexa.SimpleEventSource.Event was not raised: "; assert.throws(() => remote.raise(SimpleEventSource, "Event", { id: "Button.SinglePush.1" }), { @@ -117,7 +124,7 @@ test("raise() refuses an event the device has no capability for, and one that wo ); refused( () => device.raise(DoorbellEventSource, "DoorbellPress", { picture: "x".repeat(topics.EVENT_BYTES) }), - `${was("Alexa.DoorbellEventSource.DoorbellPress")}it is 16296 bytes as JSON, Alex2MQTT takes 16000` + `${was("Alexa.DoorbellEventSource.DoorbellPress")}it is 16309 bytes as JSON, Alex2MQTT takes 16000` ); assert.deepEqual(sent.published, []); }); diff --git a/test/messages/build.test.js b/test/messages/build.test.js index 6207ef0..249b188 100644 --- a/test/messages/build.test.js +++ b/test/messages/build.test.js @@ -4,6 +4,7 @@ const { test } = require("node:test"); const assert = require("node:assert/strict"); const { messages, MessageError } = require("alex2node"); +const { doc } = require("../helpers/fixtures.js"); const { UUID_V4 } = require("../helpers/harness.js"); const AT = "2026-09-28T10:00:00.000Z"; @@ -134,17 +135,21 @@ test("sceneEvent: ActivationStarted and DeactivationStarted with the cause and t 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 }), { +test("doorbellPress: an event without a correlationToken, with the empty context of the page", () => { + const built = messages.doorbellPress({ endpointId: "door", messageId, timestamp: AT }); + assert.deepEqual(built, { event: { header: { namespace: "Alexa.DoorbellEventSource", name: "DoorbellPress", messageId, payloadVersion: "3" }, endpoint: { endpointId: "door" }, payload: { cause: { type: "PHYSICAL_INTERACTION" }, timestamp: AT }, }, + context: {}, }); + assert.deepEqual(built.context, doc("alexa-doorbelleventsource", "DoorbellPress.event").context); }); -test("simpleEvent: the instance in the header, payloadVersion 1.0", () => { +test("simpleEvent: the instance in the header, payloadVersion 1.0, no context as on the page", () => { + assert.equal("context" in doc("alexa-simpleeventsource", "Event.event"), false); 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" }, diff --git a/test/registry/events.test.js b/test/registry/events.test.js index deac817..bbb6181 100644 --- a/test/registry/events.test.js +++ b/test/registry/events.test.js @@ -30,9 +30,15 @@ test("the events of the pages fit the payload of their descriptor, and come back ["Alexa.WakeOnLANController", "alexa-wakeonlancontroller", "WakeUp", "response"], ]; for (const [namespace, page, name, topic] of printed) { - const { header, payload } = doc(page, `${name}.event`).event; + const example = doc(page, `${name}.event`); + const { header, payload } = example.event; const descriptor = registry.get(namespace); const event = descriptor.events[name]; + // raise() sends a context where the page prints one, and the pages print an empty one or none + if (topic === "proactive") { + assert.equal(event.emptyContext === true, "context" in example, name); + assert.deepEqual(example.context ?? {}, {}, name); + } assert.deepEqual([header.namespace, header.name, header.payloadVersion], [namespace, event.name, descriptor.version]); assert.equal(event.topic, topic, name); assert.equal("correlationToken" in header, topic === "response", name);