Skip to content

Add serial and buttplugio integration tests - #86

Closed
heavyrubberslave wants to merge 23 commits into
mainfrom
feat/intergation-tests-serial-and-buttplugio
Closed

heavyrubberslave wants to merge 23 commits into
mainfrom
feat/intergation-tests-serial-and-buttplugio

Conversation

@heavyrubberslave

@heavyrubberslave heavyrubberslave commented Jun 10, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

Release Notes

  • New Features

    • Added comprehensive integration test coverage for Buttplug and SLV Ctrl serial device support.
  • Bug Fixes

    • Enhanced sensor attribute parsing and device refresh behavior for improved stability.
  • Refactor

    • Improved application architecture with streamlined dependency injection and configuration management.
  • Chores

    • Updated dependencies and expanded testing infrastructure.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3c0f58ec-1054-424b-bbf4-d6f8156676ed

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR refactors the server startup architecture to use explicit container creation instead of a service provider, migrates all serial device providers to use SerialPortStream abstractions, and adds comprehensive integration tests with mocked device simulators for both serial and WebSocket providers.

Changes

Server Architecture and Integration Test Refactoring

Layer / File(s) Summary
App Architecture Refactoring
src/app.ts, src/index.ts, src/serviceMap.ts, src/util/expressUtils.ts
Server creation, HTTP/HTTPS attachment, and WebSocket setup move from a deleted ServerServiceProvider into explicit createContainer(dataPath) and createApp(container, options) factories. CORS configuration is centralized in buildCorsOptions(). Service map entries for server.http, server.https, and server.websocket are removed.
Serial Port Abstraction Base Layer
src/device/provider/serialDeviceProvider.ts, src/factory/serialPortFactory.ts
Base class and factory updated to use SerialPortStream<BindingInterface> from @serialport/stream instead of the deprecated SerialPort type across abstract and concrete method signatures.
Device Providers Serial Abstraction
src/device/protocol/{estim2b,slvCtrlPlus,zc95}/...SerialDeviceProvider.ts
All serial device provider implementations updated to accept SerialPortStream<BindingInterface> in connectSerialDevice and preparePort signatures. Imports adjusted and error handling made explicit with null/undefined guards.
Device Implementation Updates
src/device/protocol/buttplugIo/buttplugIoDevice.ts, src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts, src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts
ButtplugIoDevice overrides getRefreshInterval to 100ms; sensor attribute parsing prefers SensorRange for range attributes; SlvCtrlPlusDevice passes explicit timeout to transport.
Dependency Updates
package.json
Bumps @timesplinter/pimple to ^2.1.1 and buttplug to ^3.2.2; adds @serialport/binding-mock, @types/ws, and ws to dev dependencies.
Test Helper Refactoring
tests/integration/helpers/appHelper.ts, tests/integration/helpers/slvCtrlPlusDeviceSimulator.ts, tests/integration/helpers/mockSerialPortFactory.ts, tests/integration/helpers/buttplugIoServerSimulator.ts
Test app construction and lifecycle management refactored to use explicit container and app objects. Introduces SlvCtrlPlusDeviceSimulator for serial protocol simulation and ButtplugIoServerSimulator for WebSocket protocol simulation. MockSerialPortFactory injects simulators into test ports.
New Integration Tests
tests/integration/buttplugIoDevice.spec.ts, tests/integration/slvCtrlSerialDevice.spec.ts
Comprehensive integration test suites verify device detection, HTTP exposure, WebSocket event emission, attribute parsing, and protocol-level behavior for both serial and WebSocket device providers.
Existing Test Updates
tests/integration/api.spec.ts, tests/integration/automationScripts.spec.ts, tests/integration/deviceEvents.spec.ts
All integration tests updated to use { app, container, tmpDir } from the new test helpers and to call connectDevices(container, specs). HTTP requests target app.instance instead of instance.expressApp.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

minor

Poem

🐰 Containers now bloom in the spring refactor,
No more service providers cause coder distraction,
Serial streams flow through each device's vein,
While simulators dance in the test domain—
Websocket and serial, side by side,
Integration tests keep the architecture's pride. 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add serial and buttplugio integration tests' clearly and specifically describes the main change—adding integration tests for serial device and Buttplug I/O providers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/intergation-tests-serial-and-buttplugio

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@heavyrubberslave

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
tests/integration/buttplugIoDevice.spec.ts (2)

173-189: ⚡ Quick win

Test name references implementation detail (100ms interval).

The test name mentions "100ms interval," but this value is not visible in the test configuration. If the polling interval changes in the implementation, the test name will be misleading. Consider either:

  1. Making the interval configurable in the test setup so it's explicit
  2. Removing the specific timing from the test name (e.g., "polls sensor values automatically")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/buttplugIoDevice.spec.ts` around lines 173 - 189, The test
title hardcodes an implementation detail ("100ms interval"); update the spec to
avoid brittle naming by either (A) making the poll interval explicit in the test
setup (e.g., pass a configurable interval into the system under test or
simulator and reference that value in the test name) so the name reflects the
configured value, or (B) rename the spec's it(...) description to remove the
specific timing (for example "polls sensor values automatically" or "polls
sensor values at the configured interval") and leave the rest of the test (the
simulator.addDevice call, device.on(DeviceEvent.deviceRefreshed) wait, and
expect on device.getAttribute) unchanged. Ensure you reference the test's
it(...) block and/or the simulator configuration when applying the change so the
name matches the actual configuration.

142-152: 💤 Low value

Consider replacing the hardcoded delay with a more deterministic approach.

The 200ms setTimeout delay introduces potential flakiness if the test environment is under load. While this may be acceptable for a simulator-based integration test, consider whether the simulator could expose a method to trigger the device addition immediately, or use a retry/poll mechanism instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/buttplugIoDevice.spec.ts` around lines 142 - 152, The test
currently uses a fragile 200ms setTimeout before adding a device; instead make
the addition deterministic by removing the sleep and either (a) triggering the
simulator push immediately (call the simulator method that emits the device-add
event before awaiting waitForDeviceConnected, e.g., invoke simulator.addDevice
prior to awaiting deviceConnected or use a provided simulator.triggerPush
method), or (b) implement an explicit retry/poll loop around
waitForDeviceConnected that repeatedly checks for the device for a bounded time
window. Update the test to call simulator.addDevice and then await
deviceConnected (or replace the sleep with a bounded poll using
waitForDeviceConnected) so the test no longer relies on a hardcoded timeout;
reference helpers: waitForDeviceConnected, simulator.addDevice, and the
deviceConnected variable when making the change.
src/app.ts (1)

36-39: ⚡ Quick win

AppOptions.dataPath is stale and currently unused.

createApp only consumes allowedOrigins (Line 169), while path ownership already lives in createContainer(dataPath). Keeping dataPath in AppOptions makes the API contract misleading.

♻️ Proposed cleanup
--- a/src/app.ts
+++ b/src/app.ts
@@
 export interface AppOptions {
     allowedOrigins: string[];
-    dataPath: string;
 }
--- a/src/index.ts
+++ b/src/index.ts
@@
-const appOptions = { allowedOrigins, dataPath: env.DATA_PATH };
+const appOptions = { allowedOrigins };

Also applies to: 168-170

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app.ts` around lines 36 - 39, AppOptions currently exposes a stale
dataPath field that isn't used by createApp—remove dataPath from the AppOptions
interface and any related type references, and update createApp to only
accept/consume allowedOrigins; rely on createContainer(dataPath) to own path
management. Specifically, delete the dataPath property from the AppOptions
interface declaration (AppOptions), update any call sites or type annotations
that construct or expect AppOptions to stop providing dataPath, and ensure
createApp (the function that reads allowedOrigins) and createContainer(dataPath)
remain the single sources of truth for path ownership.
tests/integration/helpers/mockSerialPortFactory.ts (1)

22-33: Mock serial ports are already registered in the integration test setup, so mockSerialPortFactory doesn’t need createPort(...)

  • tests/integration/slvCtrlSerialDevice.spec.ts calls SerialPortMock.binding.createPort(TEST_PORT_PATH, ...) in beforeAll before announceDetectedDevice(...).
  • src/device/provider/serialDeviceProvider.ts opens the port (port.open(...)) only after serialPortFactory.create(...) runs, so the mock binding exists by the time open is invoked.

Optional: centralize/document that required createPort(...) setup if more integration specs start announcing additional mock port paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/helpers/mockSerialPortFactory.ts` around lines 22 - 33, The
create method currently attempts to register a mock port via
SerialPortMock.binding.createPort(...) even though integration tests already
register mock ports; remove the createPort(...) call from the public override
create(...) implementation in mockSerialPortFactory so the factory only
constructs the SerialPortMock, keeps the open listener that attaches the
simulator (this.simulator.attachToPort(...)), and relies on tests to call
SerialPortMock.binding.createPort(...) in their beforeAll; optionally add a
short comment in create(...) noting that port registration is performed by the
test setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app.ts`:
- Around line 203-204: The code currently calls
websocketServer.attach(httpServer) and websocketServer.attach(httpsServer) which
re-attaches the same Socket.IO Server instance to multiple Node servers;
instead, create separate Socket.IO Server instances for each HTTP/HTTPS server
(e.g., websocketServerHttp and websocketServerHttps) and attach each to its
respective server, replicate any per-server configuration/handlers from the
current websocketServer setup to both instances, and update shutdown logic to
close both servers (or otherwise coordinate disconnects) rather than relying on
a single io.close(); ensure you update any references to websocketServer in
connection/namespace handlers to use the appropriate new instance names.

In `@tests/integration/helpers/buttplugIoServerSimulator.ts`:
- Around line 88-99: The stop() method can hang because it always awaits
Promises whose callbacks never run when this.wss or this.server are null; modify
stop() to only await the close Promises when the corresponding server exists
(i.e., if (this.wss) await new Promise(...), and if (this.server) await new
Promise(...)), or alternatively short-circuit to resolve immediately when they
are null; keep the existing connectedClients clearing and ws.close() loop as-is
and ensure you reference the stop() method, this.wss, and this.server when
making the conditional checks.

---

Nitpick comments:
In `@src/app.ts`:
- Around line 36-39: AppOptions currently exposes a stale dataPath field that
isn't used by createApp—remove dataPath from the AppOptions interface and any
related type references, and update createApp to only accept/consume
allowedOrigins; rely on createContainer(dataPath) to own path management.
Specifically, delete the dataPath property from the AppOptions interface
declaration (AppOptions), update any call sites or type annotations that
construct or expect AppOptions to stop providing dataPath, and ensure createApp
(the function that reads allowedOrigins) and createContainer(dataPath) remain
the single sources of truth for path ownership.

In `@tests/integration/buttplugIoDevice.spec.ts`:
- Around line 173-189: The test title hardcodes an implementation detail ("100ms
interval"); update the spec to avoid brittle naming by either (A) making the
poll interval explicit in the test setup (e.g., pass a configurable interval
into the system under test or simulator and reference that value in the test
name) so the name reflects the configured value, or (B) rename the spec's
it(...) description to remove the specific timing (for example "polls sensor
values automatically" or "polls sensor values at the configured interval") and
leave the rest of the test (the simulator.addDevice call,
device.on(DeviceEvent.deviceRefreshed) wait, and expect on device.getAttribute)
unchanged. Ensure you reference the test's it(...) block and/or the simulator
configuration when applying the change so the name matches the actual
configuration.
- Around line 142-152: The test currently uses a fragile 200ms setTimeout before
adding a device; instead make the addition deterministic by removing the sleep
and either (a) triggering the simulator push immediately (call the simulator
method that emits the device-add event before awaiting waitForDeviceConnected,
e.g., invoke simulator.addDevice prior to awaiting deviceConnected or use a
provided simulator.triggerPush method), or (b) implement an explicit retry/poll
loop around waitForDeviceConnected that repeatedly checks for the device for a
bounded time window. Update the test to call simulator.addDevice and then await
deviceConnected (or replace the sleep with a bounded poll using
waitForDeviceConnected) so the test no longer relies on a hardcoded timeout;
reference helpers: waitForDeviceConnected, simulator.addDevice, and the
deviceConnected variable when making the change.

In `@tests/integration/helpers/mockSerialPortFactory.ts`:
- Around line 22-33: The create method currently attempts to register a mock
port via SerialPortMock.binding.createPort(...) even though integration tests
already register mock ports; remove the createPort(...) call from the public
override create(...) implementation in mockSerialPortFactory so the factory only
constructs the SerialPortMock, keeps the open listener that attaches the
simulator (this.simulator.attachToPort(...)), and relies on tests to call
SerialPortMock.binding.createPort(...) in their beforeAll; optionally add a
short comment in create(...) noting that port registration is performed by the
test setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1836cead-3b6b-4cac-815f-3988e3a67bde

📥 Commits

Reviewing files that changed from the base of the PR and between a62792a and cd3599f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (23)
  • package.json
  • src/app.ts
  • src/device/protocol/buttplugIo/buttplugIoDevice.ts
  • src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts
  • src/device/protocol/estim2b/estim2bSerialDeviceProvider.ts
  • src/device/protocol/slvCtrlPlus/slvCtrlPlusDevice.ts
  • src/device/protocol/slvCtrlPlus/slvCtrlPlusSerialDeviceProvider.ts
  • src/device/protocol/zc95/zc95SerialDeviceProvider.ts
  • src/device/provider/serialDeviceProvider.ts
  • src/factory/serialPortFactory.ts
  • src/index.ts
  • src/serviceMap.ts
  • src/serviceProvider/serverServiceProvider.ts
  • src/util/expressUtils.ts
  • tests/integration/api.spec.ts
  • tests/integration/automationScripts.spec.ts
  • tests/integration/buttplugIoDevice.spec.ts
  • tests/integration/deviceEvents.spec.ts
  • tests/integration/helpers/appHelper.ts
  • tests/integration/helpers/buttplugIoServerSimulator.ts
  • tests/integration/helpers/mockSerialPortFactory.ts
  • tests/integration/helpers/slvCtrlPlusDeviceSimulator.ts
  • tests/integration/slvCtrlSerialDevice.spec.ts
💤 Files with no reviewable changes (2)
  • src/serviceProvider/serverServiceProvider.ts
  • src/serviceMap.ts

Comment thread src/app.ts
Comment thread src/device/protocol/buttplugIo/buttplugIoDeviceFactory.ts
Comment on lines +88 to +99
public async stop(): Promise<void> {
for (const ws of this.connectedClients) {
ws.close();
}
this.connectedClients.clear();

await new Promise<void>((resolve, reject) => {
this.wss?.close(err => (err ? reject(err) : resolve()));
});
await new Promise<void>((resolve, reject) => {
this.server?.close(err => (err ? reject(err) : resolve()));
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent stop() from hanging when simulator wasn’t started.

At Line 94 and Line 97, the Promise resolution depends on callbacks that won’t run when this.wss or this.server is null, causing teardown hangs.

Proposed fix
 public async stop(): Promise<void> {
     for (const ws of this.connectedClients) {
         ws.close();
     }
     this.connectedClients.clear();

-    await new Promise<void>((resolve, reject) => {
-        this.wss?.close(err => (err ? reject(err) : resolve()));
-    });
-    await new Promise<void>((resolve, reject) => {
-        this.server?.close(err => (err ? reject(err) : resolve()));
-    });
+    const wss = this.wss;
+    this.wss = null;
+    if (wss) {
+        await new Promise<void>((resolve, reject) => {
+            wss.close(err => (err ? reject(err) : resolve()));
+        });
+    }
+
+    const server = this.server;
+    this.server = null;
+    if (server) {
+        await new Promise<void>((resolve, reject) => {
+            server.close(err => (err ? reject(err) : resolve()));
+        });
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public async stop(): Promise<void> {
for (const ws of this.connectedClients) {
ws.close();
}
this.connectedClients.clear();
await new Promise<void>((resolve, reject) => {
this.wss?.close(err => (err ? reject(err) : resolve()));
});
await new Promise<void>((resolve, reject) => {
this.server?.close(err => (err ? reject(err) : resolve()));
});
public async stop(): Promise<void> {
for (const ws of this.connectedClients) {
ws.close();
}
this.connectedClients.clear();
const wss = this.wss;
this.wss = null;
if (wss) {
await new Promise<void>((resolve, reject) => {
wss.close(err => (err ? reject(err) : resolve()));
});
}
const server = this.server;
this.server = null;
if (server) {
await new Promise<void>((resolve, reject) => {
server.close(err => (err ? reject(err) : resolve()));
});
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/integration/helpers/buttplugIoServerSimulator.ts` around lines 88 - 99,
The stop() method can hang because it always awaits Promises whose callbacks
never run when this.wss or this.server are null; modify stop() to only await the
close Promises when the corresponding server exists (i.e., if (this.wss) await
new Promise(...), and if (this.server) await new Promise(...)), or alternatively
short-circuit to resolve immediately when they are null; keep the existing
connectedClients clearing and ws.close() loop as-is and ensure you reference the
stop() method, this.wss, and this.server when making the conditional checks.

@heavyrubberslave
heavyrubberslave changed the base branch from feat/integration-tests to main June 19, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant