From cd044eaa2455050bbfdc8ce4260124974c1b406b Mon Sep 17 00:00:00 2001 From: windkh <10910767+windkh@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:03:32 +0000 Subject: [PATCH] chore: sync repo to shared standard --- AGENTS.md | 13 +++++-- test-helpers/fake-red.js | 78 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 test-helpers/fake-red.js diff --git a/AGENTS.md b/AGENTS.md index 208bd64..3da2ef8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,6 +51,11 @@ - Node's default discovery runs **every** `.js` under `test/`, whatever it is named, so shared helpers and fixtures belong outside that directory (e.g. `test-helpers/`). The test script deliberately takes no path arguments: a `'test/**/*.test.js'` glob would need Node >= 21 and fails on Node 20, which is still supported. +- `test-helpers/fake-red.js` is a minimal Node-RED stand-in shipped by the standard. A node file exports + `function (RED) {…}`, so without a RED object its contents cannot run at all — which is why node files + tend to sit at 0% coverage while the logic extracted from them is well covered. Use it to instantiate a + node and drive its input handler, with `nock` intercepting the requests it makes, so the assertion is + about what actually went to the device rather than what a parser intended. ## Shared: Documentation @@ -61,8 +66,12 @@ ## Shared: Workflow -- CI (`.github/workflows/node.js.yml`) must pass: lint, format:check, test, coverage. -- Releases go through `.github/workflows/npm-publish.yml`. +- CI (`.github/workflows/node.js.yml`) must pass: lint, format:check, test, coverage. The coverage + report is uploaded as a build artifact, so a threshold failure can be inspected from the run. +- Releases go through `.github/workflows/npm-publish.yml`. It re-runs lint, format:check and test in a + `build` job and publishes only on `needs: build` — a release is cut from a tag, and nothing guarantees + that tag points at a commit CI ever saw. `npm publish` is irreversible, so the gate is not optional. +- `.github/workflows/standards-check.yml` runs `nrstd audit` and fails the build on drift from the standard. - Never bump the major version without an ADR explaining the breaking change. ## Shared: package.json scripts diff --git a/test-helpers/fake-red.js b/test-helpers/fake-red.js new file mode 100644 index 0000000..8239813 --- /dev/null +++ b/test-helpers/fake-red.js @@ -0,0 +1,78 @@ +'use strict'; + +// A minimal Node-RED runtime stand-in for unit tests. +// +// Node files (`nodes/*.js`) export `function (RED) { ... }`, so their contents are unreachable +// without a RED object — which is why they tend to sit at 0% coverage while the logic extracted +// out of them is well covered. What stays behind is not trivial: input dispatch, error handling, +// status reporting and the request the node actually issues. +// +// This is deliberately not `node-red-node-test-helper`. That helper starts a real runtime and +// suits flow-level wiring tests; this is ~50 lines, starts nothing, and lets a single node be +// instantiated and driven so its HTTP calls can be intercepted with nock and asserted against +// the device or service API. +// +// const server = makeFakeConfigNode({ ... }); // whatever the node reads off it +// const harness = makeFakeRed(server); +// const MyNode = require('../../nodes/my-node.js')(harness.RED); +// new MyNode({ server: 'server-id', hostname: 'device.test' }); +// await harness.send({ payload: { ... } }); +// assert.equal(harness.errors.length, 0); +// +// Every status/warn/error/send the node performs is captured, so a test asserts on what the +// node tried to do rather than on internals. +function makeFakeRed(configNode) { + const statuses = []; + const warnings = []; + const errors = []; + const sends = []; + + let inputHandler; + let closeHandler; + + const RED = { + nodes: { + createNode: function (node) { + node.status = (s) => statuses.push(s); + node.warn = (m) => warnings.push(m); + // Node-RED passes the msg as a second argument so catch nodes can handle it; + // keep both so a test can assert the message was forwarded. + node.error = (m, msg) => errors.push({ message: m, msg: msg }); + node.send = (m) => sends.push(m); + node.on = function (event, handler) { + if (event === 'input') { + inputHandler = handler; + } else if (event === 'close') { + closeHandler = handler; + } + }; + }, + getNode: function () { + return configNode; + }, + registerType: function () {}, + }, + // Present because node files commonly reach for these at require time. + httpAdmin: { get: function () {}, post: function () {} }, + log: { info: function () {}, warn: function () {}, error: function () {} }, + }; + + return { + RED: RED, + statuses: statuses, + warnings: warnings, + errors: errors, + sends: sends, + // Feeds a message through the node's input handler. Await it: the handler is async in + // most nodes, and without awaiting the assertions run before the request completes. + send: function (msg) { + return inputHandler(msg); + }, + // Drives the close handler so teardown (timers cleared, listeners removed) is testable. + close: function () { + return new Promise((resolve) => closeHandler(resolve)); + }, + }; +} + +module.exports = { makeFakeRed };