readme: quick start, generated capability table, recipes, bridge contract, migration

The readme is rewritten for 2.0: what the library needs, install, the lamp
as quick start, concepts, the table of interfaces, recipes, the bridge
contract, errors, testing without Alexa, migration from 1.x and limits.
scripts/capability-table.js (npm run docs) writes the table from the
registry and the text of three example files into their code blocks;
"Through Alexa" names only the runs of 2026-09-28, made with 1.5.2.
test/readme.test.js fails on a stale table or file, runs
examples/testing/lamp.test.js (MemoryPublisher, no broker) and compiles the
other code blocks. package.json: description, keywords, CHANGELOG.md in files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 22:04:03 +00:00
parent a59de926a2
commit 785944b2da
6 changed files with 848 additions and 174 deletions

View file

@ -31,6 +31,15 @@ to. Then discover the devices in the Alexa app.
`sensor.js` and `doorbell.js` wait for the Enter key: it opens and closes the door, and rings the bell.
`lock.js` takes `BOLT_SECONDS`, the time its bolt needs, 8 by default.
## Test a device without a broker
[testing/lamp.test.js](testing/lamp.test.js) tests a lamp with a `MemoryPublisher` and `bridge.receive()`. It needs
no credentials and connects nowhere:
```sh
node examples/testing/lamp.test.js
```
## What was tested
`test/examples.test.js` starts every file with `node`, against a broker on 127.0.0.1, discovers the device, sends

View file

@ -0,0 +1,83 @@
// A lamp tested without a broker and without Alexa. The bridge publishes into a MemoryPublisher, and
// bridge.receive() gives it a message as the broker would. connect() is never called.
//
// node examples/testing/lamp.test.js
//
// "alex2node" is the installed package; inside this checkout it resolves to the built dist/.
const { test } = require("node:test");
const assert = require("node:assert/strict");
const { Alex2MQTT, MemoryPublisher, PowerController } = require("alex2node");
// The device under test. In your program this is the module that declares your devices on a bridge.
function declareLamp(bridge, lamp) {
const device = bridge.addDevice({ endpointId: "desk-lamp", name: "Desk Lamp", categories: ["LIGHT"] });
const power = device.add(PowerController);
device.state((s) => s.set(power, "powerState", lamp.on ? "ON" : "OFF").health("OK"));
power.on("TurnOn", (ctx) => {
if (lamp.broken) throw new Error("the lamp does not answer");
lamp.on = true;
return ctx.respond();
});
return device;
}
// A bridge that publishes into sent, and what Alex2MQTT would send it
function setup(lamp) {
const sent = new MemoryPublisher();
const bridge = new Alex2MQTT("user", "password", "root", false, { publisher: sent });
declareLamp(bridge, lamp);
let count = 0;
const directive = (namespace, name, payload = {}) => {
count += 1;
return JSON.stringify({
header: { namespace, name, payloadVersion: "3", messageId: `message-${count}`, correlationToken: `token-${count}` },
endpoint: { endpointId: "desk-lamp" },
payload,
});
};
return { sent, bridge, directive };
}
test("discovery announces the lamp", async () => {
const { sent, bridge } = setup({ on: false });
await bridge.receive("root/discover", "{}");
const [{ topic, message }] = sent.published;
assert.equal(topic, "root/discover_r");
assert.equal(message[0].friendlyName, "Desk Lamp");
assert.deepEqual(message[0].capabilities.map((capability) => capability.interface),
["Alexa.PowerController", "Alexa.EndpointHealth", "Alexa"]);
});
test("TurnOn switches the lamp and answers with its state", async () => {
const lamp = { on: false };
const { sent, bridge, directive } = setup(lamp);
await bridge.receive("root/desk-lamp/alexaDirective", directive("Alexa.PowerController", "TurnOn"));
assert.equal(lamp.on, true);
const [{ topic, message }] = sent.published;
assert.equal(topic, "root/desk-lamp/alexaResponce");
assert.equal(message.event.header.name, "Response");
assert.equal(message.event.header.correlationToken, "token-1");
const power = message.context.properties.find((property) => property.name === "powerState");
assert.equal(power.value, "ON");
});
test("a handler that throws is answered with INTERNAL_ERROR", async () => {
const { sent, bridge, directive } = setup({ on: false, broken: true });
const errors = [];
bridge.on("error", (err) => errors.push(err));
await bridge.receive("root/desk-lamp/alexaDirective", directive("Alexa.PowerController", "TurnOn"));
const [{ message }] = sent.published;
assert.equal(message.event.header.name, "ErrorResponse");
assert.equal(message.event.payload.type, "INTERNAL_ERROR");
assert.equal(errors.length, 1);
});
test("a directive without a handler is answered with INVALID_DIRECTIVE", async () => {
const { sent, bridge, directive } = setup({ on: true });
await bridge.receive("root/desk-lamp/alexaDirective", directive("Alexa.PowerController", "TurnOff"));
assert.equal(sent.published[0].message.event.payload.type, "INVALID_DIRECTIVE");
});