Alex2Node/test
David 785944b2da 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>
2026-09-28 22:04:03 +00:00
..
compat device: the client of new Device() and setMqttClient() warn once 2026-09-28 21:19:10 +00:00
device device: changeReport(cause, fill) sends a ChangeReport on the 2.0 API 2026-09-28 21:37:37 +00:00
dispatch capability: on() without a function says what it got and what to pass 2026-09-28 21:19:31 +00:00
fixtures examples: one runnable file per recipe on the 2.0 API, the 1.x files in examples/legacy 2026-09-28 21:38:07 +00:00
helpers examples: one runnable file per recipe on the 2.0 API, the 1.x files in examples/legacy 2026-09-28 21:38:07 +00:00
messages errors: typed helpers for every documented error type 2026-09-28 21:14:55 +00:00
registry messages: DoorbellPress is sent with the empty context of its page 2026-09-28 21:08:10 +00:00
build.test.js build: refuse unknown arguments; name the fix when typescript is missing 2026-09-28 14:42:33 +00:00
change-report.test.js test: read an ErrorResponse off the broker; every answer has its own messageId 2026-09-28 14:26:44 +00:00
connection.test.js bridge: publish through a Publisher; devices no longer hold the broker client 2026-09-28 19:34:39 +00:00
directives.test.js test: read an ErrorResponse off the broker; every answer has its own messageId 2026-09-28 14:26:44 +00:00
discovery.test.js registry: describe an Alexa interface as data 2026-09-28 15:18:22 +00:00
examples.test.js examples: one runnable file per recipe on the 2.0 API, the 1.x files in examples/legacy 2026-09-28 21:38:07 +00:00
packaging.test.js test: find the imports of the builds with the compiler's scanner 2026-09-28 14:44:34 +00:00
packaging.test.mjs discovery: generate capability JSON from the registry 2026-09-28 15:35:46 +00:00
readme.test.js readme: quick start, generated capability table, recipes, bridge contract, migration 2026-09-28 22:04:03 +00:00
subscriptions.test.js bridge: subscribe to discover and +/alexaDirective only 2026-09-28 19:36:09 +00:00
transport.test.js bridge: a discovery request that arrives twice is answered once 2026-09-28 21:16:50 +00:00
typings.test.js build: dual CJS/ESM output, ES2020 target, Node 18 floor 2026-09-28 13:58:17 +00:00

"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 runs of 2026-09-28, with alex2node 1.5.2. 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.LockController", "Alexa.ModeController", "Alexa.MotionSensor",
    "Alexa.PercentageController", "Alexa.PowerController", "Alexa.PowerLevelController", "Alexa.RangeController",
    "Alexa.SceneController", "Alexa.TemperatureSensor", "Alexa.ThermostatController", "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);
});