Skip to content

device: replace assert with proper prefix check in enumerate_lang; add multi-packet test - #34

Merged
thpoll83 merged 5 commits into
mainfrom
claude/happy-wright-UI6hE
Jun 9, 2026
Merged

device: replace assert with proper prefix check in enumerate_lang; add multi-packet test#34
thpoll83 merged 5 commits into
mainfrom
claude/happy-wright-UI6hE

Conversation

@thpoll83

@thpoll83 thpoll83 commented Jun 8, 2026

Copy link
Copy Markdown
Owner

The while loop in enumerate_lang correctly reads all N response packets
from the firmware (GET_LANG_LIST, case 8, sends 4 back-to-back). Replace
the assert — which is silently disabled with python -O — with a logged
warning + break so a stale or mismatched reply stops the loop cleanly
rather than crashing in debug and passing corrupt data in optimised mode.

Add tests/device/poly_kybd_test.py to exercise the multi-packet reading
path with a mocked HID interface: verifies all 4 packets are consumed,
all expected language codes end up in all_languages, and that early
timeout or unexpected prefix stops the loop without crashing.

https://claude.ai/code/session_01GhZQeFcV4JFnxdr8K1YGx9

Summary by Sourcery

Improve language enumeration robustness and test coverage for the Poly keyboard firmware integration.

New Features:

  • Add support for dynamically creating or updating the language selection menu on reconnect based on the device’s reported languages.

Bug Fixes:

  • Prevent enumerate_lang from crashing or silently accepting corrupt data by replacing an assertion with a guarded prefix check that stops the read loop on unexpected replies.

Enhancements:

  • Ensure the language selection submenu is reused and refreshed instead of recreated, updating its title and entries to match the current language list.

Tests:

  • Add unit tests for PolyKybd.enumerate_lang to verify correct handling of multi-packet language lists, early timeouts, and unexpected packet prefixes.
  • Extend mock device tests to cover the full 58-language firmware set and validate language list contents and language switching.

Summary by CodeRabbit

  • Bug Fixes

    • Improved language enumeration error handling to prevent crashes and log warnings gracefully.
  • Improvements

    • Language menu now automatically refreshes when the device reconnects.
    • Language menu items properly update and repopulate when languages change.
  • Tests

    • Added comprehensive test coverage for language enumeration and full firmware language support.

claude added 4 commits June 8, 2026 14:33
…d multi-packet test

The while loop in enumerate_lang correctly reads all N response packets
from the firmware (GET_LANG_LIST, case 8, sends 4 back-to-back). Replace
the assert — which is silently disabled with python -O — with a logged
warning + break so a stale or mismatched reply stops the loop cleanly
rather than crashing in debug and passing corrupt data in optimised mode.

Add tests/device/poly_kybd_test.py to exercise the multi-packet reading
path with a mocked HID interface: verifies all 4 packets are consumed,
all expected language codes end up in all_languages, and that early
timeout or unexpected prefix stops the loop without crashing.

https://claude.ai/code/session_01GhZQeFcV4JFnxdr8K1YGx9
Adds TestPolyKybdMockAllFirmwareLanguages — a test class that initialises
PolyKybdMock with the complete 58-language string from hid_com.c case 8
(the same 4 HID packets the real firmware sends). Verifies the full string
round-trips through enumerate_lang, get_lang_list returns exactly 58
entries, every code is present, and change_language succeeds for all of them.

https://claude.ai/code/session_01GhZQeFcV4JFnxdr8K1YGx9
…t startup

If the keyboard isn't plugged in when PolyKybdHost starts, enumerate_lang
fails during __init__ and keeb_lang_menu is never created. reconnect() was
only calling query_current_lang on later connections, leaving the language
selection menu permanently absent.

Fix: inside the compatible branch of reconnect(), build keeb_lang_menu via
add_supported_lang if it is still None. The check is a no-op on subsequent
reconnects once the menu exists.

https://claude.ai/code/session_01GhZQeFcV4JFnxdr8K1YGx9
Previously add_supported_lang always called menu.addMenu() unconditionally,
so it could only safely be called once. This meant keeb_lang_menu was never
refreshed when a different keyboard (with a different language set) connected.

Two changes:
- add_supported_lang now updates in-place when keeb_lang_menu already exists:
  setTitle + clear + repopulate, keeping the menu's position in the tray.
- reconnect() always calls add_supported_lang inside the compatible branch
  (dropped the keeb_lang_menu is None guard), so swapping keyboards always
  reflects the new device's language list.

https://claude.ai/code/session_01GhZQeFcV4JFnxdr8K1YGx9
@sourcery-ai

sourcery-ai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Reviewer's Guide

Replaces a fragile assert-based protocol check in PolyKybd.enumerate_lang with a logged prefix check and early-exit, updates the UI language menu to be reusable on reconnect, and adds comprehensive language-list tests including multi-packet HID responses and full firmware language coverage using mocks.

Sequence diagram for PolyKybd.enumerate_lang multi-packet read with prefix check

sequenceDiagram
    participant Host
    participant PolyKybd
    participant HidInterface

    Host->>PolyKybd: enumerate_lang()
    PolyKybd->>HidInterface: read_with_lock(15, lock)
    HidInterface-->>PolyKybd: result, reply, lock
    PolyKybd->>PolyKybd: expected = expect(Cmd.GET_LANG_LIST).decode()
    loop while result and len(reply) > 3
        PolyKybd->>PolyKybd: reply = reply.decode().strip('\x00')
        alt reply startswith expected
            PolyKybd->>PolyKybd: lang_str += reply[3:]
            PolyKybd->>HidInterface: read_with_lock(15, lock)
            HidInterface-->>PolyKybd: result, reply, lock
        else unexpected prefix
            PolyKybd->>PolyKybd: log.warning [unexpected reply prefix]
            PolyKybd->>PolyKybd: break
        end
    end
    PolyKybd-->>Host: (result, message)
Loading

Flow diagram for host.add_supported_lang language menu reuse on reconnect

flowchart TD
    A["add_supported_lang(menu)"] --> B["keeb.enumerate_lang"]
    B --> C{"result is True?"}
    C -->|No| Z["Return"]
    C -->|Yes| D["get_current_lang"]
    D --> E["Build title from current_lang"]
    E --> F{"keeb_lang_menu is None?"}
    F -->|Yes| G["keeb_lang_menu = menu.addMenu(icon, title)"]
    F -->|No| H["keeb_lang_menu.setTitle(title)"]
    H --> I["keeb_lang_menu.clear"]
    G --> J["all_languages = keeb.get_lang_list"]
    I --> J
    J --> K["Populate language actions"]
Loading

File-Level Changes

Change Details Files
Harden enumerate_lang multi-packet parsing by replacing the assert prefix check with a logged warning and early exit.
  • Compute the expected GET_LANG_LIST reply prefix once before the loop.
  • Decode and strip NULs from the initial reply and each subsequent packet.
  • Replace the assert on reply prefix with a conditional that logs a warning and breaks the loop when the prefix is unexpected.
  • Preserve the existing loop behaviour of concatenating reply payloads and reading additional packets while the result is true and the reply is non-empty.
polyhost/device/poly_kybd.py
Make the language selection menu robust to reconnects by reusing and refreshing an existing menu instead of recreating it.
  • Ensure add_supported_lang is invoked on reconnect once compatibility is confirmed.
  • When adding supported languages, create the language submenu only if it does not yet exist; otherwise update its title and clear previous entries before repopulating.
  • Base the menu title on the current language code and flag, consistent across first creation and updates.
polyhost/host.py
Add tests to validate full firmware language coverage and enumerate_lang multi-packet behaviour with mocked HID I/O.
  • Introduce a new test module that constructs exact 4-packet GET_LANG_LIST responses and validates that enumerate_lang consumes all packets, builds the language list correctly, and stops cleanly on early timeout or unexpected prefix.
  • Add a mock-based test class initialised with the firmware’s full 58-language string to verify enumerate_lang return value, language list length, presence of all expected language codes, and successful language changes for each code.
tests/device/poly_kybd_test.py
tests/device/poly_kybd_mock_test.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@thpoll83, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 51 minutes and 17 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 168142a1-72ef-4017-8c8a-74c3404d47fc

📥 Commits

Reviewing files that changed from the base of the PR and between fcdb157 and fdbab36.

📒 Files selected for processing (1)
  • tests/device/poly_kybd_test.py
📝 Walkthrough

Walkthrough

The PR adds resilience to firmware language enumeration by replacing assertion-based prefix validation with graceful early termination when multi-packet reads encounter unexpected data. The host menu refresh logic is updated to repopulate the language menu on reconnect. Comprehensive unit tests validate multi-packet reading behavior, error recovery, and full firmware language coverage.

Changes

Language Enumeration and Menu Refresh

Layer / File(s) Summary
Resilient multi-packet language enumeration
polyhost/device/poly_kybd.py
The enumerate_lang() method decodes the expected prefix once and checks reply prefix validity in each loop iteration; on mismatch it logs a warning and breaks early instead of using assert to hard-fail.
Multi-packet enumeration unit tests
tests/device/poly_kybd_test.py
New TestEnumerateLangMultiPacket test class with mocked HID layer validates that all four response packets are consumed, languages from each packet are accumulated, and enumeration stops early on timeout or unexpected prefix in follow-up reads.
Language menu refresh on host reconnect
polyhost/host.py
When PolyHost reconnects with compatible firmware, it refreshes the language menu by calling add_supported_lang(), which now updates the submenu title, clears previous actions, and repopulates language items on each run.
Comprehensive firmware language coverage tests
tests/device/poly_kybd_mock_test.py
New test fixtures define the full firmware language set; new TestPolyKybdMockAllFirmwareLanguages test class verifies enumeration matches exactly, language list contains 58 entries, all expected codes are present, and language change succeeds for every code.

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is mostly complete but missing the required testing checkboxes and version bump label from the template. Add the testing checkboxes section and select an appropriate version bump label (likely 'bump:minor' for new feature) as specified in the template.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: replacing an assert with a prefix check in enumerate_lang and adding multi-packet tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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


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

❤️ Share

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="tests/device/poly_kybd_test.py" line_range="73-82" />
<code_context>
+        self.assertIn("enUS", langs)
+        self.assertNotIn("bgBG", langs)  # P2 was never read
+
+    def test_unexpected_prefix_stops_loop(self):
+        keeb = _make_keeb()
+        junk = _pad(b"X\x00.garbage")
+        keeb.hid.send_and_read_validate_with_lock.return_value = (True, _P1, None)
+        keeb.hid.read_with_lock.side_effect = [(True, junk, None)]
+        ok, _ = keeb.enumerate_lang()
+        self.assertTrue(ok)
+        langs = keeb.get_lang_list()
+        self.assertIn("enUS", langs)   # P1 was processed
+        self.assertNotIn("bgBG", langs)  # loop stopped before P2
</code_context>
<issue_to_address>
**suggestion (testing):** Add a case where the unexpected prefix appears after at least one valid follow-up packet.

Right now the test only exercises a bad prefix on the first follow-up packet. To fully validate the guard, please add a variant where P2 or P3 is valid and a later packet has the wrong prefix (e.g., P2 ok, P3 junk), so we confirm earlier languages are retained and the loop stops immediately on the mid-stream bad prefix.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread tests/device/poly_kybd_test.py
Addresses Sourcery review suggestion: verify that when a junk packet
arrives after at least one valid follow-up (P2 ok, P3 junk), languages
from the valid packets are retained and the loop stops immediately.

https://claude.ai/code/session_01GhZQeFcV4JFnxdr8K1YGx9

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
polyhost/host.py (1)

481-517: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Menu refresh is still gated by connection-state flips.

At Line 481, if connected_now != self.connected: prevents Line 517 from running on True -> True reconnect cycles, so language menu refresh can be skipped even when the attached keyboard changes without an observed disconnected interval.

Suggested direction
-            if connected_now != self.connected:
+            if connected_now != self.connected:
                 self.connected, msg = self.keeb.query_version_info()
                 if self.connected:
                     ...
                     if compatible:
                         self.add_supported_lang(self.menu)
+            elif connected_now:
+                # Refresh menu for same-state reconnect cycles (e.g., device swap not observed as disconnected).
+                # Consider guarding with device identity/version signature to avoid unnecessary rebuilds.
+                self.add_supported_lang(self.menu)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@polyhost/host.py` around lines 481 - 517, The connection-check branch uses
"if connected_now != self.connected:" which prevents refreshing the language
menu when the device remains connected but the attached keyboard or its
capabilities change; modify the logic so that when the keyboard is determined
compatible (compatible is True and self.connected is True) you always call
self.add_supported_lang(self.menu) regardless of whether connected_now differs
from self.connected (either move the add_supported_lang(self.menu) call out of
the connected_now != self.connected guard or add an explicit call in the
compatible True path), keeping the existing compatibility checks and status
updates intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/device/poly_kybd_test.py`:
- Line 36: The parameter annotation for extra_packets in _setup_reads uses an
implicit Optional (list[bytes] = None); update the signature to use an explicit
nullable type such as extra_packets: list[bytes] | None = None (or
extra_packets: Optional[list[bytes]] = None) and add the corresponding typing
import (Optional) if you choose that form; keep the parameter name _setup_reads
and extra_packets unchanged and ensure any stub/type checks still accept None.

---

Outside diff comments:
In `@polyhost/host.py`:
- Around line 481-517: The connection-check branch uses "if connected_now !=
self.connected:" which prevents refreshing the language menu when the device
remains connected but the attached keyboard or its capabilities change; modify
the logic so that when the keyboard is determined compatible (compatible is True
and self.connected is True) you always call self.add_supported_lang(self.menu)
regardless of whether connected_now differs from self.connected (either move the
add_supported_lang(self.menu) call out of the connected_now != self.connected
guard or add an explicit call in the compatible True path), keeping the existing
compatibility checks and status updates intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9ca26af0-7b51-4dfd-9fd6-780cc4c8547d

📥 Commits

Reviewing files that changed from the base of the PR and between 799bdc1 and fcdb157.

📒 Files selected for processing (4)
  • polyhost/device/poly_kybd.py
  • polyhost/host.py
  • tests/device/poly_kybd_mock_test.py
  • tests/device/poly_kybd_test.py


class TestEnumerateLangMultiPacket(unittest.TestCase):

def _setup_reads(self, keeb: PolyKybd, extra_packets: list[bytes] = None):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use explicit optional typing for extra_packets on Line 36.

list[bytes] = None triggers Ruff RUF013; use list[bytes] | None (or Optional[list[bytes]]) for explicit nullability.

Suggested patch
-    def _setup_reads(self, keeb: PolyKybd, extra_packets: list[bytes] = None):
+    def _setup_reads(self, keeb: PolyKybd, extra_packets: list[bytes] | None = None):
#!/bin/bash
# Verify there are no remaining implicit-Optional annotations in test files.
rg -nP --type=py ':\s*[^=\n]+=\s*None' tests
🧰 Tools
🪛 Ruff (0.15.15)

[warning] 36-36: PEP 484 prohibits implicit Optional

Convert to T | None

(RUF013)

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

In `@tests/device/poly_kybd_test.py` at line 36, The parameter annotation for
extra_packets in _setup_reads uses an implicit Optional (list[bytes] = None);
update the signature to use an explicit nullable type such as extra_packets:
list[bytes] | None = None (or extra_packets: Optional[list[bytes]] = None) and
add the corresponding typing import (Optional) if you choose that form; keep the
parameter name _setup_reads and extra_packets unchanged and ensure any stub/type
checks still accept None.

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.

2 participants