The interface registry, generated and checked discovery, typed dispatch with automatic error answers, message builders, proactive events and typed helpers for every error type; the 1.x API is kept and the examples of 1.5.2 run unchanged. docs/wire-changes.md lists what a 1.x caller can observe. On 2026-09-28 a real Alexa account drove 33 test devices on this API through the public Alex2MQTT service: Alexa accepted the discovery of all of them, 50 of 58 cases passed, 4 had no action in the control call and 4 failed on what the check could see, none on an answer of the library. The readme table names the interfaces that run covered. Voice commands were not tested yet. 307 tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
88 lines
4.9 KiB
JavaScript
88 lines
4.9 KiB
JavaScript
"use strict";
|
|
// readme.md says what the code does: its table of interfaces is the one the registry gives, a program it shows is
|
|
// the text of a file that a test runs, and the other code blocks compile.
|
|
const { test } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const { run, ROOT } = require("./helpers/examples.js");
|
|
const { table, update, shown, BEGIN, END, DRIVEN, README } = require("../scripts/capability-table.js");
|
|
const alex2node = require("..");
|
|
|
|
const readme = fs.readFileSync(README, "utf8");
|
|
const AsyncFunction = (async () => {}).constructor;
|
|
|
|
// The js code blocks, each with the file its marker line names
|
|
const blocks = [...readme.matchAll(/(?:<!-- file: (\S+) -->\n)?```js\n([\s\S]*?)\n```/g)]
|
|
.map(([, file, code]) => ({ file, code }));
|
|
const loads = (code) => /require\("alex2node"\)|from "alex2node"/.test(code);
|
|
|
|
test("the table of interfaces and the files in the readme are the ones of the code: npm run docs", () => {
|
|
assert.equal(update(readme), readme);
|
|
});
|
|
|
|
test("the table has a row for every described interface, and a line for the others", () => {
|
|
const text = table();
|
|
for (const { namespace, tier } of alex2node.registry.list()) {
|
|
const row = text.split("\n").filter((line) => line.startsWith(`| [${namespace}](`));
|
|
assert.equal(row.length, tier === 3 ? 0 : 1, namespace);
|
|
assert.ok(text.includes(`[${namespace}](`), namespace);
|
|
}
|
|
assert.ok(readme.indexOf(BEGIN) < readme.indexOf(END));
|
|
});
|
|
|
|
test("an interface is said to be driven through Alexa only when a run is recorded for it", () => {
|
|
// The run of 2026-09-28 with 33 test devices on the 2.0 API. A name is added here with the record of its run.
|
|
assert.deepEqual(Object.keys(DRIVEN).sort(), [
|
|
"Alexa", "Alexa.BrightnessController", "Alexa.ColorController", "Alexa.ColorTemperatureController",
|
|
"Alexa.ContactSensor", "Alexa.DoorbellEventSource", "Alexa.InventoryLevelSensor", "Alexa.LockController",
|
|
"Alexa.ModeController", "Alexa.MotionSensor", "Alexa.PercentageController", "Alexa.PowerController",
|
|
"Alexa.PowerLevelController", "Alexa.RangeController", "Alexa.SceneController",
|
|
"Alexa.SecurityPanelController", "Alexa.SimpleEventSource", "Alexa.TemperatureSensor",
|
|
"Alexa.ThermostatController", "Alexa.ThermostatController.Schedule", "Alexa.ToggleController",
|
|
]);
|
|
const rows = table().split("\n").filter((line) => line.startsWith("| [Alexa"));
|
|
for (const line of rows) {
|
|
const cells = line.split(" | ");
|
|
const namespace = cells[0].slice(3, cells[0].indexOf("]"));
|
|
assert.equal(cells[cells.length - 1].replace(/ \|$/, ""), DRIVEN[namespace] ?? "-", namespace);
|
|
}
|
|
});
|
|
|
|
test("a program in the readme is a file, and a test runs the file", () => {
|
|
assert.deepEqual(shown(readme), ["examples/lamp.js", "examples/testing/lamp.test.js", "examples/legacy/ExamplePowerController.js"]);
|
|
// More than the line that loads the library: a program, which has to be one of the files
|
|
const programs = blocks.filter(({ code }) => loads(code) && code.split("\n").length > 1);
|
|
assert.deepEqual(programs.map(({ file }) => file), shown(readme));
|
|
|
|
const examples = fs.readFileSync(path.join(__dirname, "examples.test.js"), "utf8");
|
|
assert.ok(examples.includes('start("lamp.js"'));
|
|
assert.ok("ExamplePowerController.js" in require("./fixtures/legacy-examples/discovery-1.5.2.json"));
|
|
// examples/testing/lamp.test.js: the next test
|
|
});
|
|
|
|
test("examples/testing/lamp.test.js passes, without a broker", async () => {
|
|
// LOOPBACK_PORT 1, where nothing listens: a connection would fail
|
|
const example = await run(path.join("testing", "lamp.test.js"));
|
|
assert.equal(await example.exited, 0, example.output());
|
|
assert.match(example.output(), /pass 4\n/);
|
|
assert.match(example.output(), /fail 0\n/);
|
|
});
|
|
|
|
test("the other code blocks of the readme compile, and load what the library exports", () => {
|
|
const fragments = blocks.filter(({ file }) => !file);
|
|
assert.ok(fragments.length >= 8);
|
|
for (const { code } of fragments) {
|
|
for (const [, names] of code.matchAll(/(?:const|import) \{([^}]+)\} (?:= require\("alex2node"\)|from "alex2node")/g)) {
|
|
for (const name of names.split(",").map((part) => part.trim())) assert.ok(name in alex2node, `${name} is not an export`);
|
|
}
|
|
const body = code.split("\n").filter((line) => !/^import .* from "alex2node";$/.test(line)).join("\n");
|
|
assert.doesNotThrow(() => new AsyncFunction(body), `this block does not compile:\n${code}`);
|
|
}
|
|
});
|
|
|
|
test("what the readme calls on AlexaErrors and on the registry is there", () => {
|
|
for (const [, helper] of readme.matchAll(/AlexaErrors\.(\w+)\(/g)) assert.equal(typeof alex2node.AlexaErrors[helper], "function", helper);
|
|
for (const [, namespace] of readme.matchAll(/registry\.get\("([^"]+)"\)/g)) assert.ok(alex2node.registry.has(namespace), namespace);
|
|
assert.equal(Object.keys(require("../dist/cjs/registry/catalog.js").ERROR_TYPES).length, 73);
|
|
});
|