bridge: a discovery request that arrives twice is answered once

A broker that mirrors another delivers every message twice, and the bridge
answered both copies of a discovery request. A request is now remembered
by its messageId for 60 s, as a directive is, and its copy is dropped with
a log line. The messageId is read from the header of the Discover
directive or from the request itself when that is the header alone.
A request without a messageId is answered every time, as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
David 2026-09-28 21:16:50 +00:00
parent d12767f9a7
commit 29ee1a315e
9 changed files with 147 additions and 21 deletions

View file

@ -89,3 +89,38 @@ test("a discovery answer that cannot be published: receive() resolves and the \"
await bridge.receive("root/discover", "");
}
});
test("a discovery request that arrives twice is answered once, by its messageId", async () => {
const sent = new MemoryPublisher();
const logged = [];
const bridge = new Alex2MQTT("user", "password", "root", false, { publisher: sent, log: (message) => logged.push(message) });
bridge.addDevice({ endpointId: "lamp-1", name: "Lamp", categories: ["LIGHT"] }).add(PowerController);
const header = (messageId) => ({ namespace: "Alexa.Discovery", name: "Discover", payloadVersion: "3", messageId });
const answers = () => sent.published.filter(({ topic }) => topic === "root/discover_r").length;
// The request as the Discover directive of Alexa, with its header alone, and the header by itself
const requests = [{ directive: { header: header("d-1"), payload: {} } }, { header: header("d-2") }, header("d-3")];
for (const [i, request] of requests.entries()) {
await bridge.receive("root/discover", JSON.stringify(request));
await bridge.receive("root/discover", JSON.stringify(request));
assert.equal(answers(), i + 1, `d-${i + 1}`);
assert.ok(logged.includes(`the discovery request d-${i + 1} is dropped: it arrived before`));
}
// A request without a messageId cannot be told from the next one: each is answered
for (const request of ["", "{}", "not JSON", JSON.stringify({ namespace: "Alexa.Discovery", name: "Discover" })]) {
await bridge.receive("root/discover", request);
await bridge.receive("root/discover", request);
}
assert.equal(answers(), 3 + 8);
// The messageId of a discovery request is not the one of a directive to an endpoint
const called = [];
bridge.getDevice("lamp-1").capability("Alexa.PowerController").on("TurnOn", (ctx) => { called.push(ctx.name); return ctx.respond(); });
await bridge.receive("root/lamp-1/alexaDirective", JSON.stringify({
header: { namespace: "Alexa.PowerController", name: "TurnOn", messageId: "d-1", correlationToken: "ct", payloadVersion: "3" },
endpoint: { endpointId: "lamp-1" },
payload: {},
}));
assert.deepEqual(called, ["TurnOn"]);
});