Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
78 changes: 78 additions & 0 deletions test-helpers/fake-red.js
Original file line number Diff line number Diff line change
@@ -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 };