messages: pure builders for every event the bridge sends

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>
This commit is contained in:
David 2026-09-28 19:28:05 +00:00
parent 2558e21787
commit 46fa06728c
68 changed files with 3410 additions and 1208 deletions

View file

@ -1,12 +1,12 @@
import { EventEmitter } from "events";
import { randomUUID } from "crypto";
import type { MqttClient } from "mqtt";
import { AlexaErrorResponse } from "../AlexaErrorResponse.js";
import { AlexaStatusMessage } from "../AlexaStatusMessage.js";
import type { ChangeCause } from "../AlexaStatusMessage.js";
import { AlexaErrorResponse } from "../compat/AlexaErrorResponse.js";
import { AlexaInterface } from "../compat/AlexaInterface.js";
import { AlexaStatusMessage } from "../compat/AlexaStatusMessage.js";
import { DisplayCategory } from "../compat/enums.js";
import type { AlexaInterfaceType } from "../compat/enums.js";
import { sceneEvent } from "../messages/build.js";
import type { ChangeCause } from "../messages/types.js";
import type { DisplayCategoryName } from "../registry/catalog.js";
import { Alexa } from "../registry/interfaces/Alexa.js";
import { EndpointHealth } from "../registry/interfaces/EndpointHealth.js";
@ -149,6 +149,7 @@ class Device extends EventEmitter {
* A proactive Alexa.ChangeReport (1.5.1): add the changed properties (the default target), optionally
* .unchanged() then the others, and .send() - it goes to <root>/changeReport, which Alex2MQTT forwards to the Alexa
* event gateway with the user's token. Alexa only accepts it for capabilities registered with proactivelyReported.
* Without a changed property send() publishes nothing and resolves with "": Alex2MQTT would drop the report.
*/
getChangeReport(cause: ChangeCause = "PHYSICAL_INTERACTION"): AlexaStatusMessage {
const msg = new AlexaStatusMessage("", this.rootTopic, this.endpointId, this.mqttClient, false, false, cause);
@ -160,14 +161,7 @@ class Device extends EventEmitter {
* (1.5.1). Resolves with the topic published to, or "" when the publish failed (never rejects, 1.5.2).
*/
sendSceneResponse(correlationToken: string, activated: boolean, cause: ChangeCause = "VOICE_INTERACTION", sendAsync = false): Promise<string> {
const payload = {
context: {},
event: {
header: { namespace: "Alexa.SceneController", name: activated ? "ActivationStarted" : "DeactivationStarted", messageId: randomUUID(), correlationToken, payloadVersion: "3" },
endpoint: { endpointId: this.endpointId },
payload: { cause: { type: cause }, timestamp: new Date().toISOString() },
},
};
const payload = sceneEvent({ endpointId: this.endpointId, correlationToken, activated, cause });
const topic = `${this.rootTopic}/${this.endpointId}/${sendAsync ? "deferredResponse" : "alexaResponce"}`;
return new Promise((resolve) => {
this.mqttClient.publish(topic, JSON.stringify(payload), (err) => {