Skip to content

perf(discovery): VID/PID-filter serial ports + minimal probe (closes #157)#201

Open
cptkoolbeenz wants to merge 15 commits into
mainfrom
perf/serial-discovery-vid-pid-filter
Open

perf(discovery): VID/PID-filter serial ports + minimal probe (closes #157)#201
cptkoolbeenz wants to merge 15 commits into
mainfrom
perf/serial-discovery-vid-pid-filter

Conversation

@cptkoolbeenz
Copy link
Copy Markdown
Member

Summary

Drops serial discovery from ~1 minute to <1 second on systems with many COM ports, AND fixes the correctness issue of sending SCPI commands to other vendors' devices (Bluetooth radios, GPS receivers, etc.).

Changes (issue priority order)

  1. VID/PID port filtering before opening — new IUsbPortDescriptorProvider abstraction with platform-specific impls:

    • Windows: WindowsUsbPortDescriptorProvider via WMI Win32_PnPEntity
    • Linux: LinuxUsbPortDescriptorProvider via /sys/class/tty
    • macOS / unknown: NullUsbPortDescriptorProvider fallback (preserves legacy probe-everything behavior)

    Ports whose descriptor doesn't match a known DAQiFi VID:PID (0x04D8:0xF794 for CDC mode) are skipped without ever being opened. System.Management added as a Windows-only conditional package.

  2. Reduced probe to GetDeviceInfo only — the previous DisableEcho / StopStreaming / TurnDeviceOn / SetProtobufStreamFormat sequence was for connection setup; identity probing only needs SYSTem:SYSInfoPB?. A healthy DAQiFi answers it regardless of stream format / power state.

  3. Tightened timeouts — wake delay 1000→200ms, response timeout 4000→1000ms, retry interval 1000→300ms, max attempts 3→2. USB CDC is fast; the previous values were defensive against ports we no longer probe at all.

Expected impact

Discovery drops from ~1min → <1s on a typical Windows system. Only DAQiFi-VID/PID ports get probed, each at ~200-500ms vs ~5.4s.

Test plan

  • 7 new tests cover (a) DaqifiUsbIds constants + classifier positive/negative cases including bootloader-PID + CH340-vendor false-positives, (b) NullUsbPortDescriptorProvider contract, (c) end-to-end DiscoverAsync filter — non-DAQiFi VID/PID returns 0 devices in <1s without probing, (d) null-descriptor fall-through preserves legacy behavior
  • All existing SerialDeviceFinderTests still pass
  • Full suite: 897/899 (2 skipped require live hardware)
  • Build clean on net9.0 + net10.0

API

The internal constructor SerialDeviceFinder(int baudRate, IUsbPortDescriptorProvider?) is added for tests; production callers continue to use the existing public constructors which now use a platform-default provider automatically.

Closes #157

…157)

Serial discovery used to take ~1 minute on systems with many COM ports
because every port was opened and probed sequentially with a ~5.4s
worst-case timeout, AND the probe path sent SCPI commands to ports
belonging to other vendors (Bluetooth radios, GPS receivers, etc.).
The desktop USB tab showed an indefinite spinner so users assumed it
was broken and fell back to manual COM entry.

Changes (in priority order from the ticket):

1. **Filter ports by USB VID/PID before opening anything** — adds an
   IUsbPortDescriptorProvider abstraction with platform-specific impls:
   - WindowsUsbPortDescriptorProvider via WMI (Win32_PnPEntity)
   - LinuxUsbPortDescriptorProvider via /sys/class/tty
   - NullUsbPortDescriptorProvider fallback for macOS / unknown
     platforms (preserves legacy probe-everything behavior)
   Ports whose descriptor doesn't match a known DAQiFi VID:PID
   (0x04D8:0xF794 for CDC mode) are skipped without ever being opened.
   System.Management is added as a Windows-only conditional package.

2. **Reduced probe to GetDeviceInfo only** — the previous DisableEcho /
   StopStreaming / TurnDeviceOn / SetProtobufStreamFormat sequence was
   for connection setup; identity probing only needs SYSTem:SYSInfoPB?.
   A healthy DAQiFi answers it regardless of stream format / power state.

3. **Tightened timeouts** — wake delay 1000→200ms, response timeout
   4000→1000ms, retry interval 1000→300ms, max attempts 3→2. USB CDC
   is fast; the previous values were defensive against ports we no
   longer probe at all.

Expected impact: discovery drops from ~1min → <1s on a typical Windows
system (only DAQiFi ports get probed; each at ~200-500ms vs ~5.4s).
The internal constructor takes a custom IUsbPortDescriptorProvider
for tests; production callers use the existing public constructors.

Test plan: 7 new tests cover (a) DaqifiUsbIds constants + classifier
positive/negative cases, (b) NullUsbPortDescriptorProvider contract,
(c) end-to-end DiscoverAsync filter — non-DAQiFi VID/PID returns 0
devices in <1s without probing, (d) null-descriptor fall-through
preserves legacy behavior. 897/899 pass (2 hardware skips); build
clean on net9.0 + net10.0.

Closes #157
@cptkoolbeenz cptkoolbeenz requested a review from a team as a code owner May 12, 2026 05:53
@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Optimize serial discovery with USB VID/PID filtering and minimal probe

✨ Enhancement 🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Filter serial ports by USB VID/PID before probing, reducing discovery from ~1min to <1s
• Implement platform-specific descriptor providers (Windows WMI, Linux sysfs, macOS fallback)
• Simplify probe sequence to GetDeviceInfo only, removing unnecessary setup commands
• Tighten timeouts and retry logic for faster USB CDC enumeration
• Add comprehensive tests for VID/PID filtering and descriptor classification
Diagram
flowchart LR
  A["Serial Ports"] --> B["Name Filter"]
  B --> C["USB Descriptor Provider"]
  C --> D{DAQiFi VID/PID?}
  D -->|Yes| E["Probe GetDeviceInfo"]
  D -->|No| F["Skip Port"]
  D -->|Unknown| E
  E --> G["Device Found"]
  F --> H["Discovery Complete"]
  G --> H
Loading

Grey Divider

File Changes

1. src/Daqifi.Core/Device/Discovery/DaqifiUsbIds.cs ✨ Enhancement +35/-0

Define DAQiFi USB vendor and product IDs

src/Daqifi.Core/Device/Discovery/DaqifiUsbIds.cs


2. src/Daqifi.Core/Device/Discovery/IUsbPortDescriptorProvider.cs ✨ Enhancement +32/-0

Add USB descriptor provider abstraction interface

src/Daqifi.Core/Device/Discovery/IUsbPortDescriptorProvider.cs


3. src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs ✨ Enhancement +68/-0

Implement Windows WMI-based USB descriptor lookup

src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs


View more (7)
4. src/Daqifi.Core/Device/Discovery/LinuxUsbPortDescriptorProvider.cs ✨ Enhancement +66/-0

Implement Linux sysfs-based USB descriptor lookup

src/Daqifi.Core/Device/Discovery/LinuxUsbPortDescriptorProvider.cs


5. src/Daqifi.Core/Device/Discovery/NullUsbPortDescriptorProvider.cs ✨ Enhancement +15/-0

Add fallback no-op descriptor provider for unsupported platforms

src/Daqifi.Core/Device/Discovery/NullUsbPortDescriptorProvider.cs


6. src/Daqifi.Core/Device/Discovery/UsbPortDescriptorProviderFactory.cs ✨ Enhancement +30/-0

Create platform-appropriate descriptor provider factory

src/Daqifi.Core/Device/Discovery/UsbPortDescriptorProviderFactory.cs


7. src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs ✨ Enhancement +80/-22

Add VID/PID filtering and reduce probe to GetDeviceInfo only

src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs


8. src/Daqifi.Core/Daqifi.Core.csproj Dependencies +8/-0

Add System.Management package for Windows WMI support

src/Daqifi.Core/Daqifi.Core.csproj


9. src/Daqifi.Core.Tests/Device/Discovery/UsbPortDescriptorTests.cs 🧪 Tests +47/-0

Test USB ID constants and descriptor classification logic

src/Daqifi.Core.Tests/Device/Discovery/UsbPortDescriptorTests.cs


10. src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs 🧪 Tests +63/-0

Add tests for VID/PID filtering and null descriptor fallback

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (2)

Context used

Grey Divider


Action required

1. Cancellation swallowed in probes ✓ Resolved 🐞 Bug ☼ Reliability
Description
ProbeSafelyAsync catches OperationCanceledException caused by the caller’s CancellationToken and
returns null, so DiscoverAsync can complete successfully (and raise DiscoveryCompleted) even though
cancellation was requested. This contradicts the method’s own XML/comment that cancellation
propagates via Task.WhenAll and can cause callers to treat a canceled discovery as a real empty
result.
Code

src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[R194-198]

+        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+        {
+            // Caller asked to stop — let WhenAll observe the cancellation.
+            return null;
+        }
Evidence
DiscoverAsync awaits Task.WhenAll(probeTasks) and never checks/throws for cancellation
afterward, so the only way cancellation reaches the caller is if probe tasks actually transition to
Canceled/throw. But ProbeSafelyAsync converts caller-token cancellation into a null result,
making WhenAll complete successfully and letting discovery return normally.

src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[111-156]
src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[182-207]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`ProbeSafelyAsync` currently swallows `OperationCanceledException` when the caller token is canceled and returns `null`. That prevents cancellation from propagating to `DiscoverAsync`, contradicting the method’s own docs/comments and changing cancellation semantics.

### Issue Context
This affects the new parallel-probe pipeline: `DiscoverAsync` awaits `Task.WhenAll(probeTasks)` and then processes results; if probe tasks convert cancellation into `null`, the outer method returns normally.

### Fix Focus Areas
- src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[182-207]

### Implementation notes
- Change the `catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)` block to `throw;` (or remove the catch entirely).
- Keep the general `catch { return null; }` for non-cancellation exceptions if the intent is to tolerate per-port probe failures.
- Ensure the XML/doc comment for `ProbeSafelyAsync` matches the actual behavior (after the fix, cancellation will propagate via `Task.WhenAll`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Null descriptor still probes ports 📎 Requirement gap ≡ Correctness
Description
The new test codifies that when GetDescriptor() returns null, discovery falls through to legacy
probing, which can open/send commands to ports without a confirmed DAQiFi VID/PID. This violates the
requirement that only VID/PID-matched DAQiFi ports are probed and others receive no traffic.
Code

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[R149-151]

+        // The legacy probe path may exceed 200ms on machines with real ports —
+        // an OperationCanceledException there still proves the contract:
+        // null descriptors fall through to probing rather than being filtered.
Evidence
PR Compliance ID 1 requires that only ports matching known DAQiFi VID/PID values are opened/probed.
The added test explicitly states and accepts that null descriptors "fall through to probing rather
than being filtered," meaning ports without a confirmed DAQiFi VID/PID may still be probed.

Filter serial ports by DAQiFi USB VID/PID before probing
src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[149-151]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Current behavior (and test expectation) allows `null` USB descriptors to fall through to probing, which can open and send DAQiFi/SCPI commands to non-DAQiFi ports when VID/PID cannot be resolved.

## Issue Context
PR Compliance requires serial discovery to avoid probing non-DAQiFi ports by filtering on known DAQiFi VID/PID values before any port is opened.

## Fix Focus Areas
- src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[149-160]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Non-Windows build may fail ✓ Resolved 🐞 Bug ≡ Correctness
Description
Daqifi.Core.csproj references System.Management only when $(OS) == Windows_NT, but
WindowsUsbPortDescriptorProvider is compiled unconditionally and directly uses System.Management
types. This can cause compilation failures on non-Windows build agents (or any environment where
System.Management isn’t otherwise available) even though runtime guards prevent execution.
Code

src/Daqifi.Core/Daqifi.Core.csproj[R42-44]

+  <ItemGroup Condition="'$(OS)' == 'Windows_NT'">
+    <PackageReference Include="System.Management" Version="10.0.7" />
+  </ItemGroup>
Evidence
The csproj only brings in System.Management on Windows, while the Windows descriptor provider uses
System.Management APIs in compiled code, so compilation depends on that reference being present.

src/Daqifi.Core/Daqifi.Core.csproj[3-45]
src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[1-68]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`WindowsUsbPortDescriptorProvider` directly references `System.Management`, but the package reference is conditioned on `$(OS) == Windows_NT`. If the project is built on non-Windows, the assembly reference may be missing even though the source is still compiled.

### Issue Context
This project targets `net9.0;net10.0` (non-`-windows` TFMs), and the Windows provider file is included in compilation by default.

### Fix Focus Areas
- src/Daqifi.Core/Daqifi.Core.csproj[38-44]
- src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[42-49]

### Proposed fix
- Remove the OS-conditioned `ItemGroup` and add an unconditional `<PackageReference Include="System.Management" .../>` so compilation works everywhere.
- Keep the existing runtime OS checks to prevent execution on non-Windows.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Throwing-provider test may no-op ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
DiscoverAsync_WithThrowingDescriptorProvider_DoesNotAbortDiscovery does not assert the throwing
provider was actually invoked; if SerialPort.GetPortNames() returns no ports, the provider is never
called and the test still passes. This reduces regression coverage for FilterByUsbDescriptor’s
exception-handling behavior on CI/containers without serial ports.
Code

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[R153-167]

+    [Fact]
+    public async Task DiscoverAsync_WithThrowingDescriptorProvider_DoesNotAbortDiscovery()
+    {
+        // A custom IUsbPortDescriptorProvider that throws must NEVER take
+        // down the whole discovery pass — fall through to legacy probing
+        // for the port and continue with the rest of the list.
+        var fakeProvider = new RecordingUsbPortDescriptorProvider(_ =>
+            throw new InvalidOperationException("simulated provider failure"));
+
+        using var finder = new SerialDeviceFinder(9600, fakeProvider);
+        using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
+
+        var devices = await finder.DiscoverAsync(cts.Token);
+        Assert.NotNull(devices);
+    }
Evidence
The test only asserts the returned collection is non-null, but port enumeration comes from
SerialPort.GetPortNames() which can be empty; in that case the descriptor provider is never
invoked, so the intended exception-handling path is not exercised.

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[153-167]
src/Daqifi.Core/Communication/Transport/SerialStreamTransport.cs[208-215]
src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[111-133]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DiscoverAsync_WithThrowingDescriptorProvider_DoesNotAbortDiscovery` can succeed without ever executing the descriptor-provider path if the host has zero serial ports, so it may not detect regressions in the provider-exception handling.

## Issue Context
Discovery enumerates ports from `SerialPort.GetPortNames()`; on environments with no ports, discovery completes without invoking the injected provider.

## Fix Focus Areas
- src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[153-167]

### Suggested approach
- Introduce a test seam so tests can supply a deterministic port list (e.g., an internal constructor overload or injected delegate like `Func<string[]> getPortNames`), then:
 - Provide a fixed non-empty port list (e.g., `["COM999"]`), and
 - Assert the throwing provider was invoked and that discovery does not throw/abort.
- If introducing a seam is not acceptable, at minimum add an assertion that the provider was called when ports exist, and explicitly document/guard the test behavior when no ports are present.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. GetDescriptor exception triggers probe ✓ Resolved 📎 Requirement gap ⛨ Security
Description
If _usbPortDescriptorProvider.GetDescriptor(port) throws, FilterByUsbDescriptor catches the
exception, sets descriptor = null, and falls through to legacy probing, which may open/send SCPI
to non-DAQiFi ports and weakens the intended VID/PID pre-filter guarantee. Because the exception is
swallowed with no diagnostic signal, a broken/misbehaving provider can silently degrade filtering
and make resulting correctness/perf regressions difficult to detect and debug.
Code

src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[R352-364]

+            UsbPortDescriptor? descriptor;
+            try
+            {
+                descriptor = _usbPortDescriptorProvider.GetDescriptor(port);
+            }
+            catch
+            {
+                // A misbehaving descriptor provider must never block discovery.
+                // The shipped providers already swallow their own errors, but
+                // a custom IUsbPortDescriptorProvider could throw — fall back
+                // to legacy probing rather than aborting the whole scan.
+                descriptor = null;
+            }
Evidence
PR Compliance ID 1 requires pre-filtering ports by DAQiFi VID/PID so non-matching ports are never
opened/probed; however, the code wraps _usbPortDescriptorProvider.GetDescriptor(port) in a
catch-all that converts any provider failure into descriptor = null. The surrounding logic (and
the accompanying new test) indicates that a null descriptor preserves legacy behavior by yielding
the port for probing, meaning errors prevent VID/PID classification but still allow the port to be
opened/probed; additionally, no warning/trace is emitted in this path, so the degraded filtering
mode occurs with no observable indication.

Filter serial ports by DAQiFi USB VID/PID before probing
src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[352-364]
src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[153-167]
src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[348-372]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`FilterByUsbDescriptor` currently suppresses all exceptions from `_usbPortDescriptorProvider.GetDescriptor(port)` by treating them as `descriptor = null`, which causes the method to fall back to legacy probing and can reintroduce opening/sending SCPI to non-DAQiFi ports when the descriptor provider fails. The failure is also silent (bare catch/no telemetry), so VID/PID filtering can be effectively disabled without any observable signal.

## Issue Context
Compliance requires that non-DAQiFi VID/PID ports are never opened/probed during discovery. The method intentionally falls back to legacy probing when classification is unavailable, but when that condition is caused by a provider failure (exception) it should be detectable (e.g., a single diagnostic per discovery pass or per unique exception type, including the port name) so degraded-mode operation can be debugged.

## Fix Focus Areas
- src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[352-365]
- src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[153-168]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. WMI PNPClass='Ports' too strict 📎 Requirement gap ≡ Correctness
Description
The WMI query restricts matches to PNPClass = 'Ports', which can fail to resolve VID/PID for some
COM ports, causing the provider to return no results and bypass VID/PID filtering. This can lead to
discovery probing/opening ports without first verifying DAQiFi VID/PID, contrary to the
port-filtering requirement.
Code

src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[R57-63]

+        // Restricting to PNPClass='Ports' skips the rest of the device tree
+        // and shrinks WMI's enumeration cost; the result collection itself
+        // owns native handles and must be disposed.
+        using var searcher = new System.Management.ManagementObjectSearcher(
+            $"SELECT DeviceID FROM Win32_PnPEntity WHERE PNPClass = 'Ports' AND Caption LIKE '%({portName})%'");
+        using var results = searcher.Get();
+        foreach (var entity in results)
Evidence
PR Compliance ID 1 requires selecting candidate ports only after verifying DAQiFi VID/PID. The new
WMI query explicitly restricts lookup to PNPClass = 'Ports', increasing the chance that descriptor
resolution returns no results for a COM port and thus preventing VID/PID verification before
discovery proceeds.

Filter serial ports by DAQiFi USB VID/PID before probing
src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[57-63]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`WindowsUsbPortDescriptorProvider` uses a WMI query constrained to `PNPClass = 'Ports'`. If a COM port is exposed under a different PnP class (or lacks the expected caption/class), the query can return no rows, preventing VID/PID classification and undermining the “filter before probing” requirement.

## Issue Context
This PR’s compliance requirement is that non-DAQiFi ports should not be opened/probed unless VID/PID verification succeeds. Overly strict WMI constraints can cause a false "unknown" classification, which may allow probing without VID/PID verification.

## Fix Focus Areas
- src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[57-63]

Possible approaches:
- Remove/relax the `PNPClass='Ports'` restriction, or broaden it with an `OR` to include other relevant classes that can host COM ports.
- Add a second (fallback) WMI query path when the first returns no results (e.g., query `Win32_SerialPort` / `PNPDeviceID` mappings) while still keeping performance acceptable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
7. Ports probed sequentially in DiscoverAsync ✓ Resolved 📎 Requirement gap ➹ Performance
Description
DiscoverAsync still probes candidate ports one-at-a-time via an await inside a foreach, so
discovery latency will scale with the number of DAQiFi-matching ports rather than probing them
concurrently.
Code

src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[R129-134]

+            var allPorts = SerialStreamTransport.GetAvailablePortNames();
+            var nameFilteredPorts = FilterProbableDaqifiPorts(allPorts);
+            var availablePorts = FilterByUsbDescriptor(nameFilteredPorts);

            foreach (var portName in availablePorts)
            {
Evidence
PR Compliance ID 4 requires parallel probing when multiple candidate ports exist. The updated
DiscoverAsync implementation still iterates availablePorts with a foreach and awaits each
TryGetDeviceInfoAsync call, which enforces sequential probing rather than concurrent execution.

Candidate DAQiFi ports are probed in parallel when multiple exist
src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[119-156]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Discovery still probes ports sequentially (one awaited probe per iteration). Compliance requires probing multiple candidate DAQiFi ports concurrently (e.g., `Task.WhenAll`) to avoid cumulative latency.

## Issue Context
`availablePorts` is computed, then each port is probed via `await TryGetDeviceInfoAsync(...)` inside a `foreach`. This structure enforces strict sequential execution even when multiple candidate ports exist.

## Fix Focus Areas
- src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[119-156]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Unescaped WMI query string ✓ Resolved 🐞 Bug ⛨ Security
Description
WindowsUsbPortDescriptorProvider interpolates portName directly into the WMI query string (despite
the comment claiming a parameterized pattern), which is brittle if portName contains WQL special
characters and can expand match scope unexpectedly. It should validate/escape portName before
embedding it in the WQL LIKE clause.
Code

src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[R44-48]

+        // Match COM-port entities by Caption suffix, e.g. "USB Serial Device (COM9)".
+        // Avoids enumerating every PnP device on the system. Use a parameterized
+        // LIKE pattern; no user input crosses the WMI boundary.
+        using var searcher = new System.Management.ManagementObjectSearcher(
+            $"SELECT DeviceID FROM Win32_PnPEntity WHERE Caption LIKE '%({portName})%'");
Evidence
The query string is constructed with direct interpolation of portName, and the nearby comment
asserts parameterization even though no parameters are used.

src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[42-49]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The WMI query is built via string interpolation: `... WHERE Caption LIKE '%({portName})%'`. This is not parameterized and is sensitive to special characters (e.g., `'`, `%`, `_`).

### Issue Context
In practice, `portName` is typically OS-enumerated (e.g., `COM9`), so exploitability is low; however, robustness and correctness improve if the code enforces expected formats and escapes characters.

### Fix Focus Areas
- src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[42-49]

### Proposed fix
- Validate `portName` is an expected COM port name (e.g., `^COM\d+$`, case-insensitive); otherwise return null.
- Escape single quotes for WQL safety (`portName.Replace("'", "''")`).
- Update the misleading comment that says the query is parameterized (it is not).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Sysfs symlink traversal mismatch ✓ Resolved 🐞 Bug ☼ Reliability
Description
LinuxUsbPortDescriptorProvider claims it walks a “symlink-resolved path” but uses Path.GetFullPath
and then Directory.GetParent, which does not actually resolve symlinks and can walk the /sys/class
path rather than the USB device tree. This can cause frequent null descriptors on Linux, silently
disabling the VID/PID filter there.
Code

src/Daqifi.Core/Device/Discovery/LinuxUsbPortDescriptorProvider.cs[R32-57]

+        // Walk up the symlink-resolved path looking for idVendor/idProduct.
+        // Bound the depth to keep this defensive against unexpected layouts.
+        try
+        {
+            var current = System.IO.Path.GetFullPath(sysfsRoot);
+            for (var i = 0; i < 8; i++)
+            {
+                var vendorPath = System.IO.Path.Combine(current, "idVendor");
+                var productPath = System.IO.Path.Combine(current, "idProduct");
+                if (System.IO.File.Exists(vendorPath) && System.IO.File.Exists(productPath))
+                {
+                    var vidText = System.IO.File.ReadAllText(vendorPath).Trim();
+                    var pidText = System.IO.File.ReadAllText(productPath).Trim();
+                    if (int.TryParse(vidText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var vid) &&
+                        int.TryParse(pidText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var pid))
+                    {
+                        return new UsbPortDescriptor(vid, pid);
+                    }
+                    return null;
+                }
+
+                var parent = System.IO.Directory.GetParent(current);
+                if (parent == null || parent.FullName == current)
+                    break;
+                current = parent.FullName;
+            }
Evidence
The code explicitly says it walks a symlink-resolved path, but uses GetFullPath +
Directory.GetParent which operate on the path string and parent directories without dereferencing
the symlink target.

src/Daqifi.Core/Device/Discovery/LinuxUsbPortDescriptorProvider.cs[20-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The implementation intends to walk up the underlying device tree to locate `idVendor`/`idProduct`, but uses `Path.GetFullPath(sysfsRoot)` and then parent traversal. `GetFullPath` normalizes paths; it does not dereference symlinks.

### Issue Context
`/sys/class/tty/<name>/device` is typically a symlink into `/sys/devices/...`. To walk *that* tree, the code should resolve the link target before climbing parents.

### Fix Focus Areas
- src/Daqifi.Core/Device/Discovery/LinuxUsbPortDescriptorProvider.cs[28-57]

### Proposed fix
- Use `DirectoryInfo` / `ResolveLinkTarget(true)` (or equivalent) to get the real path of `/sys/class/tty/<base>/device`.
- Walk parents from the resolved target path.
- Update the comment to match the actual approach if you intentionally don’t resolve symlinks.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

10. Test probes real COM port ✓ Resolved 🐞 Bug ☼ Reliability
Description
DiscoverAsync_WithThrowingDescriptorProvider_DoesNotAbortDiscovery injects a hard-coded "COM999"
port name; on some Windows setups this port can exist, and the discovery probe will open it and send
SCPI traffic. This can make the unit test flaky and can cause unintended interaction with attached
serial hardware during test runs.
Code

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[R176-180]

+        using var finder = new SerialDeviceFinder(
+            9600,
+            fakeProvider,
+            portNameProvider: () => new[] { "COM999" });
+        using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
Evidence
The test injects COM999 as a forced candidate port, and discovery probing will open whatever port
name it receives and send SCPI GetDeviceInfo on it if open succeeds.

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[163-180]
src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[262-340]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`DiscoverAsync_WithThrowingDescriptorProvider_DoesNotAbortDiscovery` forces discovery to probe a specific port name (`COM999`). If that port exists on the machine running tests, the probe path will open it and send `GetDeviceInfo`, risking unintended hardware interaction and timing-related flakiness.

### Issue Context
This test injects a fixed port list via `portNameProvider` specifically to ensure the descriptor-provider exception-handling path is exercised even when the host has no real ports.

### Fix Focus Areas
- src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[176-180]

### Suggested fix
Replace the hard-coded `COM999` with a port name that is guaranteed not to map to real hardware, e.g.:
- Use an intentionally invalid Windows port name like `"COM_DOES_NOT_EXIST"` (so `SerialPort.Open()` fails immediately without ever touching a real device), or
- Compute a non-existent port dynamically:
 - `var existing = SerialPort.GetPortNames();`
 - pick a candidate (e.g. `COM999`, `COM9999`, …) that is not in `existing`.

Either approach preserves the test’s goal (descriptor provider gets invoked) while avoiding accidental interaction with real COM ports.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Previous review results

Review updated until commit 8b69f6b

Results up to commit 416bc93


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)


Action required
1. Non-Windows build may fail ✓ Resolved 🐞 Bug ≡ Correctness
Description
Daqifi.Core.csproj references System.Management only when $(OS) == Windows_NT, but
WindowsUsbPortDescriptorProvider is compiled unconditionally and directly uses System.Management
types. This can cause compilation failures on non-Windows build agents (or any environment where
System.Management isn’t otherwise available) even though runtime guards prevent execution.
Code

src/Daqifi.Core/Daqifi.Core.csproj[R42-44]

+  <ItemGroup Condition="'$(OS)' == 'Windows_NT'">
+    <PackageReference Include="System.Management" Version="10.0.7" />
+  </ItemGroup>
Evidence
The csproj only brings in System.Management on Windows, while the Windows descriptor provider uses
System.Management APIs in compiled code, so compilation depends on that reference being present.

src/Daqifi.Core/Daqifi.Core.csproj[3-45]
src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[1-68]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`WindowsUsbPortDescriptorProvider` directly references `System.Management`, but the package reference is conditioned on `$(OS) == Windows_NT`. If the project is built on non-Windows, the assembly reference may be missing even though the source is still compiled.

### Issue Context
This project targets `net9.0;net10.0` (non-`-windows` TFMs), and the Windows provider file is included in compilation by default.

### Fix Focus Areas
- src/Daqifi.Core/Daqifi.Core.csproj[38-44]
- src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[42-49]

### Proposed fix
- Remove the OS-conditioned `ItemGroup` and add an unconditional `<PackageReference Include="System.Management" .../>` so compilation works everywhere.
- Keep the existing runtime OS checks to prevent execution on non-Windows.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended
2. Sysfs symlink traversal mismatch ✓ Resolved 🐞 Bug ☼ Reliability
Description
LinuxUsbPortDescriptorProvider claims it walks a “symlink-resolved path” but uses Path.GetFullPath
and then Directory.GetParent, which does not actually resolve symlinks and can walk the /sys/class
path rather than the USB device tree. This can cause frequent null descriptors on Linux, silently
disabling the VID/PID filter there.
Code

src/Daqifi.Core/Device/Discovery/LinuxUsbPortDescriptorProvider.cs[R32-57]

+        // Walk up the symlink-resolved path looking for idVendor/idProduct.
+        // Bound the depth to keep this defensive against unexpected layouts.
+        try
+        {
+            var current = System.IO.Path.GetFullPath(sysfsRoot);
+            for (var i = 0; i < 8; i++)
+            {
+                var vendorPath = System.IO.Path.Combine(current, "idVendor");
+                var productPath = System.IO.Path.Combine(current, "idProduct");
+                if (System.IO.File.Exists(vendorPath) && System.IO.File.Exists(productPath))
+                {
+                    var vidText = System.IO.File.ReadAllText(vendorPath).Trim();
+                    var pidText = System.IO.File.ReadAllText(productPath).Trim();
+                    if (int.TryParse(vidText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var vid) &&
+                        int.TryParse(pidText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var pid))
+                    {
+                        return new UsbPortDescriptor(vid, pid);
+                    }
+                    return null;
+                }
+
+                var parent = System.IO.Directory.GetParent(current);
+                if (parent == null || parent.FullName == current)
+                    break;
+                current = parent.FullName;
+            }
Evidence
The code explicitly says it walks a symlink-resolved path, but uses GetFullPath +
Directory.GetParent which operate on the path string and parent directories without dereferencing
the symlink target.

src/Daqifi.Core/Device/Discovery/LinuxUsbPortDescriptorProvider.cs[20-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The implementation intends to walk up the underlying device tree to locate `idVendor`/`idProduct`, but uses `Path.GetFullPath(sysfsRoot)` and then parent traversal. `GetFullPath` normalizes paths; it does not dereference symlinks.

### Issue Context
`/sys/class/tty/<name>/device` is typically a symlink into `/sys/devices/...`. To walk *that* tree, the code should resolve the link target before climbing parents.

### Fix Focus Areas
- src/Daqifi.Core/Device/Discovery/LinuxUsbPortDescriptorProvider.cs[28-57]

### Proposed fix
- Use `DirectoryInfo` / `ResolveLinkTarget(true)` (or equivalent) to get the real path of `/sys/class/tty/<base>/device`.
- Walk parents from the resolved target path.
- Update the comment to match the actual approach if you intentionally don’t resolve symlinks.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Unescaped WMI query string ✓ Resolved 🐞 Bug ⛨ Security
Description
WindowsUsbPortDescriptorProvider interpolates portName directly into the WMI query string (despite
the comment claiming a parameterized pattern), which is brittle if portName contains WQL special
characters and can expand match scope unexpectedly. It should validate/escape portName before
embedding it in the WQL LIKE clause.
Code

src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[R44-48]

+        // Match COM-port entities by Caption suffix, e.g. "USB Serial Device (COM9)".
+        // Avoids enumerating every PnP device on the system. Use a parameterized
+        // LIKE pattern; no user input crosses the WMI boundary.
+        using var searcher = new System.Management.ManagementObjectSearcher(
+            $"SELECT DeviceID FROM Win32_PnPEntity WHERE Caption LIKE '%({portName})%'");
Evidence
The query string is constructed with direct interpolation of portName, and the nearby comment
asserts parameterization even though no parameters are used.

src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[42-49]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The WMI query is built via string interpolation: `... WHERE Caption LIKE '%({portName})%'`. This is not parameterized and is sensitive to special characters (e.g., `'`, `%`, `_`).

### Issue Context
In practice, `portName` is typically OS-enumerated (e.g., `COM9`), so exploitability is low; however, robustness and correctness improve if the code enforces expected formats and escapes characters.

### Fix Focus Areas
- src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[42-49]

### Proposed fix
- Validate `portName` is an expected COM port name (e.g., `^COM\d+$`, case-insensitive); otherwise return null.
- Escape single quotes for WQL safety (`portName.Replace("'", "''")`).
- Update the misleading comment that says the query is parameterized (it is not).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Ports probed sequentially in DiscoverAsync ✓ Resolved 📎 Requirement gap ➹ Performance
Description
DiscoverAsync still probes candidate ports one-at-a-time via an await inside a foreach, so
discovery latency will scale with the number of DAQiFi-matching ports rather than probing them
concurrently.
Code

src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[R129-134]

+            var allPorts = SerialStreamTransport.GetAvailablePortNames();
+            var nameFilteredPorts = FilterProbableDaqifiPorts(allPorts);
+            var availablePorts = FilterByUsbDescriptor(nameFilteredPorts);

            foreach (var portName in availablePorts)
            {
Evidence
PR Compliance ID 4 requires parallel probing when multiple candidate ports exist. The updated
DiscoverAsync implementation still iterates availablePorts with a foreach and awaits each
TryGetDeviceInfoAsync call, which enforces sequential probing rather than concurrent execution.

Candidate DAQiFi ports are probed in parallel when multiple exist
src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[119-156]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Discovery still probes ports sequentially (one awaited probe per iteration). Compliance requires probing multiple candidate DAQiFi ports concurrently (e.g., `Task.WhenAll`) to avoid cumulative latency.

## Issue Context
`availablePorts` is computed, then each port is probed via `await TryGetDeviceInfoAsync(...)` inside a `foreach`. This structure enforces strict sequential execution even when multiple candidate ports exist.

## Fix Focus Areas
- src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[119-156]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 97d0959


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (1)


Remediation recommended
1. WMI PNPClass='Ports' too strict 📎 Requirement gap ≡ Correctness
Description
The WMI query restricts matches to PNPClass = 'Ports', which can fail to resolve VID/PID for some
COM ports, causing the provider to return no results and bypass VID/PID filtering. This can lead to
discovery probing/opening ports without first verifying DAQiFi VID/PID, contrary to the
port-filtering requirement.
Code

src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[R57-63]

+        // Restricting to PNPClass='Ports' skips the rest of the device tree
+        // and shrinks WMI's enumeration cost; the result collection itself
+        // owns native handles and must be disposed.
+        using var searcher = new System.Management.ManagementObjectSearcher(
+            $"SELECT DeviceID FROM Win32_PnPEntity WHERE PNPClass = 'Ports' AND Caption LIKE '%({portName})%'");
+        using var results = searcher.Get();
+        foreach (var entity in results)
Evidence
PR Compliance ID 1 requires selecting candidate ports only after verifying DAQiFi VID/PID. The new
WMI query explicitly restricts lookup to PNPClass = 'Ports', increasing the chance that descriptor
resolution returns no results for a COM port and thus preventing VID/PID verification before
discovery proceeds.

Filter serial ports by DAQiFi USB VID/PID before probing
src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[57-63]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`WindowsUsbPortDescriptorProvider` uses a WMI query constrained to `PNPClass = 'Ports'`. If a COM port is exposed under a different PnP class (or lacks the expected caption/class), the query can return no rows, preventing VID/PID classification and undermining the “filter before probing” requirement.

## Issue Context
This PR’s compliance requirement is that non-DAQiFi ports should not be opened/probed unless VID/PID verification succeeds. Overly strict WMI constraints can cause a false "unknown" classification, which may allow probing without VID/PID verification.

## Fix Focus Areas
- src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[57-63]

Possible approaches:
- Remove/relax the `PNPClass='Ports'` restriction, or broaden it with an `OR` to include other relevant classes that can host COM ports.
- Add a second (fallback) WMI query path when the first returns no results (e.g., query `Win32_SerialPort` / `PNPDeviceID` mappings) while still keeping performance acceptable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit d3d869a


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)


Remediation recommended
1. Throwing-provider test may no-op ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
DiscoverAsync_WithThrowingDescriptorProvider_DoesNotAbortDiscovery does not assert the throwing
provider was actually invoked; if SerialPort.GetPortNames() returns no ports, the provider is never
called and the test still passes. This reduces regression coverage for FilterByUsbDescriptor’s
exception-handling behavior on CI/containers without serial ports.
Code

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[R153-167]

+    [Fact]
+    public async Task DiscoverAsync_WithThrowingDescriptorProvider_DoesNotAbortDiscovery()
+    {
+        // A custom IUsbPortDescriptorProvider that throws must NEVER take
+        // down the whole discovery pass — fall through to legacy probing
+        // for the port and continue with the rest of the list.
+        var fakeProvider = new RecordingUsbPortDescriptorProvider(_ =>
+            throw new InvalidOperationException("simulated provider failure"));
+
+        using var finder = new SerialDeviceFinder(9600, fakeProvider);
+        using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
+
+        var devices = await finder.DiscoverAsync(cts.Token);
+        Assert.NotNull(devices);
+    }
Evidence
The test only asserts the returned collection is non-null, but port enumeration comes from
SerialPort.GetPortNames() which can be empty; in that case the descriptor provider is never
invoked, so the intended exception-handling path is not exercised.

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[153-167]
src/Daqifi.Core/Communication/Transport/SerialStreamTransport.cs[208-215]
src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[111-133]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DiscoverAsync_WithThrowingDescriptorProvider_DoesNotAbortDiscovery` can succeed without ever executing the descriptor-provider path if the host has zero serial ports, so it may not detect regressions in the provider-exception handling.

## Issue Context
Discovery enumerates ports from `SerialPort.GetPortNames()`; on environments with no ports, discovery completes without invoking the injected provider.

## Fix Focus Areas
- src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[153-167]

### Suggested approach
- Introduce a test seam so tests can supply a deterministic port list (e.g., an internal constructor overload or injected delegate like `Func<string[]> getPortNames`), then:
 - Provide a fixed non-empty port list (e.g., `["COM999"]`), and
 - Assert the throwing provider was invoked and that discovery does not throw/abort.
- If introducing a seam is not acceptable, at minimum add an assertion that the provider was called when ports exist, and explicitly document/guard the test behavior when no ports are present.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. GetDescriptor exception triggers probe ✓ Resolved 📎 Requirement gap ⛨ Security
Description
If _usbPortDescriptorProvider.GetDescriptor(port) throws, FilterByUsbDescriptor catches the
exception, sets descriptor = null, and falls through to legacy probing, which may open/send SCPI
to non-DAQiFi ports and weakens the intended VID/PID pre-filter guarantee. Because the exception is
swallowed with no diagnostic signal, a broken/misbehaving provider can silently degrade filtering
and make resulting correctness/perf regressions difficult to detect and debug.
Code

src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[R352-364]

+            UsbPortDescriptor? descriptor;
+            try
+            {
+                descriptor = _usbPortDescriptorProvider.GetDescriptor(port);
+            }
+            catch
+            {
+                // A misbehaving descriptor provider must never block discovery.
+                // The shipped providers already swallow their own errors, but
+                // a custom IUsbPortDescriptorProvider could throw — fall back
+                // to legacy probing rather than aborting the whole scan.
+                descriptor = null;
+            }
Evidence
PR Compliance ID 1 requires pre-filtering ports by DAQiFi VID/PID so non-matching ports are never
opened/probed; however, the code wraps _usbPortDescriptorProvider.GetDescriptor(port) in a
catch-all that converts any provider failure into descriptor = null. The surrounding logic (and
the accompanying new test) indicates that a null descriptor preserves legacy behavior by yielding
the port for probing, meaning errors prevent VID/PID classification but still allow the port to be
opened/probed; additionally, no warning/trace is emitted in this path, so the degraded filtering
mode occurs with no observable indication.

Filter serial ports by DAQiFi USB VID/PID before probing
src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[352-364]
src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[153-167]
src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[348-372]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`FilterByUsbDescriptor` currently suppresses all exceptions from `_usbPortDescriptorProvider.GetDescriptor(port)` by treating them as `descriptor = null`, which causes the method to fall back to legacy probing and can reintroduce opening/sending SCPI to non-DAQiFi ports when the descriptor provider fails. The failure is also silent (bare catch/no telemetry), so VID/PID filtering can be effectively disabled without any observable signal.

## Issue Context
Compliance requires that non-DAQiFi VID/PID ports are never opened/probed during discovery. The method intentionally falls back to legacy probing when classification is unavailable, but when that condition is caused by a provider failure (exception) it should be detectable (e.g., a single diagnostic per discovery pass or per unique exception type, including the port name) so degraded-mode operation can be debugged.

## Fix Focus Areas
- src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[352-365]
- src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[153-168]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit e0e76ff


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (1)


Action required
1. Null descriptor still probes ports 📎 Requirement gap ≡ Correctness
Description
The new test codifies that when GetDescriptor() returns null, discovery falls through to legacy
probing, which can open/send commands to ports without a confirmed DAQiFi VID/PID. This violates the
requirement that only VID/PID-matched DAQiFi ports are probed and others receive no traffic.
Code

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[R149-151]

+        // The legacy probe path may exceed 200ms on machines with real ports —
+        // an OperationCanceledException there still proves the contract:
+        // null descriptors fall through to probing rather than being filtered.
Evidence
PR Compliance ID 1 requires that only ports matching known DAQiFi VID/PID values are opened/probed.
The added test explicitly states and accepts that null descriptors "fall through to probing rather
than being filtered," meaning ports without a confirmed DAQiFi VID/PID may still be probed.

Filter serial ports by DAQiFi USB VID/PID before probing
src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[149-151]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Current behavior (and test expectation) allows `null` USB descriptors to fall through to probing, which can open and send DAQiFi/SCPI commands to non-DAQiFi ports when VID/PID cannot be resolved.

## Issue Context
PR Compliance requires serial discovery to avoid probing non-DAQiFi ports by filtering on known DAQiFi VID/PID values before any port is opened.

## Fix Focus Areas
- src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs[149-160]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Results up to commit 43d82c2


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)


<img src="https://www.qodo.ai/wp-content/uploads/2026/01/action...

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Non-Windows build may fail ✓ Resolved 🐞 Bug ≡ Correctness
Description
Daqifi.Core.csproj references System.Management only when $(OS) == Windows_NT, but
WindowsUsbPortDescriptorProvider is compiled unconditionally and directly uses System.Management
types. This can cause compilation failures on non-Windows build agents (or any environment where
System.Management isn’t otherwise available) even though runtime guards prevent execution.
Code

src/Daqifi.Core/Daqifi.Core.csproj[R42-44]

+  <ItemGroup Condition="'$(OS)' == 'Windows_NT'">
+    <PackageReference Include="System.Management" Version="10.0.7" />
+  </ItemGroup>
Evidence
The csproj only brings in System.Management on Windows, while the Windows descriptor provider uses
System.Management APIs in compiled code, so compilation depends on that reference being present.

src/Daqifi.Core/Daqifi.Core.csproj[3-45]
src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[1-68]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`WindowsUsbPortDescriptorProvider` directly references `System.Management`, but the package reference is conditioned on `$(OS) == Windows_NT`. If the project is built on non-Windows, the assembly reference may be missing even though the source is still compiled.

### Issue Context
This project targets `net9.0;net10.0` (non-`-windows` TFMs), and the Windows provider file is included in compilation by default.

### Fix Focus Areas
- src/Daqifi.Core/Daqifi.Core.csproj[38-44]
- src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[42-49]

### Proposed fix
- Remove the OS-conditioned `ItemGroup` and add an unconditional `<PackageReference Include="System.Management" .../>` so compilation works everywhere.
- Keep the existing runtime OS checks to prevent execution on non-Windows.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Unescaped WMI query string ✓ Resolved 🐞 Bug ⛨ Security
Description
WindowsUsbPortDescriptorProvider interpolates portName directly into the WMI query string (despite
the comment claiming a parameterized pattern), which is brittle if portName contains WQL special
characters and can expand match scope unexpectedly. It should validate/escape portName before
embedding it in the WQL LIKE clause.
Code

src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[R44-48]

+        // Match COM-port entities by Caption suffix, e.g. "USB Serial Device (COM9)".
+        // Avoids enumerating every PnP device on the system. Use a parameterized
+        // LIKE pattern; no user input crosses the WMI boundary.
+        using var searcher = new System.Management.ManagementObjectSearcher(
+            $"SELECT DeviceID FROM Win32_PnPEntity WHERE Caption LIKE '%({portName})%'");
Evidence
The query string is constructed with direct interpolation of portName, and the nearby comment
asserts parameterization even though no parameters are used.

src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[42-49]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The WMI query is built via string interpolation: `... WHERE Caption LIKE '%({portName})%'`. This is not parameterized and is sensitive to special characters (e.g., `'`, `%`, `_`).

### Issue Context
In practice, `portName` is typically OS-enumerated (e.g., `COM9`), so exploitability is low; however, robustness and correctness improve if the code enforces expected formats and escapes characters.

### Fix Focus Areas
- src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs[42-49]

### Proposed fix
- Validate `portName` is an expected COM port name (e.g., `^COM\d+$`, case-insensitive); otherwise return null.
- Escape single quotes for WQL safety (`portName.Replace("'", "''")`).
- Update the misleading comment that says the query is parameterized (it is not).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Sysfs symlink traversal mismatch ✓ Resolved 🐞 Bug ☼ Reliability
Description
LinuxUsbPortDescriptorProvider claims it walks a “symlink-resolved path” but uses Path.GetFullPath
and then Directory.GetParent, which does not actually resolve symlinks and can walk the /sys/class
path rather than the USB device tree. This can cause frequent null descriptors on Linux, silently
disabling the VID/PID filter there.
Code

src/Daqifi.Core/Device/Discovery/LinuxUsbPortDescriptorProvider.cs[R32-57]

+        // Walk up the symlink-resolved path looking for idVendor/idProduct.
+        // Bound the depth to keep this defensive against unexpected layouts.
+        try
+        {
+            var current = System.IO.Path.GetFullPath(sysfsRoot);
+            for (var i = 0; i < 8; i++)
+            {
+                var vendorPath = System.IO.Path.Combine(current, "idVendor");
+                var productPath = System.IO.Path.Combine(current, "idProduct");
+                if (System.IO.File.Exists(vendorPath) && System.IO.File.Exists(productPath))
+                {
+                    var vidText = System.IO.File.ReadAllText(vendorPath).Trim();
+                    var pidText = System.IO.File.ReadAllText(productPath).Trim();
+                    if (int.TryParse(vidText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var vid) &&
+                        int.TryParse(pidText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var pid))
+                    {
+                        return new UsbPortDescriptor(vid, pid);
+                    }
+                    return null;
+                }
+
+                var parent = System.IO.Directory.GetParent(current);
+                if (parent == null || parent.FullName == current)
+                    break;
+                current = parent.FullName;
+            }
Evidence
The code explicitly says it walks a symlink-resolved path, but uses GetFullPath +
Directory.GetParent which operate on the path string and parent directories without dereferencing
the symlink target.

src/Daqifi.Core/Device/Discovery/LinuxUsbPortDescriptorProvider.cs[20-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The implementation intends to walk up the underlying device tree to locate `idVendor`/`idProduct`, but uses `Path.GetFullPath(sysfsRoot)` and then parent traversal. `GetFullPath` normalizes paths; it does not dereference symlinks.

### Issue Context
`/sys/class/tty/<name>/device` is typically a symlink into `/sys/devices/...`. To walk *that* tree, the code should resolve the link target before climbing parents.

### Fix Focus Areas
- src/Daqifi.Core/Device/Discovery/LinuxUsbPortDescriptorProvider.cs[28-57]

### Proposed fix
- Use `DirectoryInfo` / `ResolveLinkTarget(true)` (or equivalent) to get the real path of `/sys/class/tty/<base>/device`.
- Walk parents from the resolved target path.
- Update the comment to match the actual approach if you intentionally don’t resolve symlinks.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Ports probed sequentially in DiscoverAsync ✓ Resolved 📎 Requirement gap ➹ Performance
Description
DiscoverAsync still probes candidate ports one-at-a-time via an await inside a foreach, so
discovery latency will scale with the number of DAQiFi-matching ports rather than probing them
concurrently.
Code

src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[R129-134]

+            var allPorts = SerialStreamTransport.GetAvailablePortNames();
+            var nameFilteredPorts = FilterProbableDaqifiPorts(allPorts);
+            var availablePorts = FilterByUsbDescriptor(nameFilteredPorts);

            foreach (var portName in availablePorts)
            {
Evidence
PR Compliance ID 4 requires parallel probing when multiple candidate ports exist. The updated
DiscoverAsync implementation still iterates availablePorts with a foreach and awaits each
TryGetDeviceInfoAsync call, which enforces sequential probing rather than concurrent execution.

Candidate DAQiFi ports are probed in parallel when multiple exist
src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[119-156]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Discovery still probes ports sequentially (one awaited probe per iteration). Compliance requires probing multiple candidate DAQiFi ports concurrently (e.g., `Task.WhenAll`) to avoid cumulative latency.

## Issue Context
`availablePorts` is computed, then each port is probed via `await TryGetDeviceInfoAsync(...)` inside a `foreach`. This structure enforces strict sequential execution even when multiple candidate ports exist.

## Fix Focus Areas
- src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs[119-156]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

PR Code Suggestions ✨

Latest suggestions up to 8b69f6b

Warning

/improve is deprecated. Use /agentic_review instead (removal date not yet scheduled).

CategorySuggestion                                                                                                                                    Impact
Possible issue
Make the probe-fallback test deterministic

Make the fallback probe test deterministic by injecting a fake port name and
asserting the provider was invoked.

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs [50-69]

 var fakeProvider = new RecordingUsbPortDescriptorProvider(_ => null);
 
-using var finder = new SerialDeviceFinder(9600, fakeProvider);
-using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
+using var finder = new SerialDeviceFinder(
+    9600,
+    fakeProvider,
+    portNameProvider: () => new[] { "MOCK_PORT_DOES_NOT_EXIST" });
+using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
 
-// Just verifying it doesn't throw and returns a (probably empty) list;
-// actual ports depend on the test machine. The key contract is that
-// null-descriptor doesn't filter the port out of consideration.
-// The legacy probe path may exceed 200ms on machines with real ports —
-// an OperationCanceledException there still proves the contract:
-// null descriptors fall through to probing rather than being filtered.
-try
-{
-    var devices = await finder.DiscoverAsync(cts.Token);
-    Assert.NotNull(devices);
-}
-catch (OperationCanceledException)
-{
-    // Probe ran (legacy fallback engaged) and exceeded the test budget.
-}
+var devices = await finder.DiscoverAsync(cts.Token);
 
+Assert.NotNull(devices);
+Assert.True(fakeProvider.CallCount > 0,
+    "Descriptor provider was not invoked — test did not exercise the null-descriptor path.");
+

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 8

__

Why: Injecting a mock portNameProvider makes the test robust and 100% deterministic across all CI environments by removing the reliance on the host's actual serial ports and arbitrary OperationCanceledException timeouts.

Medium
Relax flaky timing-based assertion

Relax the timing assertion threshold to prevent potential test flakiness on slow
CI environments.

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs [29-39]

 var stopwatch = System.Diagnostics.Stopwatch.StartNew();
 var devices = await finder.DiscoverAsync(cts.Token);
 stopwatch.Stop();
 
 Assert.Empty(devices);
 Assert.Equal(3, fakeProvider.CallCount);
-// If any port were probed, the test would take seconds per port
-// (DeviceWakeUpDelayMs + ResponseTimeoutMs); should be well under
-// 500ms for the classifier-only path even on a many-port system.
-Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(1),
+
+// Optional perf guardrail: keep generous to avoid CI flakiness.
+Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(5),
     $"Discovery took {stopwatch.ElapsedMilliseconds}ms — classifier filter may not be wired correctly.");

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 2

__

Why: While addressing test flakiness is valid, relaxing the threshold to 5 seconds would defeat the purpose of the test, as a broken implementation running 3 parallel probes would take ~2.7s and falsely pass the relaxed assertion.

Low
  • More

Previous suggestions

✅ Suggestions up to commit 5f213f6
CategorySuggestion                                                                                                                                    Impact
Possible issue
Make the test deterministic
Suggestion Impact:The test now injects a deterministic three-port list using portNameProvider and asserts fakeProvider.CallCount equals 3, removing dependency on the host’s real serial ports and verifying the classifier path ran.

code diff:

+        // injected port should have been classified once and zero devices
+        // returned — proving the filter ran AND that the port-probe path
+        // was never reached. Inject a deterministic 3-port list so the
+        // test exercises the classifier path even on CI hosts with no
+        // real serial ports.
         var fakeProvider = new RecordingUsbPortDescriptorProvider(_ =>
-        {
-            Interlocked.Increment(ref classifierCallCount);
-            return new UsbPortDescriptor(0x1A86, 0x7523); // CH340, not DAQiFi
-        });
-
-        using var finder = new SerialDeviceFinder(9600, fakeProvider);
+            new UsbPortDescriptor(0x1A86, 0x7523)); // CH340, not DAQiFi
+
+        using var finder = new SerialDeviceFinder(
+            9600,
+            fakeProvider,
+            portNameProvider: () => new[] { "COM1", "COM2", "COM3" });
         using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
 
         var stopwatch = System.Diagnostics.Stopwatch.StartNew();
@@ -123,6 +124,7 @@
         stopwatch.Stop();
 
         Assert.Empty(devices);
+        Assert.Equal(3, fakeProvider.CallCount);

Inject a fixed list of mock serial ports using portNameProvider and assert that
the descriptor provider was invoked. This removes the test's dependency on the
host's actual serial ports.

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs [25-37]

-using var finder = new SerialDeviceFinder(9600, fakeProvider);
+using var finder = new SerialDeviceFinder(
+    9600,
+    fakeProvider,
+    portNameProvider: () => new[] { "COM1", "COM2", "COM3" });
 using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
 
 var stopwatch = System.Diagnostics.Stopwatch.StartNew();
 var devices = await finder.DiscoverAsync(cts.Token);
 stopwatch.Stop();
 
 Assert.Empty(devices);
-// If any port were probed, the test would take seconds per port
-// (DeviceWakeUpDelayMs + ResponseTimeoutMs); should be well under
-// 500ms for the classifier-only path even on a many-port system.
+Assert.Equal(3, fakeProvider.CallCount);
 Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(1),
     $"Discovery took {stopwatch.ElapsedMilliseconds}ms — classifier filter may not be wired correctly.");
Suggestion importance[1-10]: 9

__

Why: Supplying explicit port names via the portNameProvider ensures the test doesn't vacuously pass on CI environments lacking serial ports, while the added assertion rigorously validates that the classifier filter was actually exercised.

High
General
Remove dependency on real ports
Suggestion Impact:The test was updated to inject a deterministic port list via portNameProvider (COM1/COM2/COM3), removing dependency on real ports, and it added an assertion on fakeProvider.CallCount to validate classification behavior.

code diff:

+        // injected port should have been classified once and zero devices
+        // returned — proving the filter ran AND that the port-probe path
+        // was never reached. Inject a deterministic 3-port list so the
+        // test exercises the classifier path even on CI hosts with no
+        // real serial ports.
         var fakeProvider = new RecordingUsbPortDescriptorProvider(_ =>
-        {
-            Interlocked.Increment(ref classifierCallCount);
-            return new UsbPortDescriptor(0x1A86, 0x7523); // CH340, not DAQiFi
-        });
-
-        using var finder = new SerialDeviceFinder(9600, fakeProvider);
+            new UsbPortDescriptor(0x1A86, 0x7523)); // CH340, not DAQiFi
+
+        using var finder = new SerialDeviceFinder(
+            9600,
+            fakeProvider,
+            portNameProvider: () => new[] { "COM1", "COM2", "COM3" });
         using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
 
         var stopwatch = System.Diagnostics.Stopwatch.StartNew();
@@ -123,6 +124,7 @@
         stopwatch.Stop();
 
         Assert.Empty(devices);
+        Assert.Equal(3, fakeProvider.CallCount);
         // If any port were probed, the test would take seconds per port

Use portNameProvider to supply a mock port name, aiming to remove the test's
dependency on the host's actual serial ports. This forces the test to
consistently fall through to the probe path across all platforms.

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs [48-67]

 var fakeProvider = new RecordingUsbPortDescriptorProvider(_ => null);
 
-using var finder = new SerialDeviceFinder(9600, fakeProvider);
+using var finder = new SerialDeviceFinder(
+    9600,
+    fakeProvider,
+    portNameProvider: () => new[] { "MOCK_PORT_DOES_NOT_EXIST" });
 using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
 
-// Just verifying it doesn't throw and returns a (probably empty) list;
-// actual ports depend on the test machine. The key contract is that
-// null-descriptor doesn't filter the port out of consideration.
-// The legacy probe path may exceed 200ms on machines with real ports —
-// an OperationCanceledException there still proves the contract:
-// null descriptors fall through to probing rather than being filtered.
 try
 {
     var devices = await finder.DiscoverAsync(cts.Token);
     Assert.NotNull(devices);
 }
 catch (OperationCanceledException)
 {
     // Probe ran (legacy fallback engaged) and exceeded the test budget.
 }
Suggestion importance[1-10]: 3

__

Why: While removing test reliance on physical hardware is well-intentioned, supplying a non-existent port causes the probe to fail instantly, rendering the OperationCanceledException catch block dead code and making the test vacuous since it lacks a substitute assertion (like CallCount > 0).

Low
✅ Suggestions up to commit f0f5926
CategorySuggestion                                                                                                                                    Impact
General
Remove timing-based test flakiness
Suggestion Impact:The test was updated to inject a deterministic 3-port list via portNameProvider and to assert the fake provider CallCount equals 3, reducing reliance on host serial ports and adding deterministic verification. (Timing-based stopwatch assertions were not removed, though.)

code diff:

@@ -105,17 +105,18 @@
         //
         // The fake provider classifies every port as a CH340 (non-DAQiFi)
         // and tracks GetDescriptor calls. After DiscoverAsync, every
-        // platform-listed port should have been classified once and zero
-        // devices returned — proving the filter ran AND that the port-
-        // probe path was never reached.
-        var classifierCallCount = 0;
+        // injected port should have been classified once and zero devices
+        // returned — proving the filter ran AND that the port-probe path
+        // was never reached. Inject a deterministic 3-port list so the
+        // test exercises the classifier path even on CI hosts with no
+        // real serial ports.
         var fakeProvider = new RecordingUsbPortDescriptorProvider(_ =>
-        {
-            Interlocked.Increment(ref classifierCallCount);
-            return new UsbPortDescriptor(0x1A86, 0x7523); // CH340, not DAQiFi
-        });
-
-        using var finder = new SerialDeviceFinder(9600, fakeProvider);
+            new UsbPortDescriptor(0x1A86, 0x7523)); // CH340, not DAQiFi
+
+        using var finder = new SerialDeviceFinder(
+            9600,
+            fakeProvider,
+            portNameProvider: () => new[] { "COM1", "COM2", "COM3" });
         using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
 
         var stopwatch = System.Diagnostics.Stopwatch.StartNew();
@@ -123,6 +124,7 @@
         stopwatch.Stop();
 
         Assert.Empty(devices);
+        Assert.Equal(3, fakeProvider.CallCount);
         // If any port were probed, the test would take seconds per port
         // (DeviceWakeUpDelayMs + ResponseTimeoutMs); should be well under
         // 500ms for the classifier-only path even on a many-port system.
@@ -173,15 +175,19 @@
         var fakeProvider = new RecordingUsbPortDescriptorProvider(_ =>
             throw new InvalidOperationException("simulated provider failure"));
 
+        // Use an obviously-invalid port name so SerialPort.Open() fails
+        // immediately on every platform — never touches a real device, even
+        // if the host happens to expose a high-numbered COM port (COM999 etc.
+        // can exist on some Windows setups with virtual serial drivers).
         using var finder = new SerialDeviceFinder(
             9600,
             fakeProvider,
-            portNameProvider: () => new[] { "COM999" });
+            portNameProvider: () => new[] { "MOCK_PORT_DOES_NOT_EXIST" });
         using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
 
-        // Probe path may exceed 200ms (legacy fallback engages on COM999 then
-        // fails to open). OCE is acceptable; what matters is that the throwing
-        // provider was actually called and didn't propagate.
+        // Probe path is short (legacy fallback fails to open immediately on
+        // the bogus port name). OCE is still acceptable; what matters is that
+        // the throwing provider was actually called and didn't propagate.

Inject a fixed portNameProvider and replace wall-clock timing assertions with
exact call count verification to make the test deterministic.

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs [25-37]

-using var finder = new SerialDeviceFinder(9600, fakeProvider);
+using var finder = new SerialDeviceFinder(
+    9600,
+    fakeProvider,
+    portNameProvider: () => new[] { "COM1", "COM2", "COM3" });
 using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
 
-var stopwatch = System.Diagnostics.Stopwatch.StartNew();
 var devices = await finder.DiscoverAsync(cts.Token);
-stopwatch.Stop();
 
 Assert.Empty(devices);
-// If any port were probed, the test would take seconds per port
-// (DeviceWakeUpDelayMs + ResponseTimeoutMs); should be well under
-// 500ms for the classifier-only path even on a many-port system.
-Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(1),
-    $"Discovery took {stopwatch.ElapsedMilliseconds}ms — classifier filter may not be wired correctly.");
+Assert.Equal(3, fakeProvider.CallCount);
Suggestion importance[1-10]: 7

__

Why: Removing time-based assertions and system-dependent state by injecting specific mock test ports prevents CI flakiness and improves the test's reliability.

Medium
✅ Suggestions up to commit 4cc0f08
CategorySuggestion                                                                                                                                    Impact
General
Remove flaky timing assertion
Suggestion Impact:The commit added explicit assertions on the descriptor provider call count (e.g., Assert.Equal(3, fakeProvider.CallCount) and Assert.True(fakeProvider.CallCount > 0)) by enhancing the fake provider to track invocations, though it did not remove the existing stopwatch timing assertion.

code diff:

@@ -105,17 +105,18 @@
         //
         // The fake provider classifies every port as a CH340 (non-DAQiFi)
         // and tracks GetDescriptor calls. After DiscoverAsync, every
-        // platform-listed port should have been classified once and zero
-        // devices returned — proving the filter ran AND that the port-
-        // probe path was never reached.
-        var classifierCallCount = 0;
+        // injected port should have been classified once and zero devices
+        // returned — proving the filter ran AND that the port-probe path
+        // was never reached. Inject a deterministic 3-port list so the
+        // test exercises the classifier path even on CI hosts with no
+        // real serial ports.
         var fakeProvider = new RecordingUsbPortDescriptorProvider(_ =>
-        {
-            Interlocked.Increment(ref classifierCallCount);
-            return new UsbPortDescriptor(0x1A86, 0x7523); // CH340, not DAQiFi
-        });
-
-        using var finder = new SerialDeviceFinder(9600, fakeProvider);
+            new UsbPortDescriptor(0x1A86, 0x7523)); // CH340, not DAQiFi
+
+        using var finder = new SerialDeviceFinder(
+            9600,
+            fakeProvider,
+            portNameProvider: () => new[] { "COM1", "COM2", "COM3" });
         using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
 
         var stopwatch = System.Diagnostics.Stopwatch.StartNew();
@@ -123,6 +124,7 @@
         stopwatch.Stop();
 
         Assert.Empty(devices);
+        Assert.Equal(3, fakeProvider.CallCount);
         // If any port were probed, the test would take seconds per port
         // (DeviceWakeUpDelayMs + ResponseTimeoutMs); should be well under
         // 500ms for the classifier-only path even on a many-port system.
@@ -166,15 +168,26 @@
         // A custom IUsbPortDescriptorProvider that throws must NEVER take
         // down the whole discovery pass — fall through to legacy probing
         // for the port and continue with the rest of the list.
+        //
+        // Inject a fixed port list so the throwing provider IS invoked even
+        // on CI hosts with zero real serial ports — otherwise the test would
+        // pass vacuously without exercising the exception-handling path.
         var fakeProvider = new RecordingUsbPortDescriptorProvider(_ =>
             throw new InvalidOperationException("simulated provider failure"));
 
-        using var finder = new SerialDeviceFinder(9600, fakeProvider);
+        // Use an obviously-invalid port name so SerialPort.Open() fails
+        // immediately on every platform — never touches a real device, even
+        // if the host happens to expose a high-numbered COM port (COM999 etc.
+        // can exist on some Windows setups with virtual serial drivers).
+        using var finder = new SerialDeviceFinder(
+            9600,
+            fakeProvider,
+            portNameProvider: () => new[] { "MOCK_PORT_DOES_NOT_EXIST" });
         using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
 
-        // Same caveat as the null-descriptor test: probe path may exceed
-        // 200ms on machines with real ports. OCE is acceptable here too
-        // — what matters is that the throwing provider didn't propagate.
+        // Probe path is short (legacy fallback fails to open immediately on
+        // the bogus port name). OCE is still acceptable; what matters is that
+        // the throwing provider was actually called and didn't propagate.
         try
         {
             var devices = await finder.DiscoverAsync(cts.Token);
@@ -184,14 +197,23 @@
         {
             // Probe ran (provider throw was caught and treated as null).
         }
+
+        Assert.True(fakeProvider.CallCount > 0,
+            "Throwing descriptor provider was never invoked — the exception-handling path is not exercised by this test.");
     }

Replace the stopwatch timing assertion with an explicit assertion on
classifierCallCount.

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs [28-37]

-var stopwatch = System.Diagnostics.Stopwatch.StartNew();
 var devices = await finder.DiscoverAsync(cts.Token);
-stopwatch.Stop();
 
 Assert.Empty(devices);
-// If any port were probed, the test would take seconds per port
-// (DeviceWakeUpDelayMs + ResponseTimeoutMs); should be well under
-// 500ms for the classifier-only path even on a many-port system.
-Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(1),
-    $"Discovery took {stopwatch.ElapsedMilliseconds}ms — classifier filter may not be wired correctly.");
+Assert.True(classifierCallCount > 0, "Descriptor provider was never consulted; USB filter may not be wired correctly.");
Suggestion importance[1-10]: 8

__

Why: Replacing the flaky timing-based assertion with a direct check of classifierCallCount significantly improves test reliability and correctly utilizes the mock provider logic.

Medium
Possible issue
Prevent null results enumeration

Initialize probeResults to an empty array and add a defensive catch block to
handle unexpected exceptions during Task.WhenAll.

src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs [115-136]

-IDeviceInfo?[]? probeResults = null;
+var probeResults = Array.Empty<IDeviceInfo?>();
+
 try
 {
     probeResults = await Task.WhenAll(probeTasks).ConfigureAwait(false);
 }
 catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 {
     // Drain any tasks that did finish successfully before cancellation.
+    probeResults = probeTasks
+        .Where(t => t.Status == TaskStatus.RanToCompletion)
+        .Select(t => t.Result)
+        .ToArray();
+}
+catch
+{
+    // Defensive: if anything unexpected escapes a probe task, don't
+    // crash discovery; return whatever completed successfully.
     probeResults = probeTasks
         .Where(t => t.Status == TaskStatus.RanToCompletion)
         .Select(t => t.Result)
         .ToArray();
 }
 
 foreach (var deviceInfo in probeResults)
 {
     if (deviceInfo != null)
     {
         discoveredDevices.Add(deviceInfo);
         OnDeviceDiscovered(deviceInfo);
     }
 }
Suggestion importance[1-10]: 7

__

Why: Handling unexpected exceptions ensures probeResults doesn't remain null, preventing a NullReferenceException and maintaining resilient discovery behavior.

Medium
Security
Escape special characters in query

Escape WQL LIKE special characters in portName prior to embedding it in the WMI
query.

src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs [65-66]

+var likeSafePortName = portName
+    .Replace("[", "[[]", StringComparison.Ordinal)
+    .Replace("%", "[%]", StringComparison.Ordinal)
+    .Replace("_", "[_]", StringComparison.Ordinal);
+
 using var searcher = new System.Management.ManagementObjectSearcher(
-    $"SELECT DeviceID FROM Win32_PnPEntity WHERE PNPClass = 'Ports' AND Caption LIKE '%({portName})%'");
+    $"SELECT DeviceID FROM Win32_PnPEntity WHERE PNPClass = 'Ports' AND Caption LIKE '%({likeSafePortName})%'");
Suggestion importance[1-10]: 3

__

Why: Escaping WQL special characters offers only marginal future-proofing, since the strict regex validation already guarantees portName only contains alphanumeric characters.

Low
✅ Suggestions up to commit 1164d84
CategorySuggestion                                                                                                                                    Impact
Possible issue
Limit parallel probing concurrency
Suggestion Impact:The commit introduced a MaxParallelProbes cap and a SemaphoreSlim (probeGate) to limit concurrent ProbeSafelyAsync executions, wrapping each probe in WaitAsync/Release and awaiting Task.WhenAll with ConfigureAwait(false). It also added extra handling for cancellation and a test seam for port name retrieval.

code diff:

+    // Upper bound on concurrent SerialPort opens during a single discovery
+    // pass. Most OS serial stacks tolerate 4-8 simultaneous opens cleanly;
+    // beyond that, IO failures and slow opens stack up. Common case (with
+    // VID/PID classifier) leaves 0-1 candidates so this cap rarely engages.
+    private const int MaxParallelProbes = 4;
 
     #endregion
 
@@ -43,6 +48,7 @@
     private readonly int _baudRate;
     private readonly SemaphoreSlim _discoverySemaphore = new(1, 1);
     private readonly IUsbPortDescriptorProvider _usbPortDescriptorProvider;
+    private readonly Func<string[]>? _portNameProvider;
     private bool _disposed;
 
     #endregion
@@ -91,11 +97,21 @@
     /// <see cref="NullUsbPortDescriptorProvider.Instance"/> explicitly to
     /// force the legacy probe-everything behavior.
     /// </param>
-    internal SerialDeviceFinder(int baudRate, IUsbPortDescriptorProvider? usbPortDescriptorProvider)
+    /// <param name="portNameProvider">
+    /// Test seam: when non-null, supplies the candidate port-name list in
+    /// place of <see cref="SerialStreamTransport.GetAvailablePortNames"/>.
+    /// Lets unit tests deterministically exercise the descriptor / probe
+    /// path on hosts (CI containers) that have no real serial ports.
+    /// </param>
+    internal SerialDeviceFinder(
+        int baudRate,
+        IUsbPortDescriptorProvider? usbPortDescriptorProvider,
+        Func<string[]>? portNameProvider = null)
     {
         _baudRate = baudRate;
         _usbPortDescriptorProvider = usbPortDescriptorProvider
             ?? UsbPortDescriptorProviderFactory.CreateForCurrentPlatform();
+        _portNameProvider = portNameProvider;
     }
 
     #endregion
@@ -126,7 +142,8 @@
             // Ports the descriptor provider can't classify (e.g. on macOS
             // where we have no impl yet) fall through to legacy probing,
             // matched only by name-pattern in FilterProbableDaqifiPorts.
-            var allPorts = SerialStreamTransport.GetAvailablePortNames();
+            var allPorts = _portNameProvider?.Invoke()
+                ?? SerialStreamTransport.GetAvailablePortNames();
             var nameFilteredPorts = FilterProbableDaqifiPorts(allPorts);
             var availablePorts = FilterByUsbDescriptor(nameFilteredPorts).ToList();
 
@@ -136,11 +153,44 @@
             // but the legacy fallback (null descriptor) can still surface
             // multiple unclassifiable ports — probing them sequentially adds
             // ~1.2s per port (DeviceWakeUpDelayMs + ResponseTimeoutMs).
-            var probeTasks = availablePorts
-                .Select(portName => ProbeSafelyAsync(portName, cancellationToken))
-                .ToList();
-
-            var probeResults = await Task.WhenAll(probeTasks);
+            //
+            // Cap concurrency at MaxParallelProbes so a 20-port system with
+            // a missing descriptor provider doesn't open 20 serial handles at
+            // once — most platforms throttle quietly above ~8 concurrent opens.
+            var maxConcurrency = Math.Max(1, Math.Min(MaxParallelProbes, availablePorts.Count));
+            using var probeGate = new SemaphoreSlim(maxConcurrency, maxConcurrency);
+
+            var probeTasks = availablePorts.Select(async portName =>
+            {
+                await probeGate.WaitAsync(cancellationToken).ConfigureAwait(false);
+                try
+                {
+                    return await ProbeSafelyAsync(portName, cancellationToken).ConfigureAwait(false);
+                }
+                finally
+                {
+                    probeGate.Release();
+                }
+            }).ToList();
+
+            // Catch OCE here so the DiscoveryCompleted event always fires
+            // (callers expect a "discovery pass terminated" signal regardless
+            // of how it ended). ProbeSafelyAsync rethrows OCE on caller
+            // cancellation so WhenAll short-circuits the rest of the set —
+            // we then settle for whatever probes already finished cleanly.
+            IDeviceInfo?[]? probeResults = null;
+            try
+            {
+                probeResults = await Task.WhenAll(probeTasks).ConfigureAwait(false);
+            }
+            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+            {
+                // Drain any tasks that did finish successfully before cancellation.
+                probeResults = probeTasks
+                    .Where(t => t.Status == TaskStatus.RanToCompletion)
+                    .Select(t => t.Result)
+                    .ToArray();
+            }

Use a SemaphoreSlim to limit the number of concurrent port probes to a safe
upper bound.

src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs [85-97]

-var probeTasks = availablePorts
-    .Select(portName => ProbeSafelyAsync(portName, cancellationToken))
-    .ToList();
+var maxConcurrency = Math.Max(1, Math.Min(4, availablePorts.Count));
+using var gate = new SemaphoreSlim(maxConcurrency, maxConcurrency);
 
-var probeResults = await Task.WhenAll(probeTasks);
+var probeTasks = availablePorts.Select(async portName =>
+{
+    await gate.WaitAsync(cancellationToken).ConfigureAwait(false);
+    try
+    {
+        return await ProbeSafelyAsync(portName, cancellationToken).ConfigureAwait(false);
+    }
+    finally
+    {
+        gate.Release();
+    }
+}).ToList();
+
+var probeResults = await Task.WhenAll(probeTasks).ConfigureAwait(false);
 
 foreach (var deviceInfo in probeResults)
 {
     if (deviceInfo != null)
     {
         discoveredDevices.Add(deviceInfo);
         OnDeviceDiscovered(deviceInfo);
     }
 }
Suggestion importance[1-10]: 7

__

Why: Limiting concurrency with a SemaphoreSlim improves robustness and prevents potential resource exhaustion or I/O failures when many unclassified ports fall back to legacy probing.

Medium
Remove flaky timing-based assertion
Suggestion Impact:The commit added deterministic injected port lists and implemented call counting in the fake descriptor provider, then asserted that the provider was called (e.g., CallCount == 3 / > 0). This addresses the suggestion’s goal of asserting classifier/provider invocation (and avoids CI hosts with zero serial ports), though it did not remove the existing stopwatch timing assertion or uniformly reduce the timeout as suggested.

code diff:

@@ -105,17 +105,18 @@
         //
         // The fake provider classifies every port as a CH340 (non-DAQiFi)
         // and tracks GetDescriptor calls. After DiscoverAsync, every
-        // platform-listed port should have been classified once and zero
-        // devices returned — proving the filter ran AND that the port-
-        // probe path was never reached.
-        var classifierCallCount = 0;
+        // injected port should have been classified once and zero devices
+        // returned — proving the filter ran AND that the port-probe path
+        // was never reached. Inject a deterministic 3-port list so the
+        // test exercises the classifier path even on CI hosts with no
+        // real serial ports.
         var fakeProvider = new RecordingUsbPortDescriptorProvider(_ =>
-        {
-            Interlocked.Increment(ref classifierCallCount);
-            return new UsbPortDescriptor(0x1A86, 0x7523); // CH340, not DAQiFi
-        });
-
-        using var finder = new SerialDeviceFinder(9600, fakeProvider);
+            new UsbPortDescriptor(0x1A86, 0x7523)); // CH340, not DAQiFi
+
+        using var finder = new SerialDeviceFinder(
+            9600,
+            fakeProvider,
+            portNameProvider: () => new[] { "COM1", "COM2", "COM3" });
         using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
 
         var stopwatch = System.Diagnostics.Stopwatch.StartNew();
@@ -123,6 +124,7 @@
         stopwatch.Stop();
 
         Assert.Empty(devices);
+        Assert.Equal(3, fakeProvider.CallCount);
         // If any port were probed, the test would take seconds per port
         // (DeviceWakeUpDelayMs + ResponseTimeoutMs); should be well under
         // 500ms for the classifier-only path even on a many-port system.
@@ -166,15 +168,26 @@
         // A custom IUsbPortDescriptorProvider that throws must NEVER take
         // down the whole discovery pass — fall through to legacy probing
         // for the port and continue with the rest of the list.
+        //
+        // Inject a fixed port list so the throwing provider IS invoked even
+        // on CI hosts with zero real serial ports — otherwise the test would
+        // pass vacuously without exercising the exception-handling path.
         var fakeProvider = new RecordingUsbPortDescriptorProvider(_ =>
             throw new InvalidOperationException("simulated provider failure"));
 
-        using var finder = new SerialDeviceFinder(9600, fakeProvider);
+        // Use an obviously-invalid port name so SerialPort.Open() fails
+        // immediately on every platform — never touches a real device, even
+        // if the host happens to expose a high-numbered COM port (COM999 etc.
+        // can exist on some Windows setups with virtual serial drivers).
+        using var finder = new SerialDeviceFinder(
+            9600,
+            fakeProvider,
+            portNameProvider: () => new[] { "MOCK_PORT_DOES_NOT_EXIST" });
         using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
 
-        // Same caveat as the null-descriptor test: probe path may exceed
-        // 200ms on machines with real ports. OCE is acceptable here too
-        // — what matters is that the throwing provider didn't propagate.
+        // Probe path is short (legacy fallback fails to open immediately on
+        // the bogus port name). OCE is still acceptable; what matters is that
+        // the throwing provider was actually called and didn't propagate.
         try
         {
             var devices = await finder.DiscoverAsync(cts.Token);
@@ -184,14 +197,23 @@
         {
             // Probe ran (provider throw was caught and treated as null).
         }
+
+        Assert.True(fakeProvider.CallCount > 0,
+            "Throwing descriptor provider was never invoked — the exception-handling path is not exercised by this test.");
     }
 
     private sealed class RecordingUsbPortDescriptorProvider : IUsbPortDescriptorProvider
     {
         private readonly Func<string, UsbPortDescriptor?> _classifier;
+        private int _callCount;
         public RecordingUsbPortDescriptorProvider(Func<string, UsbPortDescriptor?> classifier)
             => _classifier = classifier;
-        public UsbPortDescriptor? GetDescriptor(string portName) => _classifier(portName);
+        public int CallCount => Volatile.Read(ref _callCount);
+        public UsbPortDescriptor? GetDescriptor(string portName)
+        {
+            Interlocked.Increment(ref _callCount);
+            return _classifier(portName);
+        }

Replace the wall-clock timing assertion with a small cancellation budget and
assert that the classifier was invoked.

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs [25-37]

 using var finder = new SerialDeviceFinder(9600, fakeProvider);
-using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
+using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(150));
 
-var stopwatch = System.Diagnostics.Stopwatch.StartNew();
 var devices = await finder.DiscoverAsync(cts.Token);
-stopwatch.Stop();
 
 Assert.Empty(devices);
-// If any port were probed, the test would take seconds per port
-// (DeviceWakeUpDelayMs + ResponseTimeoutMs); should be well under
-// 500ms for the classifier-only path even on a many-port system.
-Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(1),
-    $"Discovery took {stopwatch.ElapsedMilliseconds}ms — classifier filter may not be wired correctly.");
+Assert.True(classifierCallCount > 0, "Descriptor provider should have been consulted.");
Suggestion importance[1-10]: 4

__

Why: While avoiding wall-clock timing assertions can reduce flakiness, the suggested classifierCallCount > 0 assertion will fail on CI runners with zero serial ports, thereby introducing a new failure point.

Low
✅ Suggestions up to commit 43d82c2
CategorySuggestion                                                                                                                                    Impact
Possible issue
Propagate cancellation correctly
Suggestion Impact:The commit changed ProbeSafelyAsync to rethrow OperationCanceledException when cancellation is requested, ensuring cancellation propagates to Task.WhenAll; it also added handling around WhenAll to catch cancellation while still firing completion signaling and collecting completed results.

code diff:

+            // Catch OCE here so the DiscoveryCompleted event always fires
+            // (callers expect a "discovery pass terminated" signal regardless
+            // of how it ended). ProbeSafelyAsync rethrows OCE on caller
+            // cancellation so WhenAll short-circuits the rest of the set —
+            // we then settle for whatever probes already finished cleanly.
+            IDeviceInfo?[]? probeResults = null;
+            try
+            {
+                probeResults = await Task.WhenAll(probeTasks).ConfigureAwait(false);
+            }
+            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+            {
+                // Drain any tasks that did finish successfully before cancellation.
+                probeResults = probeTasks
+                    .Where(t => t.Status == TaskStatus.RanToCompletion)
+                    .Select(t => t.Result)
+                    .ToArray();
+            }
 
             foreach (var deviceInfo in probeResults)
             {
@@ -193,8 +231,9 @@
         }
         catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
         {
-            // Caller asked to stop — let WhenAll observe the cancellation.
-            return null;
+            // Caller asked to stop — propagate so Task.WhenAll observes the
+            // cancellation and short-circuits the rest of the probe set.
+            throw;
         }

Rethrow OperationCanceledException instead of returning null to correctly
propagate cancellation.

src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs [109-127]

 private async Task<IDeviceInfo?> ProbeSafelyAsync(string portName, CancellationToken cancellationToken)
 {
     try
     {
         return await TryGetDeviceInfoAsync(portName, cancellationToken).ConfigureAwait(false);
     }
     catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
     {
-        // Caller asked to stop — let WhenAll observe the cancellation.
-        return null;
+        // Caller asked to stop — propagate cancellation.
+        throw;
     }
     catch
     {
         // Probe failure (port locked, no response, IO error, etc.) is
         // expected for non-DAQiFi serial devices — keep the rest of the
         // concurrent probe set going and return no device for this port.
         return null;
     }
 }
Suggestion importance[1-10]: 9

__

Why: Returning null swallows the cancellation instead of propagating it, breaking the intended behavior described in the method's docstring where Task.WhenAll should short-circuit.

High
Assert filter execution in test
Suggestion Impact:The commit added call-count tracking to the RecordingUsbPortDescriptorProvider and asserted it was invoked (Assert.Equal(3, fakeProvider.CallCount) and Assert.True(fakeProvider.CallCount > 0)), ensuring the USB descriptor pre-filter/exception path is actually exercised.

code diff:

         var fakeProvider = new RecordingUsbPortDescriptorProvider(_ =>
-        {
-            Interlocked.Increment(ref classifierCallCount);
-            return new UsbPortDescriptor(0x1A86, 0x7523); // CH340, not DAQiFi
-        });
-
-        using var finder = new SerialDeviceFinder(9600, fakeProvider);
+            new UsbPortDescriptor(0x1A86, 0x7523)); // CH340, not DAQiFi
+
+        using var finder = new SerialDeviceFinder(
+            9600,
+            fakeProvider,
+            portNameProvider: () => new[] { "COM1", "COM2", "COM3" });
         using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
 
         var stopwatch = System.Diagnostics.Stopwatch.StartNew();
@@ -123,6 +124,7 @@
         stopwatch.Stop();
 
         Assert.Empty(devices);
+        Assert.Equal(3, fakeProvider.CallCount);
         // If any port were probed, the test would take seconds per port
         // (DeviceWakeUpDelayMs + ResponseTimeoutMs); should be well under
         // 500ms for the classifier-only path even on a many-port system.
@@ -166,15 +168,26 @@
         // A custom IUsbPortDescriptorProvider that throws must NEVER take
         // down the whole discovery pass — fall through to legacy probing
         // for the port and continue with the rest of the list.
+        //
+        // Inject a fixed port list so the throwing provider IS invoked even
+        // on CI hosts with zero real serial ports — otherwise the test would
+        // pass vacuously without exercising the exception-handling path.
         var fakeProvider = new RecordingUsbPortDescriptorProvider(_ =>
             throw new InvalidOperationException("simulated provider failure"));
 
-        using var finder = new SerialDeviceFinder(9600, fakeProvider);
+        // Use an obviously-invalid port name so SerialPort.Open() fails
+        // immediately on every platform — never touches a real device, even
+        // if the host happens to expose a high-numbered COM port (COM999 etc.
+        // can exist on some Windows setups with virtual serial drivers).
+        using var finder = new SerialDeviceFinder(
+            9600,
+            fakeProvider,
+            portNameProvider: () => new[] { "MOCK_PORT_DOES_NOT_EXIST" });
         using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
 
-        // Same caveat as the null-descriptor test: probe path may exceed
-        // 200ms on machines with real ports. OCE is acceptable here too
-        // — what matters is that the throwing provider didn't propagate.
+        // Probe path is short (legacy fallback fails to open immediately on
+        // the bogus port name). OCE is still acceptable; what matters is that
+        // the throwing provider was actually called and didn't propagate.
         try
         {
             var devices = await finder.DiscoverAsync(cts.Token);
@@ -184,14 +197,23 @@
         {
             // Probe ran (provider throw was caught and treated as null).
         }
+
+        Assert.True(fakeProvider.CallCount > 0,
+            "Throwing descriptor provider was never invoked — the exception-handling path is not exercised by this test.");
     }
 
     private sealed class RecordingUsbPortDescriptorProvider : IUsbPortDescriptorProvider
     {
         private readonly Func<string, UsbPortDescriptor?> _classifier;
+        private int _callCount;
         public RecordingUsbPortDescriptorProvider(Func<string, UsbPortDescriptor?> classifier)
             => _classifier = classifier;
-        public UsbPortDescriptor? GetDescriptor(string portName) => _classifier(portName);
+        public int CallCount => Volatile.Read(ref _callCount);
+        public UsbPortDescriptor? GetDescriptor(string portName)
+        {
+            Interlocked.Increment(ref _callCount);
+            return _classifier(portName);
+        }

Add an assertion for classifierCallCount to ensure the USB-descriptor filter is
actually executed in the test.

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs [18-37]

 var classifierCallCount = 0;
 var fakeProvider = new RecordingUsbPortDescriptorProvider(_ =>
 {
     Interlocked.Increment(ref classifierCallCount);
     return new UsbPortDescriptor(0x1A86, 0x7523); // CH340, not DAQiFi
 });
 ...
 Assert.Empty(devices);
-// If any port were probed, the test would take seconds per port
-// (DeviceWakeUpDelayMs + ResponseTimeoutMs); should be well under
-// 500ms for the classifier-only path even on a many-port system.
+Assert.True(classifierCallCount > 0, "Descriptor provider was never invoked; USB pre-filter may not be wired.");
 Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(1),
     $"Discovery took {stopwatch.ElapsedMilliseconds}ms — classifier filter may not be wired correctly.");
Suggestion importance[1-10]: 8

__

Why: The PR introduces classifierCallCount in the test but forgets to assert it, leaving the test incomplete; this suggestion ensures the filter's execution is properly validated.

Medium
General
Limit concurrent port probing

Limit the number of concurrent port probing tasks using a SemaphoreSlim.

src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs [85-89]

-var probeTasks = availablePorts
-    .Select(portName => ProbeSafelyAsync(portName, cancellationToken))
-    .ToList();
+var maxConcurrentProbes = 4;
+using var probeGate = new SemaphoreSlim(maxConcurrentProbes, maxConcurrentProbes);
 
-var probeResults = await Task.WhenAll(probeTasks);
+var probeTasks = availablePorts.Select(async portName =>
+{
+    await probeGate.WaitAsync(cancellationToken).ConfigureAwait(false);
+    try
+    {
+        return await ProbeSafelyAsync(portName, cancellationToken).ConfigureAwait(false);
+    }
+    finally
+    {
+        probeGate.Release();
+    }
+}).ToList();
 
+var probeResults = await Task.WhenAll(probeTasks).ConfigureAwait(false);
+
Suggestion importance[1-10]: 3

__

Why: While bounding concurrency is a good practice, the pre-filtering of candidate ports typically leaves a very small number of items, making this a minor enhancement.

Low
✅ Suggestions up to commit 1f24638
CategorySuggestion                                                                                                                                    Impact
Possible issue
Prevent non-Windows build failures
Suggestion Impact:The csproj was updated to remove the Windows-only condition on the System.Management PackageReference, making it unconditional so WindowsUsbPortDescriptorProvider.cs can compile on Linux/macOS.

code diff:

-  <!-- Windows-only USB descriptor lookup via WMI for SerialDeviceFinder
-       VID/PID port filtering. The package adds nothing for Linux/macOS;
-       the runtime platform check in WindowsUsbPortDescriptorProvider
-       returns null on those platforms anyway. -->
-  <ItemGroup Condition="'$(OS)' == 'Windows_NT'">
+  <!-- USB descriptor lookup uses WMI on Windows. The package itself is
+       cross-platform metadata-only; only the WMI types execute, and the
+       runtime guard in WindowsUsbPortDescriptorProvider ensures it never
+       runs on non-Windows platforms. The reference is unconditional so
+       WindowsUsbPortDescriptorProvider.cs (compiled on every target)
+       can resolve System.Management even during a Linux/macOS build. -->
+  <ItemGroup>
     <PackageReference Include="System.Management" Version="10.0.7" />

Make the System.Management package reference unconditional to prevent
compilation failures on non-Windows platforms. Alternatively, conditionally
exclude WindowsUsbPortDescriptorProvider.cs from compilation on those platforms.

src/Daqifi.Core/Daqifi.Core.csproj [38-44]

-<!-- Windows-only USB descriptor lookup via WMI for SerialDeviceFinder
-     VID/PID port filtering. The package adds nothing for Linux/macOS;
-     the runtime platform check in WindowsUsbPortDescriptorProvider
-     returns null on those platforms anyway. -->
-<ItemGroup Condition="'$(OS)' == 'Windows_NT'">
+<!-- USB descriptor lookup uses WMI on Windows.
+     The runtime guard in WindowsUsbPortDescriptorProvider ensures it is never executed on non-Windows. -->
+<ItemGroup>
   <PackageReference Include="System.Management" Version="10.0.7" />
 </ItemGroup>

[Suggestion processed]

Suggestion importance[1-10]: 10

__

Why: The suggestion correctly identifies that a conditional package reference will break cross-platform compilation since the WindowsUsbPortDescriptorProvider.cs file utilizing it is unconditionally compiled.

High
Avoid nondeterministic cancellation failures
Suggestion Impact:The commit addressed nondeterministic cancellation by changing the tests to tolerate OperationCanceledException during probing (wrapping DiscoverAsync in try/catch) rather than removing the timeout and passing CancellationToken.None.

code diff:

         // Just verifying it doesn't throw and returns a (probably empty) list;
         // actual ports depend on the test machine. The key contract is that
         // null-descriptor doesn't filter the port out of consideration.
-        var devices = await finder.DiscoverAsync(cts.Token);
-        Assert.NotNull(devices);
+        // The legacy probe path may exceed 200ms on machines with real ports —
+        // an OperationCanceledException there still proves the contract:
+        // null descriptors fall through to probing rather than being filtered.
+        try
+        {
+            var devices = await finder.DiscoverAsync(cts.Token);
+            Assert.NotNull(devices);
+        }
+        catch (OperationCanceledException)
+        {
+            // Probe ran (legacy fallback engaged) and exceeded the test budget.
+        }
     }

Remove the tight 200ms cancellation timeout to prevent nondeterministic test
failures on environments with actual serial ports. Pass CancellationToken.None
instead to reliably test the fallback mechanism.

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs [40-58]

 [Fact]
 public async Task DiscoverAsync_WithNullDescriptor_FallsThroughToProbe()
 {
     ...
     using var finder = new SerialDeviceFinder(9600, fakeProvider);
-    using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
 
-    ...
-    var devices = await finder.DiscoverAsync(cts.Token);
+    // Just verifying it doesn't throw and returns a (probably empty) list;
+    // actual ports depend on the test machine. The key contract is that
+    // null-descriptor doesn't filter the port out of consideration.
+    var devices = await finder.DiscoverAsync(CancellationToken.None);
     Assert.NotNull(devices);
 }
Suggestion importance[1-10]: 8

__

Why: The suggestion correctly identifies that the tight 200ms timeout will cause the test to legitimately fail with an OperationCanceledException on runners that possess actual serial ports to probe.

Medium
Remove flaky timing-based assertion
Suggestion Impact:The commit addressed test flakiness related to timing/probe duration by changing the tests to tolerate OperationCanceledException when the probe path exceeds a short timeout, rather than implementing the suggested classifierCallCount-based assertion.

code diff:

@@ -146,8 +146,18 @@
         // Just verifying it doesn't throw and returns a (probably empty) list;
         // actual ports depend on the test machine. The key contract is that
         // null-descriptor doesn't filter the port out of consideration.
-        var devices = await finder.DiscoverAsync(cts.Token);
-        Assert.NotNull(devices);
+        // The legacy probe path may exceed 200ms on machines with real ports —
+        // an OperationCanceledException there still proves the contract:
+        // null descriptors fall through to probing rather than being filtered.
+        try
+        {
+            var devices = await finder.DiscoverAsync(cts.Token);
+            Assert.NotNull(devices);
+        }
+        catch (OperationCanceledException)
+        {
+            // Probe ran (legacy fallback engaged) and exceeded the test budget.
+        }
     }
 
     [Fact]
@@ -162,8 +172,18 @@
         using var finder = new SerialDeviceFinder(9600, fakeProvider);
         using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
 
-        var devices = await finder.DiscoverAsync(cts.Token);
-        Assert.NotNull(devices);
+        // Same caveat as the null-descriptor test: probe path may exceed
+        // 200ms on machines with real ports. OCE is acceptable here too
+        // — what matters is that the throwing provider didn't propagate.
+        try
+        {
+            var devices = await finder.DiscoverAsync(cts.Token);
+            Assert.NotNull(devices);
+        }
+        catch (OperationCanceledException)
+        {
+            // Probe ran (provider throw was caught and treated as null).
+        }
     }

Replace the timing-based assertion with an explicit check that the descriptor
provider was called (classifierCallCount > 0). This ensures the test validates
the filtering mechanism rather than relying on execution speed.

src/Daqifi.Core.Tests/Device/Discovery/SerialDeviceFinderTests.cs [5-38]

 [Fact]
 public async Task DiscoverAsync_WithNonDaqifiVidPid_DoesNotProbePort()
 {
     ...
     using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
 
-    var stopwatch = System.Diagnostics.Stopwatch.StartNew();
     var devices = await finder.DiscoverAsync(cts.Token);
-    stopwatch.Stop();
 
     Assert.Empty(devices);
-    ...
-    Assert.True(stopwatch.Elapsed < TimeSpan.FromSeconds(1),
-        $"Discovery took {stopwatch.ElapsedMilliseconds}ms — classifier filter may not be wired correctly.");
+    Assert.True(classifierCallCount > 0, "Descriptor provider was never consulted; USB filter may not be wired correctly.");
 }
Suggestion importance[1-10]: 5

__

Why: Although it correctly identifies the unasserted classifierCallCount variable and the potential flakiness of timing checks, the proposed fix classifierCallCount > 0 will incorrectly fail the test on CI environments with zero COM ports.

Low
✅ Suggestions up to commit d3d869a
CategorySuggestion                                                                                                                                    Impact
Possible issue
Guard against null port names
Suggestion Impact:Implemented an early return when portName is null or whitespace, preventing regex evaluation and avoiding exception-based control flow.

code diff:

+        // Bail explicitly on null/whitespace rather than relying on the
+        // outer try/catch to swallow an NRE from the regex match.
+        if (string.IsNullOrWhiteSpace(portName))
+            return null;
+

Add an explicit check to return null if portName is null or whitespace before
regex evaluation.

src/Daqifi.Core/Device/Discovery/WindowsUsbPortDescriptorProvider.cs [49-54]

 private static UsbPortDescriptor? QueryWmi(string portName)
 {
+    if (string.IsNullOrWhiteSpace(portName))
+        return null;
+
     // Reject anything that doesn't match COM<n> shape — the WQL string is
     // built by interpolation, so a stray q...

/sys/class/tty/<base>/device is a symlink into the actual USB device
tree. Path.GetFullPath returned the logical path, so walking parents
ended at /sys/class/tty/<base> instead of reaching the USB node where
idVendor/idProduct live — every Linux lookup silently returned null.

Use DirectoryInfo.ResolveLinkTarget(returnFinalTarget: true) to follow
the symlink to the physical device path before traversal.
Comment thread src/Daqifi.Core/Daqifi.Core.csproj Outdated
@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Persistent review updated to latest commit 6c7cd42

@qodo-code-review
Copy link
Copy Markdown

Persistent suggestions updated to latest commit 6c7cd42

The WQL query string is built by interpolation. SerialPort.GetPortNames()
returns COM<n> on Windows, but a stray single quote in unexpected input
would corrupt the LIKE clause. Reject anything that doesn't match
^COM\d+$ before sending the query.
@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Persistent review updated to latest commit ba62343

@qodo-code-review
Copy link
Copy Markdown

Persistent suggestions updated to latest commit ba62343

Hex parsing happens to be culture-invariant in practice, but the
defensive style fix passes CultureInfo.InvariantCulture explicitly so
analyzers don't flag the call sites and behavior is independent of
ambient culture.
@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Persistent review updated to latest commit 643a988

@qodo-code-review
Copy link
Copy Markdown

Persistent suggestions updated to latest commit 643a988

ManagementObjectSearcher.Get() returns a ManagementObjectCollection that
owns native handles and must be disposed; without this the enumerator
leaks every call. Also narrows the LIKE-based search to PNPClass='Ports'
so WMI doesn't enumerate non-port PnP devices before filtering.
@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Persistent review updated to latest commit 97d0959

@qodo-code-review
Copy link
Copy Markdown

Persistent suggestions updated to latest commit 97d0959

The shipped IUsbPortDescriptorProvider implementations already swallow
their own errors and return null, but a custom provider could throw —
that must not abort the whole discovery pass. Catch unconditionally and
fall through to legacy probing for the port. Adds a test that exercises
a throwing provider end-to-end.
@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Persistent review updated to latest commit 43d82c2

@qodo-code-review
Copy link
Copy Markdown

Persistent suggestions updated to latest commit 43d82c2

Comment thread src/Daqifi.Core/Device/Discovery/SerialDeviceFinder.cs
…tion

ProbeSafelyAsync was returning null on caller cancellation, swallowing
the signal and forcing every other probe in the parallel set to run to
completion. Rethrow OperationCanceledException when
cancellationToken.IsCancellationRequested so Task.WhenAll short-
circuits — matches the docstring contract and the upstream
DiscoverAsync expectation.
@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Persistent review updated to latest commit 1164d84

@qodo-code-review
Copy link
Copy Markdown

Persistent suggestions updated to latest commit 1164d84

…n event

Two coordinated changes:

1. SemaphoreSlim cap (MaxParallelProbes = 4) — caps concurrent SerialPort
   opens so a system with many unclassified ports doesn't exhaust file
   handles or hit IO failures. Common case (descriptor classifier
   identifies the DAQiFi port) leaves 0-1 candidates so the cap rarely
   engages.

2. Catch OCE around Task.WhenAll — ProbeSafelyAsync now rethrows OCE
   on caller cancellation (correct: lets WhenAll short-circuit the
   remaining probes). DiscoverAsync swallows that OCE, drains tasks
   that finished cleanly before cancellation, and still fires
   DiscoveryCompleted. Restores the historical contract that the
   completion event always signals "this discovery pass terminated"
   regardless of how it ended.
@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Persistent review updated to latest commit 4cc0f08

@qodo-code-review
Copy link
Copy Markdown

Persistent suggestions updated to latest commit 4cc0f08

Add Func<string[]> port-name test seam to SerialDeviceFinder so the
DiscoverAsync_WithThrowingDescriptorProvider_DoesNotAbortDiscovery test
deterministically exercises the FilterByUsbDescriptor catch path on
hosts (CI containers) with zero real serial ports — previously the
test could pass vacuously when SerialPort.GetPortNames() returned [].

Also instrument RecordingUsbPortDescriptorProvider with a CallCount so
the test can assert the throwing provider was actually invoked.
@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Persistent review updated to latest commit f0f5926

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

Persistent /agentic_review findings — intentional design choices:

  • On release create nuget version #2 "Null descriptor still probes ports" and chore: update target frameworks #6 "WMI PNPClass='Ports' too strict" both ask for strict VID/PID gating with no fall-through. The PR's deliberate behavior is "filter when descriptor known, fall through to legacy probe when classifier returns null/can't run." Reasons:
    • macOS has no descriptor provider implementation yet — strict gating would make discovery a no-op there.
    • Linux sysfs lookups can fail under various conditions (containers without /sys, permission issues).
    • Windows WMI queries can return empty for legitimate USB-CDC devices that aren't exposed under PNPClass='Ports' (e.g. composite devices, virtual COM ports under Modems or MEDIA).
    • The pre-filter goal is performance (drop ~5s × N non-DAQiFi ports), not security. Falling through to legacy probe on null preserves the existing behavior — never worse than main.

If we hardened to strict VID/PID-only, every macOS user and any Windows user with a non-standard PnP class would lose discovery entirely. That's a regression worse than the perf win.

The "Null descriptor falls through" behavior is documented in code (SerialDeviceFinder.cs:128-133 and FilterByUsbDescriptor) and codified by the DiscoverAsync_WithNullDescriptor_FallsThroughToProbe test. Closing these two findings as wontfix / by-design.

Test infrastructure gap (#5 "Throwing-provider test may no-op") is fixed in the latest commit by adding a Func<string[]> test seam.

@qodo-code-review
Copy link
Copy Markdown

Persistent suggestions updated to latest commit f0f5926

Replace "COM999" with "MOCK_PORT_DOES_NOT_EXIST" in the throwing-provider
test so SerialPort.Open() fails immediately on every platform — COM999 can
exist on Windows hosts with virtual serial drivers, which would risk
unintended hardware interaction during test runs.
@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Persistent review updated to latest commit 5f213f6

@qodo-code-review
Copy link
Copy Markdown

Persistent suggestions updated to latest commit 5f213f6

Inject a fixed 3-port list into DiscoverAsync_WithNonDaqifiVidPid_DoesNotProbePort
so the classifier filter path is exercised even on CI hosts with no real
serial ports. Replaces the host-dependent classifierCallCount counter with
the existing fakeProvider.CallCount and asserts == 3.
@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/improve

@cptkoolbeenz
Copy link
Copy Markdown
Member Author

/agentic_review

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review Bot commented May 12, 2026

Persistent review updated to latest commit 8b69f6b

@qodo-code-review
Copy link
Copy Markdown

Persistent suggestions updated to latest commit 8b69f6b

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.

perf: serial device discovery is too slow — probes ports sequentially with excessive commands and timeouts

1 participant