Skip to content

test: Add a virtual device harness and move every test onto it - #260

Merged
RobinBol merged 8 commits into
masterfrom
feature/virtual-device-test-harness
Oct 27, 2026
Merged

RobinBol merged 8 commits into
masterfrom
feature/virtual-device-test-harness

Conversation

@RobinBol

Copy link
Copy Markdown
Collaborator

What

One test harness, createVirtualDevice(), replaces the four ways the suite faked a node. It puts the code under test (zclNode) and a scripted device (virtualDevice) on two nodes linked in memory, logs every frame, lets a test drop, rewrite or inject frames, and enforces Homey's 82-byte ZCL frame limit. Every test now uses it, and docs/testing.md explains how.

Why

Real devices behave too well to reproduce bugs like the readAttributes retry loop. Several existing tests also could not fail: breaking the code they covered left them green.

How

  • A frame reaches the other node after sendFrame resolves, as on Homey, so response timeouts really run.
  • settle() waits for frames in flight and fails on a request/response loop between the nodes.
  • Four bugs on master are todo tests in test/virtualDevice.js. #258 and #259 remove theirs.
  • Tests that could not fail now can, each checked by breaking the library code: waitForResponse: false, mixed manufacturer ids, not answering a Default Response, and assertions inside handlers.
  • Duplicate tests and the tests of the removed MOCK_DEVICES helper are gone. test/manual/xiaomiHacks.js never decoded a frame and is now a real test.

Anything Else

Per-cluster command tests still encode and decode with the same definition, so a wrong command id or argument width passes. Asserting spec bytes per cluster is a follow-up.

Real devices behave too well to reproduce bugs like the readAttributes retry loop.
createVirtualDevice puts the device on its own node behind an in-memory link that
records every frame, enforces Homey's 82-byte ZCL frame limit, and lets a test drop,
rewrite or inject frames.

test/virtualDevice.js keeps four bugs that reproduce on master as todo tests:
the readAttributes loop on unrequested ids (#258), readAttributes([]) on metering
overflowing a frame (#259), Endpoint.unbind leaving a Cluster in bindings, and
attr.undefined emitted for unknown ids in a report.

Refs #258
Refs #259
The suite had four ways to fake a node: createMockNode, createConnectedNodePair,
MOCK_DEVICES and hand-built Node stubs. All of them are now createVirtualDevice,
and test/util/mockNode.js is gone.

Migrating exposed tests that could not fail. They now can, each checked by
breaking the library code it covers:
- waitForResponse: false was passed as command args, so the test waited anyway.
- The mixed-manufacturer tests accepted any error, including one from a binding
  left over from an earlier test.
- Not answering a default response was never reached, because our own default
  responses set Disable Default Response; the device now sends one without it.
- Assertions inside handlers became error responses or hung done callbacks;
  tests now assert after await or settle(), and on wire frames instead of spies.
- The attribute 0x0000 test asserted nothing; it now decodes a noData report.

Duplicates of the ZCLError and Default Response tests are removed, the four
tests of MOCK_DEVICES itself are dropped, and OTA runs the way Homey uses it:
the device requests, the node serves. The harness gains frame meta, eager
parsing and a 1000-frame cap that turns a request/response loop into a failure.
docs/testing.md explains both sides of createVirtualDevice, its options and
return values, recipes for misbehaving devices, and the rules that keep a test
able to fail. AGENTS.md and the README point to it.
test/manual/xiaomiHacks.js registered a Basic cluster that declares its own
data type for Xiaomi's attribute 0xff01, but never decoded a frame, so it
checked nothing. The new test has the virtual device send a report shaped
like the ones Xiaomi devices send and asserts the decoded records. It fails
when the library decodes with the type on the wire instead of the declared
one, which is what such a custom cluster relies on.
Frames were delivered while sendFrame was still running, so every reply
arrived before the command armed its response timeout: setting that timeout
to 0 failed one test. Frames now arrive on the next turn of the event loop,
as on Homey where sendFrame resolves on the APS ACK and the response comes
later. The same mutation now fails 33 tests.

A loop between two handlers only debug-logged the frame cap error, so the
test passed. settle() now rethrows a failed delivery, including the cap.
An interceptor that returns a non-Buffer throws instead of being dropped
silently.

frameToClient() and createSingleClusterDevice() replace the hand-built
headers and per-file single-cluster helpers. frames[].error, which the
node never let through, is gone, and docs/testing.md matches the code.
@RobinBol RobinBol self-assigned this Oct 27, 2026
@github-actions github-actions Bot added the internal Tooling, CI or docs, left out of the release notes label Oct 27, 2026
@RobinBol
RobinBol requested a balanced review from Copilot October 27, 2026 12:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The review identifies no active issues with the virtual-device test-harness migration.

No open issues 🎉

What changed in this PR

This PR unifies node-based tests around an in-memory virtual device harness, improving coverage of Zigbee frame handling without changing production library code.

Changes:

  • Adds paired nodes with asynchronous delivery, frame recording and interception, outgoing frame-size enforcement, and loop detection.
  • Migrates tests from legacy mocks, moves assertions outside handlers, and adds Xiaomi decoding coverage plus four known-bug tests.
  • Documents the harness and removes obsolete helpers and manual test code.
File Description
test/​xiaomiLifeline.js Adds an automated Xiaomi lifeline report-decoding test.
test/​virtualDevice.js Tests harness behavior and records four known bugs as todos.
test/​util/​virtualDevice.js Implements the shared virtual device harness and frame helpers.
test/​util/​mockNode.js Removes legacy mock-node factories and device presets.
test/​util/​index.js Exports the new harness utilities.
test/​testZCLError.js Consolidates error-response tests with explicit status-byte assertions.
test/​testTrxSequenceNumber.js Migrates sequence-number tests to the harness.
test/​testNode.js Migrates node tests and strengthens observable assertions.
test/​testMfgSpecificCommands.js Checks manufacturer headers and mixed-manufacturer rejection.
test/​testClusterResponseTimeout.js Migrates timeout and response-matching scenarios to the harness.
test/​ota.js Models device-initiated OTA requests with the node serving responses.
test/​onOff.js Migrates On/Off command tests to paired nodes.
test/​manual/​xiaomiHacks.js Removes the manual Xiaomi example replaced by automated coverage.
test/​levelControl.js Migrates Level Control command tests.
test/​iasZone.js Migrates commands and notifications; removes preset-only tests.
test/​doorLock.js Migrates command and notification tests; moves assertions outside handlers.
test/​defaultResponse.js Checks response behavior through the virtual link.
test/​colorControl.js Migrates tests while retaining specification-byte checks.
test/​attributeReport.js Verifies emitted values and metadata after delivery completes.
README.md Links contributor guidance to the testing documentation.
docs/​testing.md Documents harness APIs, recipes, rules, and limitations.
AGENTS.md Updates test placement and harness guidance.

Review effort: Balanced

The index only re-exported test/util/virtualDevice.js. docs/adding-clusters.md
still pointed at the removed mock node helpers there; it now links the
testing guide.
The diagram repeated the bullets under it, frameToClient was explained
three times, and the misbehaving-device snippet was plain BoundCluster use.
Option and return value details now live as JSDoc on createVirtualDevice,
next to the code they describe, so they cannot drift from it. The guide
keeps the idea, a table of example tests per scenario, and the rules.
@RobinBol
RobinBol merged commit ddc6682 into master Oct 27, 2026
2 checks passed
@RobinBol
RobinBol deleted the feature/virtual-device-test-harness branch October 27, 2026 12:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

internal Tooling, CI or docs, left out of the release notes

Development

Successfully merging this pull request may close these issues.

2 participants