Repository navigation
fix(tgxl): announce refused relay commands again. Principle XI. - #6138
Conversation
MainWindow connects TunerModel::relayedCommandRefused to showUnsupportedControlNotice() again (#6044). The #6008 fallback rework replaced the wiring block that held it, so since 4d5b989 a TGXL reached by IP only refused OPERATE / STANDBY / BYPASS with no notice. tgxl_refused_notice_wiring_test pins the connection in the wiring source, the one place no behavioural seam reaches. It fails on 4d5b989's source and passes with this change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Issue fit
Yes. No linked issue, and GOVERNANCE.md does not want one here — this is a bug fix with a named root cause, not an architectural change. I verified the cause independently rather than taking the body's word for it: git show 4d5b9893 -- src/gui/MainWindow_Wiring.cpp removes exactly 19 lines from that file, and the only connect( among them is &TunerModel::relayedCommandRefused plus its four-line comment and showUnsupportedControlNotice() body — the auth-greeting block took its place in the same spot after setDirectConnection(&m_tgxlConn). The restore is verbatim: I diffed the eleven removed lines against the added hunk and they match character for character, comment included.
The fix is also complete, which I checked rather than assumed. #6044 added four refusal hookups (relayedCommandRefused, plus antennaChoiceRefused from SpectrumOverlayMenu, VfoWidget and RxApplet); the three antenna ones are all still present at head (MainWindow_Wiring.cpp:5266, :5693, :7163), so this was the sole casualty and nothing else from that PR needs restoring.
Scope
| File | What it changes | Claimed? | Verdict |
|---|---|---|---|
src/gui/MainWindow_Wiring.cpp |
Restores the #6044 connect verbatim, after the fallback block | Yes | In scope |
tests/tests.cmake |
Registers the new pin target | Yes | In scope — but see nit 1 for where the block landed |
tests/tgxl_refused_notice_wiring_test.cpp |
New source pin | Yes | In scope |
Everything in the diff is explained by the stated fix. No new public or protocol surface, no settings key, no capability, no changed default, no deleted guard (the - lines are only the hunk context). CHANGELOG.md is untouched, which is correct.
Blockers
None.
Nits (all non-blocking)
tests.cmakeblock placement — the new block landed betweenspot_label_menu_test's descriptive comment (3569–3571) and itsadd_executable(3581), so that comment now reads as documentation fortgxl_refused_notice_wiring_test. Inline comment with the fix.- The pin is a text search, with the fragility that implies — it would go red on a harmless move of the connect into another MainWindow translation unit, and green if the connect were made against a different
TunerModelinstance. That's inherent to a source pin and the body is honest about it; the precedent is well established in this repo (dbm_range_plausibility_test,pan_dimension_routing_test,split_qsy_settings_test,passive_spots_policy_testall readMainWindow_Wiring.cppthe same way). Inline note on the"});"scan. - What "announced again" means in practice.
showUnsupportedControlNotice()latches onm_commandDroppedNoticeShown(#5263): one status-bar notice per connect session, reset on theRadioModel::connectionStateChanged(true)edge for any family, and the latch is shared with thecommandDroppedpath — so a gate refusal elsewhere in the session consumes the TGXL notice too. With a non-Flex radio connected that is one notice per connect session; with noRadioModelsession at all (direct presence only) it is one notice for the app run. Pre-existing and restored verbatim, not introduced here — worth stating so the maintainer knows the ceiling on the behaviour being restored. - The operator-visible text doesn't name the real cause. The message is "This radio doesn't support that control — nothing was sent to the radio." For a TGXL refusal the cause is "no Flex radio to relay it", which the
qCWarningstates precisely and the status bar does not. Also pre-existing from #6044 — but since the operator-facing announcement is the entire point of this PR, it's the one thing worth a follow-up.
What I tried to break
- "Restored verbatim" — extracted the removed lines from
4d5b9893and diffed them against the added hunk. Identical, including the four comment lines and thelcDeviceswording. Nothing was quietly "improved" in the restore. - "The pin fails on
main" — I can't build here, so I ran the test's exact predicate (indexOf("&TunerModel::relayedCommandRefused"), thenindexOf("});", at), thencontains("showUnsupportedControlNotice()")) in Python against both trees:main→ both claims false, exit 1; PR head → both true, exit 0. The claim holds. Note this is a simulation of the predicate, not a run of the binary — the new test is not on the frozen 7-test per-PR gate (check_ci_test_gate.py: 7 selected, 7 frozen, unchanged), so the greenbuild/check-macos/check-windowsjobs compiled it but did not execute it. It runs at merge in full-suite and weekly under sanitizers, which matches #5405. - Double notice —
wireMeters()has exactly one call site (MainWindow.cpp:2140), and no other translation unit connectsrelayedCommandRefused, so the restore cannot produce two notices per refusal. Moving the block ~50 lines later than its original position is also harmless: the signal can only fire on an operator action, long after either ordering has run. - Incomplete fix — enumerated every
connect(#6008 removed from that file (exactly one) and every hookup #6044 added (four), and confirmed the other three survive at head. There is no second surface left silent. - Compile risk —
lcDevicesis used three other times in the same TU, so the restore needs no new include;check_test_registration.pyandcheck_ci_test_gate.pyboth pass at head, and all 7 checks on741f432are green (CodeQL C++ is absent for the known disk-space reason in #6130, not anything here). - Lifetime and sockets — the lambda takes
thisas its context object with a member-owned sender, so no dangling on teardown. The new test opens no socket (QFile only), and none is added or removed anywhere in the diff.
Findings above are reasoned from the code and from git history, not reproduced at runtime: I have no build, no test execution and no way to drive the GUI, so I could not observe the notice actually appearing in the status bar.
Recommendation
Approve with nits. The regression is real, the cause is confirmed by archaeology rather than assertion, the restore is byte-identical, and the fix is complete across every sibling hookup. The only change I'd ask for before merge is moving the tests.cmake block so spot_label_menu_test keeps its comment; the rest are observations for the maintainer and follow-ups, not merge conditions. I would not ask for a behavioural test here — MainWindow has no construction seam, the source-pin pattern has clear precedent, and tuner_model_test already covers the emission side.
Thanks for catching this one and for naming your own PR as the cause in the body — that's the kind of disclosure that makes the archaeology cheap for the next reader.
🤖 aethersdr-agent · cost: $8.0893 · model: claude-opus-5
GitHub rejected the inline anchors (GitHub API 422: Unprocessable Entity); findings below:
tests/tests.cmake:3572
The new block landed between spot_label_menu_test's descriptive comment (3569–3571: "Right-click on a client-side spot label offers Remove Spot … offscreen QMenu, socket-free") and that target's add_executable at 3581. As merged, the spot comment reads as documentation for tgxl_refused_notice_wiring_test, and the next reader has to diff to find out otherwise.
No suggestion fence because the fix spans lines outside this diff: either move this whole block (3572–3579) down past spot_label_menu_test's set_tests_properties, or move the three spot comment lines down to sit against their own add_executable. Cosmetic, but these comments are the only index of what each of 645 targets covers.
tests/tgxl_refused_notice_wiring_test.cpp:34-38
Worth naming what this pin does and doesn't catch — the header comment is already honest, so this is confirmation rather than a complaint.
I ran these three predicates against both trees to check the body's fail-before claim: on main the indexOf returns -1 and both checks go red; at this head both pass. So the pin does bite.
Two limits it inherits from being a text search:
- A harmless move of the connect into another MainWindow TU (
MainWindow_Session.cppalready carries thecommandDroppedtwin) turns this red with the behaviour intact. indexOf("});", at)finds the lambda's own close only because the body has no nested});. Adding one — aQTimer::singleShot, a nested connect — silently shrinks the window thecontains()searches, so the check could pass on a body that no longer calls the notice. Not worth hardening today; just the thing to watch if that lambda ever grows.
The pattern itself has clear precedent here: dbm_range_plausibility_test, pan_dimension_routing_test, split_qsy_settings_test and passive_spots_policy_test all pin claims in MainWindow_Wiring.cpp the same way.
src/gui/MainWindow_Wiring.cpp:6389-6399
Verified verbatim against what 4d5b9893 removed — the eleven lines match character for character, comment included, so nothing was quietly reworded in the restore. Landing ~50 lines later than the original spot (it was directly after setDirectConnection(&m_tgxlConn)) is harmless: the signal can only fire on an operator action, long after either ordering has run. wireMeters() has one call site (MainWindow.cpp:2140) and no other TU connects this signal, so there's no double notice.
One thing worth knowing about the ceiling on what this restores, since the operator-facing announcement is the point of the PR: showUnsupportedControlNotice() latches on m_commandDroppedNoticeShown (#5263) and that latch is shared with the commandDropped path. With a radio connected the operator gets one notice per connect session — and a capability-gate refusal anywhere else in that session consumes it first. With direct presence and no RadioModel session, the latch never resets, so it's one notice for the app run. All pre-existing, none of it introduced here.
Separately: the qCWarning names the real cause precisely ("need a Flex radio to relay them"), but the status-bar text the operator actually sees is the generic "This radio doesn't support that control." Also from #6044, so out of scope for a verbatim restore — but it's the natural follow-up if this refusal is worth announcing at all.
main gained aethersdr#6138, aethersdr#6115 and aethersdr#6027 since this branch was cut. No textual conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main gained aethersdr#6138, aethersdr#6115 and aethersdr#6027 since this branch was cut. No textual conflict; the touchpoint manifest is regenerated in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #6008 merged (
4d5b9893), a TGXL reached by IP only refuses OPERATE / STANDBY / BYPASS with no notice.MainWindowno longer connectsTunerModel::relayedCommandRefusedtoshowUnsupportedControlNotice(), the hookup #6044 added.Cause. My fallback rework on #6008 (
071ddd0b) replaced the wiring block between two comment markers inMainWindow_Wiring.cpp, and #6044's connect sat inside that block. The #6027 author found it while replaying their branch.Change
src/gui/MainWindow_Wiring.cpp: the fix(gui): non-Flex controls work or refuse aloud. Principle VIII. #6044 connect is restored verbatim, right after the fallback block.tests/tgxl_refused_notice_wiring_test.cpp(new, registered intests/tests.cmake): a source pin that the connection exists and raises the notice.MainWindowcan't be constructed in a test, so this is the one claim no behavioural seam reaches. TunerModel's emission is already covered bytuner_model_test.Proof
main's currentMainWindow_Wiring.cpp("MainWindow connects TunerModel::relayedCommandRefused", "the refusal raises the unsupported-control notice") and passes with this change.AetherSDRbuilds on Linux.Constitution principle honored
🤖 Generated with Claude Code