Skip to content

Expose protocol operations and make the test suite portable - #42

Merged
jonathanstokes merged 14 commits into
stokes-audio:mainfrom
tony-xmelon:codex/coros-4-1-protocol-coverage
Sep 22, 2026
Merged

jonathanstokes merged 14 commits into
stokes-audio:mainfrom
tony-xmelon:codex/coros-4-1-protocol-coverage

Conversation

@tony-xmelon

@tony-xmelon tony-xmelon commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • expose inhibited_modules(), set_device_name(), undo(), and redo()
  • add offline wire tests and guarded, reversible hardware coverage
  • make hardware-gate paths and diagnostics portable on Windows
  • make the historical namespace hash tolerant of Git CRLF conversion
  • write generated catalog files explicitly as UTF-8
  • baseline watchdog threads so combined offline/hardware runs do not interfere
  • update api.md, manual-coverage.md, the hardware guide, and the changelog
  • preserve the measured profile-specific Version announce behavior after syncing fix: the seam's first hardware run - correct the burst record, and skips are not regressions #53

This is PR A from the split requested in review. The CorOS 4.1 catalog and profile evidence remain isolated in #44.

Related issue

Split and revised in response to the review on this PR.

Testing

  • final head: cb4d96e
  • python -m pytest -q: 2702 passed, 3 skipped
  • focused post-sync hardware-gate/report tests: 51 passed
  • python -m mypy: passed
  • git diff --check: passed

Hardware

No compatible unit is available for a full final-head run: the attached standard Quad Cortex runs CorOS 4.1.0, while this PR deliberately excludes the CorOS 4.1 snapshot now isolated in #44 at the maintainer's request. Consequently the full suite correctly refuses its three missing 4.1 generated snapshots. A maintainer with the baseline CorOS 4.0.1 profile will need to run pytest tests/hardware --hardware before merge.

The four operations added here were nevertheless run individually on final commit cb4d96e against the standard Quad Cortex on CorOS 4.1.0, with read-back/device confirmation and restoration:

--verifies set_device_name
tests: 1 passed, 0 failed, 0 errored, 0 skipped
passed: ['set_device_name'] (1)
===================== 1 passed, 101 deselected in 16.07s ======================

--verifies undo
tests: 1 passed, 0 failed, 0 errored, 0 skipped
passed: ['redo', 'undo'] (2)
===================== 1 passed, 101 deselected in 52.77s ======================

--verifies inhibited_modules
tests: 1 passed, 0 failed, 0 errored, 0 skipped
passed: ['inhibited_modules'] (1)
===================== 1 passed, 101 deselected in 12.40s ======================

set_device_name was read back and restored. Undo and redo reversed and reapplied a disposable scratch-preset edit, after which the original preset state was restored and the scratch preset removed. inhibited_modules returned both optional module flags from the unit.

Checklist

  • Opened as a draft; Hardware limitation and targeted runs are recorded above
  • Tests pass locally
  • Added or updated tests for the change
  • Updated documentation for the public API changes
  • No CorOS 4.1 catalog changes bundled in

Upstream sync — 2026-09-12

Merged current upstream main at 640a8e9 (including merged PRs #63–#65) into this branch. Current head: 5b9bca4

Full offline Windows suite: 2727 passed, 3 skipped; mypy: clean across 46 source files; git diff --check: clean.

The Windows path-separator and CRLF-fixture portability checks pass on this branch because their fixes are part of #42.

Upstream sync - 2026-09-19

Merged upstream main at cfe39cc6592148d4196fa3028c2776f6618c41b6 (including maintainer PRs #70-#75) and resolved overlaps in favor of the current upstream API and evidence model.

Current head: f0ac2e4039d058ca4fa96efea920f641c9ac9194

Focused validation: 4 focused protocol tests passed; the updated manual-coverage count check passed.

GitHub CI on this exact head is green: build + twine check, mypy, and tests on Python 3.11, 3.12, and 3.13 all pass.

No new maintainer comments were posted during this sync. This PR is ready for maintainer re-review; the displayed CHANGES_REQUESTED decision is the earlier review and has not yet been refreshed.

@jonathanstokes jonathanstokes 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.

Thanks @tony-xmelon. The four new operations are exactly the kind of thing I want in here, and the Windows fixes are overdue. I'd like to get those in quickly.

The catalog refresh is where I have to slow down. My unit is on 4.0.1 and I'm keeping it there for a while, so I can't verify the 4.1 constants, and tests/hardware/test_generated_constants.py would fail on my unit the day this merges. The doc changes about PresetDirty and the connect burst were also observed on 4.1, so I'd rather record them as 4.1 findings next to the 4.0.1 ones than replace what I measured.

Where I want this library to go is support for both firmware versions at once. Connect, detect what we're talking to, and hand back the right client and device object for it. That's the same shape I asked for on PR #31 for the Mini. Once that exists, a 4.1 catalog has a home that doesn't break 4.0.1 users.

So, if you don't mind, could you split this in two?

PR A, mergeable once I verify it on 4.0.1:

  • inhibited_modules(), set_device_name(), undo(), redo() and their offline wire tests
  • the Windows fixes: as_posix() in the gate, UTF-8 on the generator writes, the CRLF-tolerant fixture hash
  • the watchdog thread baselining in test_state.py, with a comment saying it's for the combined run
  • the api.md, manual-coverage.md and changelog entries for the four operations
  • the new hardware test for name and undo/redo

PR B, after we agree on the two-version design:

  • the regenerated models.py, options.py, params.py and the 412 to 420 counts
  • the renamed constants and the eleven removed option enums, declared as breaking with an old-to-new map
  • the PresetDirty, connect-burst Version count and dropped-read observations, recorded as 4.1.0 findings with the capture behind them
  • the RecallPreset retry and the _retry_read helper, since the drops were seen on 4.1
  • the loosened hardware assertions that came with those observations
  • the STEERING and protocol.md baseline change, which I'll write as an ADR

I have other smaller feedback I'll save for the split PRs. If splitting is more than you want to take on, say so and I'll carve PR A out myself and credit you on it.

Stokes

@tony-xmelon
tony-xmelon force-pushed the codex/coros-4-1-protocol-coverage branch from acf3ffe to 0ead41d Compare September 3, 2026 20:25
@tony-xmelon tony-xmelon changed the title Cover CorOS 4.1 protocol state and make the full suite portable Expose protocol operations and make the test suite portable Sep 3, 2026
@tony-xmelon

Copy link
Copy Markdown
Contributor Author

Split as requested. This PR is now PR A: the four operations, offline/hardware coverage, and the Windows/combined-run fixes only. The CorOS 4.1 catalog, retry, and observation-driven assertions are isolated in draft PR #44, with the breaking-name migration map and the version-design/capture/ADR work called out as remaining. I also rebased PR A onto current main.

@tony-xmelon

Copy link
Copy Markdown
Contributor Author

Thank you, Stokes, for the swift and detailed feedback — it was very helpful.

I have split the work as requested:

We have more potentially useful work locally, particularly around ModelRepo clone/parameter inheritance and metadata, native local backups, and additional remote-control surfaces. We will keep these as small, focused contributions and test them thoroughly — offline and on hardware where applicable — before submitting anything further.

Two adjacent areas are deliberately not being claimed as finished pyquadcortex features yet: our tap-tempo control currently uses the documented MIDI CC #44 rather than a verified HID operation, and although the protobuf exposes ScreenshotMessage, we have not yet established and hardware-tested its complete request/reply flow. If MIDI-backed helpers fit the project's scope, or if you have pointers on the intended Screenshot flow, those would be especially welcome.

Any other feedback on scope, API shape, testing expectations, or preferred priorities is very welcome. Thanks again.

@tony-xmelon

Copy link
Copy Markdown
Contributor Author

As promised, the additional work has now been split into small, independent PRs:

Each was branched directly from main, reviewed independently, and run through focused tests, the applicable full offline suite, mypy, build/package checks, and live hardware checks where the behavior depends on the device.

The screenshot route is now established: a bare Screenshot{READ} is ignored, but a request carrying the preset display-folder name, factory flag, and slot returns an 800 x 384 PNG. #49 deliberately exposes this as preset_screenshot() rather than claiming arbitrary live-screen capture.

Tap tempo remains unsubmitted because the only verified remote route is the documented MIDI CC #44, not a HID/protobuf operation in this client. I did not want to submit a speculative API. Any guidance on whether MIDI-backed helpers belong in this project—or any other priorities, API-shape feedback, or testing pointers—is very welcome. Thanks again.

@tony-xmelon
tony-xmelon force-pushed the codex/coros-4-1-protocol-coverage branch from 0ead41d to 83d54ae Compare September 3, 2026 21:29

@jonathanstokes jonathanstokes 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.

Thank you for splitting this so quickly. This half is clean, and I want to get it in.

What I tested on my unit (CorOS 4.0.1, firmware d14e). All three hardware tests pass here: the device-name round trip, the inhibited-modules read, and the undo/redo test against a scratch preset. So none of these four operations is 4.1-only, and the docstrings can say they are confirmed on both versions. For the undo test I made a copy of my loaded preset in an empty slot, pointed the env var at that copy, and deleted it afterwards. That worked well and I think it is the shape the test itself should use (see the inline notes).

While testing I also found one thing about the unit that matters for this PR. A Version READ is answered twice: the full reply, and about 1 ms later an echo of the READ that carries only action. version() correlates by type, so two calls close together alternate between the full reply and the empty echo. I saw this directly: five calls in a row returned full, empty, full, empty, full. The name test reads version().custom_name and writes it back, so on a bad turn it would write an empty name. The 1 s sleeps happen to hide this. Details inline.

Two things I could not attach to a line, because the files are not in this diff:

  • docs/protocol.md has no coverage-table rows yet for set_device_name, undo/redo and inhibited_modules. The rows were in the original PR and got lost in the split. The table header also says every operation was exercised on 4.0.1, so the rows can now say "4.0.1 and 4.1.0" honestly.
  • docs/domain-model.md still says (around line 1145) that there is no grid-level undo a host can trigger, and rows 1274 and 1300 still give "never driven" as the reason for omitting it. This PR proves otherwise, so those lines need a small update.

Everything else is inline. Items marked Nit: are optional. Thanks again, and please tell me if any of this is unclear.

Stokes

Comment thread tests/hardware/readme.md Resolved
Comment thread pyquadcortex/protocol/client.py Resolved
Comment thread tests/test_state.py Resolved
Comment thread scripts/generate_models.py Resolved
Comment thread tests/test_namespace.py Resolved
Comment thread tests/hardware/test_settings_values.py Resolved
Comment thread docs/api.md Resolved
@tony-xmelon tony-xmelon mentioned this pull request Sep 3, 2026
3 of 4 tasks
@tony-xmelon

Copy link
Copy Markdown
Contributor Author

One correction to my previous note: by remote tap I meant clicking the physical Quad Cortex touchscreen from the app, not tap tempo. We have now recovered and hardware-verified that Cortex Control flow and submitted it separately as #50.

#50 adds live 800 x 480 screen capture plus raw-coordinate touchscreen taps. It is distinct from #49, which renders a preset screenshot rather than capturing the live framebuffer. The touch path was tested twice on CorOS 4.1: it opened the intended block's Parameter Editor, returned cleanly to the Grid, and left the preset hash, slot, scene, and dirty state unchanged.

Thank you again for the swift feedback and for prompting the split. We will continue submitting further additions only as small, independently reviewed and thoroughly tested PRs. Any further pointers on scope, API shape, priorities, or protocol expectations are very welcome.

@tony-xmelon

Copy link
Copy Markdown
Contributor Author

Thank you for the swift and detailed feedback, Stokes. I pushed f64984b addressing the review: Version replies are filtered to the full device response; device-name presence/type handling is tightened; the undo/redo hardware test now creates and cleans up its own scratch preset and restores the original state; the controlled evidence wording and coverage tables are corrected; watchdog baselines are inclusive; and the optional fixture/readability nits are included. The complete offline suite is green (2,543 passed, 3 skipped), mypy is clean, and git diff --check is clean.\n\nI have also split the broader work into focused PRs and will submit further additions in similarly small, tested pieces. If you have any further feedback or pointers on scope, protocol evidence, or preferred API shape, they are very welcome.

@tony-xmelon

Copy link
Copy Markdown
Contributor Author

@jonathanstokes All requested changes are now pushed, each inline comment has a specific response, and all 15 review conversations are resolved. When convenient, could you please re-review this PR? Thank you again.

jonathanstokes added a commit that referenced this pull request Sep 4, 2026
…ies on

Pre-PR self-review findings, addressed:

- The test held the firmware half of the identity predicate and not the
  serial half; a predicate on app_fw_version alone passed it. Both halves
  are now asserted.
- Listener-before-wake ordering was pinned for request() only. A sibling
  test in test_transport.py holds it for await_broadcast, which version()
  now uses and the identity entry's arrival count relies on.
- LoopbackTransport's replies fallback is scoped to the reads that really
  answer with a same-type message (SAME_TYPE_READS), so any other type
  filed in the wrong dict still hits the loud error.
- ReplyingTransport asserts the trigger sent exactly one message before
  keying the canned reply off it.
- The CLI's _open_unconnected docstring no longer gives a stale reason.
- version()'s docstring and the coverage row record the post-fix hardware
  run: five back-to-back calls through the new path all returned the full
  reply.
- ADR-0020: status line matches the file's convention; the version()
  consequence no longer says the fix "lands first" or demands the full
  reply; ATMA = Mini is labelled as inferred from the schema; the catalog
  PR is named as #42, split into #44; the footswitch sentence reads as
  intended and names its source.
- STEERING, CLAUDE.md and architecture.md say the seam is not built yet
  rather than describing it in the present tense, and attribute the
  "d14e on 4.1.0" claim to the contributor.
- protocol.md's new Version section is 4.4, after 4.3, instead of an
  unnumbered heading that swallowed the rest of section 4; the host undo
  measurement behind the domain-model correction has a coverage row.
tony-xmelon pushed a commit to tony-xmelon/pyquadcortex that referenced this pull request Sep 5, 2026
Two contributions arrived from units the maintainer does not have - a Mini
opened over USB (stokes-audio#31) and a CorOS 4.1.0 catalog (stokes-audio#42, then stokes-audio#44) - and
neither could be verified on the unit that defines the baseline. ADR-0020
decides the shape before the seam is built: a connection resolves a device
profile from device_type and zenos_git_hash in the unit's own Version
reply, an unknown pair refuses to connect, profile= is the deliberate way
to measure an unmeasured unit, and everything that differs by firmware or
model lives on the profile. Profiles are named by CorOS version, never by
app_fw, because d14e is reported on both 4.0.1 and 4.1.0.

STEERING sections 2 and 6, the decision table and change log, the
protocol.md header, the architecture checklist, README, contributing.md
and CLAUDE.md now describe profiles with evidence levels instead of one
baseline. protocol.md gains the record behind the version() fix, "A
Version READ is answered twice". domain-model.md corrects the claim that
no host can trigger undo (UndoRedo{UPDATE, undo: true} does, measured on
4.0.1 and reported on 4.1.0) and records the U+00A4 separator seen in 27
labels of the 4.1.0 catalog and none of 4.0.1.

No code branches on a version string yet. protocol.models, params and
options still mean the Quad Cortex 4.0.1 snapshot, and CC_VERSION still
announces 4.0.1.
…tocol-coverage

# Conflicts:
#	docs/domain-model.md
#	docs/protocol.md
#	pyquadcortex/protocol/client.py
@tony-xmelon

Copy link
Copy Markdown
Contributor Author

Updated onto current main in b3cd54c and resolved the overlap with ADR-0020/#51 by keeping the maintainer's await_broadcast-based version() implementation and partial-identity semantics. The four contributed operations remain isolated here. Validation: 2,544 passed, 3 skipped; mypy clean; git diff --check clean. GitHub would not allow me to submit a formal review request through the API, so @jonathanstokes, could you please re-review when convenient?

jonathanstokes added a commit that referenced this pull request Sep 6, 2026
The suite is the instrument that measures a profile, and it could not be run
against the unit that needs measuring: it connected with no profile= and
refused before a test could look. --profile CLASSNAME resolves a name over
QuadCortex and everything registered under it, and an unknown name stops the
run naming the real ones.

The 4.1 docstring also cites its evidence: PR #42's description, 2026-09-03,
2742 passed / 8 skipped on CorOS 4.1.0 / d14e, by tony-xmelon - a
contributor's report the maintainer has not reproduced.
@tony-xmelon

Copy link
Copy Markdown
Contributor Author

Updated onto the merged ADR-0020/device-profile seam from #52 in 4dcafd8. The PR remains limited to the four operations and portability fixes; the new hardware-operation inventory now marks set_device_name, undo, redo, and inhibited_modules explicitly. Validation on the updated branch: 2,698 passed, 3 skipped; mypy clean; git diff --check clean. @jonathanstokes, could you please re-review when convenient? Thank you.

@tony-xmelon
tony-xmelon marked this pull request as draft September 8, 2026 08:41
@tony-xmelon

Copy link
Copy Markdown
Contributor Author

Synced with upstream �2331cc (including #53 and the new #54 contribution policy) at head 50238a1. I moved this back to draft so the required hardware suite can be rerun on this exact final commit before requesting review. Offline validation after the sync: 2702 passed, 3 skipped; mypy clean; git diff --check clean.

@tony-xmelon
tony-xmelon marked this pull request as ready for review September 8, 2026 09:31
@tony-xmelon tony-xmelon mentioned this pull request Sep 8, 2026
5 tasks done
@tony-xmelon

Copy link
Copy Markdown
Contributor Author

@jonathanstokes, thank you again for the detailed review. The requested split and follow-up changes are complete, the final head is cb4d96e, and the PR is marked ready with fresh offline and targeted hardware evidence in the description. GitHub does not permit this contributor account to invoke the re-request-review API here, so please treat this as the re-review request when convenient.

@tony-xmelon

Copy link
Copy Markdown
Contributor Author

Synced with upstream main at 640a8e9 (including #63–#65). New head: 5b9bca4. Full offline Windows suite: 2727 passed, 3 skipped; mypy: clean across 46 source files; git diff --check: clean. The three shared Windows-only baseline checks remain isolated in #42.

…tocol-coverage

# Conflicts:
#	changelog.md
#	docs/api.md
#	docs/domain-model.md
#	docs/manual-coverage.md
#	docs/protocol.md
#	pyquadcortex/protocol/client.py
#	tests/hardware/readme.md
@jonathanstokes

jonathanstokes commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Thanks @tony-xmelon. I re-reviewed this on f0ac2e40.

I did not run the hardware suite. Four things need changing first, and three of them came in with the September 19 sync from main rather than from your earlier work. I checked all fifteen threads from the September 3 review against the code instead of reading the replies, and those are genuinely addressed. This is new ground.

One thing to say up front, because most of what follows is about my unit. You do not have a 4.0.1 unit, and the full suite refuses on yours while the 4.1 snapshots are held back in #44. So 4.0.1 behavior was never something you could check, and I am not treating any of it as something you missed. Catching it is what the maintainer run is for. I am writing it down so the next push has it.

1. The connect-burst test cannot pass on my unit.

tests/hardware/test_model_state.py picks what to expect by counting factory models:

expected_announces = {412: 1, 420: 0}
assert factory_count in expected_announces

412 stopped being the 4.0.1 count on September 15. Model.hidden was read by presence, and that wrongly dropped two Bogna amps. tests/test_models.py now pins the count at 414 and explains why in its comment. The generator builds models.ALL from the same factory_models() call this assertion makes. So my unit reports 414, and the run fails on that line.

There was no way for you to see this. The number only bites on a 4.0.1 unit, your runs were on cb4d96e which is before the sync, and the offline suite cannot reach it either, since tests/hardware/ modules only have to import.

2. That assertion keys firmware behavior on a model count.

CLAUDE.md says anything that differs by firmware or model lives on the profile class, and nothing else tests a version string. A factory-model count is standing in for the CorOS version here. profiles.py is untouched, and the QuadCortex41 docstring already says what to do instead. 420 is the 4.1 count too, so this is 4.1 material that did not get split out. Easy to lose track of in a split this size.

3. A 4.1.0 observation is recorded only in a test docstring.

The note about four 4.1.0 connections carrying no announce answer should sit in docs/protocol.md beside the 4.0.1 record, dated and named. This is an easy one to miss, and it matters, because 4.0.1 and 4.1.0 disagree here, one announce against none. Right now that disagreement is not written anywhere a reader would look. Your 4.1.0 half of it is the part I cannot produce myself, so I do want it kept.

The simplest fix for all three is to put tests/hardware/test_model_state.py back to its base state here, and move the connect-burst observation to #44 where the profile and the 420-model catalog both live. None of the four new operations needs it.

4. The undo test can discard my unsaved edits.

test_undo_and_redo_reverse_and_reapply_a_scratch_edit builds its own scratch preset instead of using scratch_preset, which arrived in the same sync. The fixture opens with a guard the hand-rolled version does not have:

assert qc.preset_dirty() is False, "the loaded preset has unsaved edits; save or reload it first"

If I run the suite with unsaved edits on the loaded preset, the test saves those edits into the copy, then teardown recalls the original and discards them. ADR-0005 asks a successful run to leave the unit as it found it.

Your run passed, and mine would pass too on any day the loaded preset happens to be clean. The guard is for the day it is not. Switching to scratch_preset covers it, and it drops the second scratch name at the same time.

Smaller things, none of them blocking:

  • tests/hardware/readme.md still says "Nothing here saves a preset" at line 78, and the undo test now saves one. The new paragraph went in at the top and the old sentence stayed. Your reply on that thread said the readme now covers the instrument tag and the default scene, and I could not find that part.
  • Lines 27 and 32 of the name test still read custom_name with no HasField. I asked for all three reads and only the first got one. version() returns a partial reply on purpose, and a partial without the name makes that assert fail even when the restore worked.
  • Transport.request(match=) has no caller. Main solved the double answer through await_broadcast(match=) while this was in flight, so the merge left your version behind. Drop it, or tell me why it stays.
  • Six entries in UNMARKED_OPERATIONS have reasons that are now out of date. The new test drives list_presets, save_current_preset, delete_preset, read_preset, wait_for_listing and loaded_position, and their excuses still say no hardware test uses them.
  • QuadCortex41.VERIFIED is still only create_local_backup. A 4.1 user calling set_device_name() gets refused by a method whose docstring says it is confirmed on 4.1.0. That one is on your side of the line, so you can settle it without me.

The four operations are good and the docstrings read well. I want this in.

This is waiting on you now. Once the hardware test file is back to base and the dirty-preset guard is there, I will run the suite on my 4.0.1 unit and we can finish it.

Stokes

@tony-xmelon

Copy link
Copy Markdown
Contributor Author

Thank you for the careful re-review and for distinguishing the 4.0.1 findings from things we could have observed on our 4.1 unit.

All requested changes are now on 68be881:

  • restored tests/hardware/test_model_state.py to the 4.0.1/base connect-burst behavior, removing both the stale 412 count and the count-as-firmware proxy;
  • moved the 4.1 connect-burst observation into Add profile-owned CorOS 4.1 catalog and observations #44's protocol documentation, where the profile-owned 4.1 evidence belongs;
  • rewrote the undo/redo hardware test around the shared scratch_preset fixture and its dirty-preset guard;
  • removed the contradictory save claim and documented the scratch preset's instrument reset/default scene behavior;
  • applied HasField("custom_name") handling to all three device-name reads;
  • removed the now-unused Transport.request(match=) path;
  • refreshed the six stale hardware-inventory reasons; and
  • added set_device_name to QuadCortex41.VERIFIED.

The upstream September 21 changes are merged as well. Exact-head validation on Windows is 2968 passed, 7 skipped; mypy is clean; build and twine checks pass; and git diff --check is clean. I also fixed the new writing-test path normalization on Windows in this PR's portability scope.

The branch is ready for your 4.0.1 hardware run when convenient. Thanks again—the profile guidance and the new shared fixture made the end state materially clearer.

@jonathanstokes jonathanstokes 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.

Re-reviewed on 68be881f, and I ran the hardware suite on my 4.0.1 unit. This is an approval.

The hardware run

Full suite, on this exact head, CorOS 4.0.1 / d14e:

============== operations on QuadCortex (CorOS 4.0.1, MAINTAINER) ==============
tests: 120 passed, 0 failed, 0 errored, 3 skipped; operations measured: 21 of 21 claimed
passed:                                 ['active_scene', 'clear_expression', 'create_local_backup', 'inhibited_modules', 'read_current_preset', 'redo', 'set_bypass', 'set_chain_input', 'set_device_name', 'set_expression', 'set_global_eq', 'set_hold_timing', 'set_input_port', 'set_param', 'set_scene_color', 'set_scene_label', 'set_tempo_mode', 'switch_scene', 'tempo_mode', 'undo', 'update_settings'] (21)
failed or skipped:                      [] (0)
passed, not VERIFIED:                   [] (0)   <- candidates to add
VERIFIED and claimed by a test, failed: [] (0)   <- regressions by name
120 passed, 3 skipped in 285.29s (0:04:45)

All four new operations passed and are named by a test: set_device_name, undo, redo, inhibited_modules. Nothing failed, nothing errored, and the run left the unit as it found it.

The four points from my September 21 re-review

I checked each against the code rather than the reply. All four are genuinely fixed.

  1. tests/hardware/test_model_state.py is byte-for-byte what is on main. The 412 count is gone, and so is the assertion that carried it.
  2. The model count no longer stands in for a firmware version. The revert took it with it.
  3. The 4.1.0 connect-burst observation moved to #44. Nothing left here depends on it.
  4. The undo test takes scratch_preset and gets the dirty-preset guard with it. It is the post-#76 fixture, keyed on the start-of-session flag, not a shape that only still imports.

Of the five smaller ones, three are fully fixed: the readme sentence, HasField on all three name reads, and Transport.request(match=) on the production side.

What I am not blocking on

None of the below can fail CI or the hardware run, and none of it changes what a 4.0.1 user gets. I am happy for this to merge as is and for these to come later, but they are all real and four of them are the shape this repo cares most about: a true statement replaced by a false one.

Two of the six refreshed reasons in UNMARKED_OPERATIONS are not true. This is my point (d), answered a little too eagerly. Suggestions inline.

docs/protocol.md now has two undo/redo rows. Line 2159 is mine, already on main, and it says the operations "land in PR #42". This PR added a second row at 2095 instead of updating that one. After merge a reader gets two different answers, and one of them says the work has not landed yet. The same stale forward reference sits in the prose at line 1786.

undo, redo and inhibited_modules are not in QuadCortex41.VERIFIED. You added set_device_name, which is what I named, but the sentence I wrote about it is now true of the other three word for word.

Three test doubles kept the match= you dropped from the real transport. Harmless today, and a trap later.

Thanks for this. The four operations are clean, the wire bytes are pinned both ways, and the evidence is in the docstring and the coverage table where it belongs.

"read_preset": "the undo/redo test reads its scratch copy to choose an edit, but verifies the history result rather than the read operation itself",
"find_preset": "no hardware test yet; it needs a name that exists on the unit, which the loaded preset supplies - just not written",
"wait_for_listing": "no hardware test yet; it is the polling wrapper round list_presets and says nothing until a write changes a listing",
"wait_for_listing": "scratch_preset teardown uses it to confirm deletion; no test independently verifies its polling and stability contract",

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.

wait_for_listing appears nowhere under tests/hardware/. _release_scratch calls delete_preset and nothing else, so the teardown does not use it and confirms no deletion. The reason it replaced was correct. The marker test only checks a reason is longer than 20 characters, so CI cannot catch this.

Suggested change
"wait_for_listing": "scratch_preset teardown uses it to confirm deletion; no test independently verifies its polling and stability contract",
"wait_for_listing": "no hardware test yet; it is the polling wrapper round list_presets and says nothing until a write changes a listing",

"save_current_preset": "the undo/redo test's scratch_preset fixture confirms a save into a free User slot, but the test verifies undo/redo rather than the save wire shape",
"recall_preset": "no hardware test yet; a recall discards the loaded preset's unsaved edits, so it needs scratch_preset around it",
"delete_preset": "no hardware test yet; only scratch_preset's teardown deletes anything, and a teardown asserts nothing",
"delete_preset": "the undo/redo test's scratch_preset teardown deletes its copy and confirms absence; no test isolates the delete wire shape itself",

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.

The teardown does not confirm absence. _release_scratch deletes unconditionally, and the fixture's own comment explains that a listing guard was tried there and removed on purpose, because list_presets is unreliable on a single READ. delete_preset also returns None rather than raising when the unit does not answer.

Suggested change
"delete_preset": "the undo/redo test's scratch_preset teardown deletes its copy and confirms absence; no test isolates the delete wire shape itself",
"delete_preset": "the undo/redo test's scratch_preset teardown deletes its copy, but the delete is deliberately unconfirmed and a teardown asserts nothing",

``create_local_backup`` has its own dated 4.1.0 capture and is VERIFIED;
``create_local_backup`` has its own dated 4.1.0 capture. The contributed
device-name round trip also verified ``set_device_name``; both are VERIFIED.
other inherited operations refuse under ``Support.VERIFIED`` and run with

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.

Splitting the semicolon left this sentence starting lowercase.

Suggested change
other inherited operations refuse under ``Support.VERIFIED`` and run with
Other inherited operations refuse under ``Support.VERIFIED`` and run with

MEASURED_ON = ("4.1.0",)
EVIDENCE = Evidence.CONTRIBUTED
VERIFIED = frozenset({"create_local_backup"})
VERIFIED = frozenset({"create_local_backup", "set_device_name"})

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.

This is the one thing I would most like settled, and it is yours to settle. What I wrote about set_device_name is now true of three more: on a 4.1.0 unit with the default Support.VERIFIED, qc.undo(), qc.redo() and qc.inhibited_modules() raise ControlNotDrivable, while their docstrings say they are confirmed on 4.1.0. Your evidence for those three looks the same as for set_device_name. If it is, promote them. If you are holding them back on purpose, say why here and I will take that.

Comment thread docs/protocol.md
| connect handshake | `ResetCommsBuffers` + `Version` UPDATE + `ModelRepo` READ + `Connection` + subscribe READs | read-back | the connect gate; state pushes flow only after it |
| version read | `Version{action: READ}` | read-back | two messages come back; `version()` accepts only one carrying `device_serial_number` or `app_fw_version` (section 4.4) |
| `set_device_name` | `Version{UPDATE, custom_name}` | read-back + on-unit | sparse echo, read-back, and restoration confirmed on CorOS 4.0.1 and 4.1.0 |
| `undo` / `redo` | `UndoRedo{UPDATE, undo}` / `{UPDATE, redo}` | read-back + on-unit | a bypass edit was reversed and reapplied on disposable preset copies; empty-history behaviour is unknown |

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.

This table already has an undo/redo row at line 2159, which I wrote: host undo / redo (undo() / redo() land in PR #42). Two rows for one operation, and they disagree on the verification column. The fix is to update 2159 and drop this one, rather than add beside it. Line 1786 needs the same treatment: its (PR #42, where undo() and redo() land) becomes false on merge.

Presence matters here: protobuf also returns false when an optional field was
never sent, and reporting that default as device state would be a guess.
"""
# Read twice because a first state READ can be dropped; both successful

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.

inhibited_modules() goes through _read_state to await_broadcast, which raises TimeoutError when no matching broadcast arrives. There is no retry. So on the unit this comment describes, a dropped first READ fails the test after the timeout rather than being covered by the second pass. The loop repeats the assertion, which is still worth doing; it just is not what the comment claims.

Suggested change
# Read twice because a first state READ can be dropped; both successful
# Read twice so a second reply must also carry explicit fields rather
# than protobuf defaults. A dropped first READ raises; there is no retry.

Comment thread tests/test_client.py
self.sent.append(msg)

def request(self, msg, timeout=5.0):
def request(self, msg, timeout=5.0, match=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.

You dropped match= from the real Transport.request, but the doubles here, in tests/test_device.py and in tests/test_state.py kept it, and the last two implement the match semantics fully. The doubles are now wider than the thing they stand for: a later self._t.request(msg, match=...) in client.py would pass the whole offline suite and raise TypeError against a real unit. Worth removing from all three while it is still free. self.last_match on the next line has no reader either.

@jonathanstokes
jonathanstokes merged commit 06ddd29 into stokes-audio:main Sep 22, 2026
5 checks passed
jonathanstokes added a commit that referenced this pull request Sep 22, 2026
#42 was approved and its hardware suite passed on the 4.0.1 unit. These are the
non-blocking findings from that review, applied rather than left to the author.
None of them changes what the library does.

Two of the six refreshed reasons in UNMARKED_OPERATIONS were not true. The
wait_for_listing entry said "scratch_preset teardown uses it to confirm
deletion"; wait_for_listing appears nowhere under tests/hardware/, and
_release_scratch calls delete_preset and nothing else. The reason it replaced
was correct, so it is back. The delete_preset entry claimed the teardown
"confirms absence"; the fixture's own comment explains that a listing guard was
tried there and removed on purpose, because list_presets is unreliable on a
single READ, and delete_preset returns None rather than raising when the unit
does not answer. Nothing catches either one: the marker test checks only that a
reason is longer than 20 characters.

docs/protocol.md carried two undo/redo rows in one table. The maintainer's row
was already there and said the operations "land in PR #42"; #42 added a second
row beside it instead of correcting it. After the merge a reader gets two
answers, and one of them says the work has not landed. writing.md asks a
correction to replace the wrong sentence rather than sit next to it, so the two
rows are folded into the original's position. Every fact from both survives: the
2026-09-03 4.0.1 measurement, the contributor's 4.1.0 run, the bypass edit
reversed and reapplied on disposable preset copies, and empty-history behaviour
being unknown. The verification column takes the stronger of the two,
"read-back + on-unit". The same stale forward reference in the prose at section
11 now names undo() and redo() as the drivers instead of promising them.

Three test doubles kept a request(match=) argument that the real transport no
longer has, and two of them implemented its semantics fully. A double wider than
the thing it stands for is a trap: a later request(msg, match=...) in client.py
would pass the whole offline suite and raise TypeError against a unit. In
test_client.py the same edit also set self.last_match on every request() call,
which would overwrite the predicate await_broadcast stores there and turn the
assertions that read it into a TypeError on None.

The read-twice comment in the inhibited_modules hardware test said the second
read covers a dropped first READ. It does not: _read_state goes to
await_broadcast, which raises TimeoutError with no retry. Reading twice is still
worth doing, so the loop stays and the comment now says what it is for.

profiles.py starts a sentence lowercase, from the semicolon #42 split.

Not fixed here: undo, redo and inhibited_modules are absent from
QuadCortex41.VERIFIED while their docstrings claim 4.1.0 confirmation, which is
the defect #42 fixed for set_device_name alone. Promoting them needs 4.1
hardware evidence this repository does not have, and that evidence is the
contributor's. It is raised on #42 for him to settle.

No changelog entry: nothing here is visible to someone who installs the package.
jonathanstokes added a commit that referenced this pull request Sep 22, 2026
Two findings from /triage-pr on the first commit, both the same defect this
pull request exists to remove: a sentence the code contradicts.

The merged undo/redo coverage row claimed "read-back + on-unit". The table's
own legend defines on-unit as confirmed on the unit's own screen, and no record
in this repository says anyone watched the screen. Section 11 says "Measured
2026-09-03 on CorOS 4.0.1 by preset read-back". The undo and redo docstrings
say a bypass edit was reversed and reapplied, with no screen reading.
test_identity_and_history.py asserts only through read_current_preset(). The
fold took the stronger of the two columns as though strength were the question;
it is not, the measurement is, and the row it replaced already had it right.
Back to "preset read-back", which is the maintainer's own wording. Nothing
catches this: no test parses the Verified-by column.

The section comment above the preset-library entries still said the
scratch_preset fixture exists "but no test uses it yet". #42 landed the test
that uses it, and the two entries immediately below the comment both describe
that test. Four consecutive lines gave two answers to the same question.

Also corrected: "drive it" in section 11 pointed at the 4.1.0 measurement
rather than at the messages, because that measurement is the nearest
antecedent.

Dismissed: the merged row spelling the redo half `{redo: true}` rather than
`{UPDATE, redo}` - the action is carried by the first half and this is the
pre-existing row's shorthand.

Raised for later, not here: nothing pins the test doubles' request() signature
against protocol.transport.Transport.request, which is why #42 could widen
three doubles and stay green. That wants a source-reading test of its own.
jonathanstokes added a commit that referenced this pull request Sep 22, 2026
Six follow-ups from #42, four of them a statement the code contradicts
jonathanstokes added a commit that referenced this pull request Sep 22, 2026
Git already detects them, so this changes no behaviour today. It is stated
because the file exists to serve contributors on Windows, and this repository
added its .gitattributes rule after CRLF fixture failures in #42 and #45.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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