Repository navigation
fix(hl2): update seam tests for the transmit tail. Principle XI. - #6126
Conversation
#6104 made the HL2 hold its tune drive through the radio's transmit tail (kTxTailHoldMs, 70 ms) after an unkey, restoring RF drive only when the tail ends. hl2_backend_seam_test still asserted the restore at the unkey itself, so main's Full Suite failed four checks (#6122). The test was wrongly classified as socket-owning during #6104's local runs: its only match was disconnectRadio() against a connectRadio( pattern. tunePowerAndRestore now parks the tail and expires it through the test access friend, and asserts both phases: the tune drive held on the register and the wire right after the unkey, then RF power (including power set during the tune) restored when the tail ends. #4912's health row is checked against the wire in both phases, including a change made during the tail. Restoring at the unkey, or never restoring, fails the test. Refs #6122 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AADGw3HVn5FMMVrx2RQZ9g
|
Admin-merged on the maintainer's approval (self-authored fix-forward for #6122). Head
|
There was a problem hiding this comment.
Issue fit
Yes. #6122 is a genuine full-suite failure on fe1fec5, and I confirmed that SHA is an ancestor of main (git merge-base --is-ancestor-equivalent: it is the parent of the two commits now on top), so this is a real red test on main and not a PR-branch artifact. The four failing assertions in #6122 map one-to-one onto the four check() strings this diff rewrites, and the fifth-wheel one (#4912: health reports the register the wire carries) is explained by state carry-over, which is the part most worth noting: on main, the stale third case left m_rfDriveOwed && m_txTailPending both set, so the following setTxPower(60) took Hl2Backend.cpp:5121's early return and the register stayed on the tune drive. The diff's added expireTxTail() before that block is what makes it pass again, not a changed assertion — that lines up exactly with the body's claim that the product is right and the test was stale.
I also traced the diff's framing against the product instead of taking it on trust: the deferral the test now asserts is Hl2Backend.cpp:5119-5122 (setTxPower early-returns while m_rfDriveOwed && m_txTailPending) plus finishTxTail() at 4959-4971 (m_rfDriveOwed && !m_tuning → applyDrive(m_rfPowerPercent)). Every new assertion is the direct consequence of those two, including the asymmetry noted inline. The body's "70 ms" is correct (kTxTailHoldMs = kUnkeyUnmuteHoldMs = 70, Hl2Backend.h:954,963).
The body's misclassification story also checks out: tests/hl2_backend_seam_test.cpp:228 (b.disconnectRadio()) is the file's only substring hit for connectRadio(, and the file opens no socket — no QTcpSocket/QUdpSocket/bind/listen/connectToHost anywhere in it. I went looking for a committed classifier that would repeat the mistake (tools/, docs/agents/tests-ci.md, AGENTS.md, CONTRIBUTING.md) and found no script or recipe carrying that pattern, so this reads as a one-off local grep rather than a repo artifact needing a fix here.
Socket-test register (per the disclosure rule in docs/agents/tests-ci.md:74-80): this PR adds and removes no socket-owning test. hl2_backend_seam_test binds nothing; its tests.cmake:1593-1596 block links Qt6::Network only for MetisClient's types.
Scope
| File | What it changes | Claimed by title/body? | Verdict |
|---|---|---|---|
tests/hl2_backend_seam_test.cpp |
Three test-access helpers (parkTxTail, txTailPending, expireTxTail); tunePowerAndRestore split into pre-tail / post-tail phases; one new #4912-during-the-tail block |
Yes — "Change (test only)" | In scope |
One file, one function, no product code. Everything in the diff is explained by the issue. No new public surface, no deleted guard, no CHANGELOG.md entry, no settings or capability touch — nothing for the governance or preference audits to bite on (this changes no UI, default, or behavior).
Blockers
None.
Nits
- The three helpers at lines 113-119 are byte-identical to
tests/hl2_tx_gate_test.cpp:82,106,111-114. Not worth churn now, but the second copy is the point where a shared test header starts paying for itself. - Case A asserts
wireDrive(b) == -1and case B assertswireDrive(b) == driveFor(10)for what the labels describe as the same claim. Both are right — see the inline comment — but it costs the next reader a trace. One clause of comment would fix it. - Nothing here exercises the timer edge (
expireTxTailstops the timer and callsfinishTxTail()directly), so a brokentimeout → finishTxTailconnect would leave this test green. That is covered inhl2_tx_gate_test.cpp(txTailArmedat :514 and a real 20 ms hold at :633), so it is a boundary rather than a gap — mentioning it only so the division of labour is on the record.
What I tried to break
- "The test would pass against the unfixed code." It would not. Remove the deferral so the restore lands at the unkey and
reg() == driveFor(10)fails in all three pre-tail checks; makefinishTxTail()skipapplyDriveand all three post-tail checks fail. That matches the body's mutation table, which I verified by reasoning againstHl2Backend.cpp:5119-5122and4959-4971rather than by running it. - State left behind for the next case. Each case ends with
expireTxTail, som_txTailPending/m_rfDriveOwedare clear at the nextsetTune(true), andbeginTxTail'sif (!m_txTailTimer->isActive())guard (:4946) can re-arm because the helper stops the timer first. The parked hold is the only writer ofm_txTailHoldMs— the product never assigns it — so parking cannot be clobbered mid-test. - Cross-contamination from the parked hold or a leftover 70 ms timer.
parkTxTailis scoped totunePowerAndRestore's own local backend; other functions in this file that unkey (manualMoxCancelsCwHang) use their own backend, andm_txTailTimeris a child of it, so nothing survives the scope. No ordering dependency between test functions. - Other assertions #6104 should have broken but this PR does not touch.
finishTxTailalso defersapplyBandFilter("unkey"); I grepped this file and the sibling HL2 tests for post-unkey band-filter and drive-restore assertions and found none that the tail would invalidate, consistent with #6122 listing exactly four failures. - Could not check: whether the test actually passes.
hl2_backend_seam_testis deliberately absent from.github/ci-test-gate.txt, so the seven green checks one3dda09prove it compiles and nothing more — no CI job on this PR executes it. The "passes" and mutation evidence are the author's local runs, which I found internally consistent but did not reproduce; I have no build or test run here, and every finding above is reasoned from source. The real confirmation is the next full-suite run on main, where sticky #6122 (fp 32fb57419e1f) should close itself.
Recommendation
Approve with nits. Test-only, correctly scoped to a red-on-main test, and the new assertions are the honest consequence of #6104's deferral rather than the implementation asserted back to itself — the pre-tail phase is the half that would catch a regression to the old restore-at-unkey behavior, which is the right place to spend a check. Next step: merge and let the following full-suite run close #6122; the nits are comment-level and need not hold it.
🤖 aethersdr-agent · cost: $5.0231 · model: claude-opus-5
| static void parkTxTail(Hl2Backend& b) { b.m_txTailHoldMs = 60 * 60 * 1000; } | ||
| static bool txTailPending(const Hl2Backend& b) { return b.m_txTailPending; } | ||
| static void expireTxTail(Hl2Backend& b) | ||
| { | ||
| b.m_txTailTimer->stop(); | ||
| b.finishTxTail(); | ||
| } |
There was a problem hiding this comment.
These three are byte-identical to tests/hl2_tx_gate_test.cpp:82,106,111-114 (same parked hour, same stop-then-finishTxTail() body). Fine as is — separate executables, so no ODR concern — but worth noting that the HL2 tail now has two copies of its test seam, and a shared tests/ header is the obvious home if a third appears.
Worth recording why the direct call is the right choice here: stopping the timer first means the check proves what finishTxTail() decides, not that the timer was stopped — same discipline as expireHang above. The cost is that the timeout → finishTxTail connect (Hl2Backend.cpp:348) is not exercised by this file; hl2_tx_gate_test.cpp:514,633 covers that with txTailArmed and a real 20 ms hold.
|
|
||
| b.setTune(true, 10, authority.operation); | ||
| b.setTune(false, 10, authority.operation); | ||
| check(Access::txTailPending(b) && reg() == driveFor(10) && Access::wireDrive(b) == driveFor(10), |
There was a problem hiding this comment.
Both this == driveFor(10) and case A's == -1 at line 348 carry the same label ("holds the tune drive through the tail"), but they assert it through different mechanics, and the difference is drain order rather than semantics: case A's preceding check already called wireDrive(), which consumes the queued banks, so -1 means "the unkey queued no new drive"; here nothing drained the bank setTune(true) queued, so the last drive on the wire is still the tune drive. Both are correct against MetisClient's queue-on-change behaviour — it just costs the next reader a trace to see why the same claim gets opposite expected values twelve lines apart. One clause would settle it, e.g. // nothing drained since setTune(true), so the tune bank is still the last one queued.
| // #4912 inside a tail: the request moves, the register and wire do not. | ||
| b.setTune(true, 10, authority.operation); | ||
| b.setTune(false, 10, authority.operation); | ||
| Access::wireDrive(b); | ||
| b.setTxPower(70); | ||
| check(health(b, "rfPowerPercent").toInt() == 70 && reg() == driveFor(10) | ||
| && Access::wireDrive(b) == -1, | ||
| "#4912: during the tail, health reports the tune register the wire still carries"); |
There was a problem hiding this comment.
This is the strongest of the added checks, and worth saying why: it is the only one that pins setTxPower's tail guard at Hl2Backend.cpp:5121 (m_rfDriveOwed && m_txTailPending → return) as a distinct path from the m_tuning guard one line above. Delete just the tail guard and this block fails while every other check in the function still passes, since m_tuning is already clear by here — so it is not redundant with the mid-tune case at line 362.
It is also the check that fixes the fourth #6122 failure by side effect: on main the preceding setTxPower(60) hit this same guard with carried-over state, which is why #4912: health reports the register the wire carries failed without its assertion ever being wrong. Calling that out in the body would make the four-for-four mapping obvious to a reader diffing against the issue.
…not the backend's own LAN-only link (#6125) ## Summary Fixes #6121. WAN/SmartLink receive edits reached a gate that checked only FlexBackend's undialed LAN connection, so tuning, mode, filter and AGC requests never reached the radio. - Gate receive intents on `RadioModel::isConnected()`, which checks LAN, WAN and backend connectivity. The WAN check also works without a LAN connection object. If a vended LAN connection is down, a connected backend can still establish connectivity; this includes the simulator's direct backend path. - Log a disconnected receive intent through `aether.protocol`. Do not emit `commandDropped`, whose UI consumer reports an unsupported control rather than a disconnected session. - Cover both a backend without a LAN connection and the real FlexBackend with its LAN connection undialed. Inject WAN connectivity without opening a socket. The real Flex fixture checks exact tune/mode/filter/AGC command strings through the injected slice command sink, disconnected refusal, warning output, and absence of the misleading UI signal. No new protocol verb, capability, setting, dependency, or transmit path is added. ## Constitution principle honored Principle VIII — Evidence Over Assertion. The reporter bisected the regression to #5907 (`142f1d63` versus `e48a242f`). Local mutation checks independently demonstrate that the regression suite detects the broken connectivity gates and diagnostics. ## Validation - macOS arm64, pinned Qt 6.12.0: full app build and focused `receive_intent_routing_test` pass; 50 assertions. After incorporating main #6111, both the receive test and neighboring `transmit_seam_drop_notice_test` pass (2/2 CTests). - Four independently rebuilt mutants fail: backend-only receive gate; LAN check masking a connected WAN link; restored unsupported-control signal; removed disconnected warning. The correct implementation is restored and rebuilt afterward. - Agent automation bridge: isolated offscreen `DEMO-0001`, TX disabled. Tune/mode/filter/AGC readbacks pass for LSB and USB; disconnected tuning produces no unsupported-control status notice. Owned demo process stopped after verification. - Engine-boundary, touchpoint, capability-record and command-plane checks pass. PR-only output matched the original merge base; the final candidate also includes the upstream #6111 capability ratchet decrease from 71 to 70. - The submitter reports real-radio verification on a FLEX-6500 firmware 3.9.18 over WAN and a separate FLEX-6500 firmware 4.2.20 over LAN. This review did not repeat hardware or RF testing. - Full CTest suite was not rerun in this review. The submitter's earlier 620/621 result predates #6126, which repairs the unrelated HL2 seam-test expectations now included from main. ## Checklist - [x] Commits are signed - [x] No new flat-key AppSettings calls - [x] Clean-room implementation - [x] No meter UI change or CHANGELOG edit - [x] Regression tests use injected state/command capture, with no network peer Original implementation generated with Claude Code. Additional review and regression coverage by Codex. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> Co-authored-by: Ozy <Ozy311@users.noreply.github.com>
Brings the branch onto main at d9de044 so CI runs against it. The merge is textually clean. Main's HL2 changes since the branch point (aethersdr#6104, aethersdr#6113, aethersdr#6126) are in TX keying and tune, openReceiverDsp, capabilities and the TX reset in pushInitialState(); none writes Receiver::mode, agcMode or the held AGC. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n disk. Principle VIII. (#6136) ## Problem The weekly **ASan + UBSan** sanitizer lane has failed in its Build step for about three weeks with `final link failed: No space left on device`, so it has tested nothing in that time. About 250 of the test executables statically link the whole of `libaethercore`. In a Debug ASan build each of those averages about 200 MB, and together they outgrow the runner's ~83 GB of free disk. The runner's `/` and `/mnt` are the same 145 GB disk, so building on `/mnt` doesn't help. Trimming debug info (`-g1` plus compressed debug sections) still measured 127 GB locally. ## Fix - **`CMakeLists.txt`:** a new option, `AETHER_SHARED_CORE` (default **OFF**), builds `libaethercore` as a shared library with position-independent code, because the vendored static libraries end up inside it. On Windows it's a configure error. Release, ordinary and TSan builds keep the static core unchanged. - **`.github/workflows/sanitizers.yml`:** the ASan entry passes `shared_core: "ON"`, and TSan passes `"OFF"`. The Build step prints `df -h /` before and after the build, including when the build fails (it keeps the build status and exits with it after the second reading). - **`.github/workflows/sanitizers.yml`, reporting:** every report step was gated on ctest's exit code, so a Configure or Build failure skipped them all, with no artifact and no tracking issue. That is how the lane went dark. A new step runs only when Configure or Build failed. It writes a report naming the step, with a stable per-step fingerprint, and the artifact and tracking-issue steps also run on that path. - **`.github/workflows/static-checks.yml`:** the "Sanitizer option configures" job gains a fifth case: `-DAETHER_SHARED_CORE=ON` must generate a `libaethercore.so` rule, and the default configure must still generate `libaethercore.a`. That's the per-PR check of the option; the sanitizer lane itself only runs weekly. ## Validation **Locally** (Linux, Debug ASan + UBSan): | | static core | shared core | |---|---|---| | Build tree | 127 GB even at `-g1` + zlib | **17 GB** | | Average test executable | ~200 MB | **13 MB** (`libaethercore.so` 668 MB once) | | Test targets that build and link | — | all 591 | Running the full suite under ASan gave **the same failures with the shared core as with the static core**, so linking doesn't change results. **On the runner** (validation run [37141490698](https://github.com/aethersdr/AetherSDR/actions/runs/37141490698), dispatched from this branch): - **Disk:** `/` went from 62 GB used / 83 GB free to **81 GB used / 64 GB free** after the build. - **Tests:** the suite ran to completion. **618 of 623 tests passed.** All 5 failures are existing issues in the code under test: - `offline_health_registry_test`: an ASan stack-use-after-scope in the test itself (`offline_health_registry_test.cpp:95`). - `rx_applet_squelch_reconciliation_test` and `vfo_dsp_launcher_width_test`: a UBSan downcast in `VfoWidget::eventFilter` (`VfoWidget.cpp:6766`) during `~VfoWidget`. - `hl2_backend_seam_test`: main was red when the run started; #6126 has since fixed it. - `weather_radar_loading_test`: a timeout, which also fails locally. - **TSan job:** it failed at Configure with `Qt 6.8.3 found, but AetherSDR requires Qt 6.12`. That's the stale `:tsan` image, fixed separately by #6123, and unrelated to this change (TSan keeps `shared_core: "OFF"`). **Checks:** the new static-checks case, run locally, passes on this head. It fails with its `::error::` when the ON branch is forced to STATIC, and also when the default is forced to SHARED. The Build step exits 1 when the build fails and 0 otherwise, checked under `bash --posix`. The real check is the next scheduled sanitizer run on `main` after merge: it should reach Run tests under sanitizer instead of dying at Build. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fixes #6122
Root cause
#6104 (
fe1fec51) changed the HL2 so that after an unkey it holds the tune drive through the radio's transmit tail (Hl2Backend::kTxTailHoldMs, 70 ms). It restores RF drive only when the tail ends. This is a TX-safety change: the gateware keeps radiating its FIFO plus PTT hang after the MOX-off frame.hl2_backend_seam_teststill asserted the old contract, a restore at the unkey itself. So main's Full Suite failed four checks:#4549: an unkey that bypasses setTune() still restores RF power#4549: releasing TUNE restores RF power#4549: the unkey applies the power set during the tune#4912: health reports the register the wire carriesThe product is right; the test was stale.
This reached main because #6104's local runs excluded
hl2_backend_seam_testas socket-owning. That was a misclassification: the only match wasdisconnectRadio()against aconnectRadio(grep pattern. The source opens no socket.Change (test only)
tunePowerAndRestorenow parks the tail and ends it through the test-access friend (parkTxTail,expireTxTail). It asserts both phases.Immediately after the unkey, the tune drive is still on the register and on the wire. This holds for:
setTune();When the tail ends, RF power is restored on the register and the wire, including the power set during the tune.
#4912: health reports the register the wire carries in both phases. A new check covers an RF power change made during the tail: the requested percent moves, while the register and the wire stay on the tune drive until the tail ends.
No product code changes.
Evidence
hl2_backend_seam_testpasses.Mutations: each one makes the test fail, and each file was restored and checked by sha256.
Full socket-free ctest at -j8: 566 of 620 run. Every test whose source opens a socket was excluded after reading the source, not just matching the name. All pass except
connection_panel_size_test, which already fails locally (Four tests fail deterministically on clean main locally: the a11y announcement fixture never receives focus (offscreen) #5811).hl2_*tests ran and passed. That includeshl2_backend_seam_test,hl2_tx_gate_test,hl2_pan_create_async_testandhl2_receiver_count_restart_paced_test.hl2_*tests bind UDP and were not run:hl2_emergency_stop_testandhl2_receiver_count_restart_test.transmit_seam_drop_notice_testandicom_backend_seam_test.The four static gates and
gen_seam_probe_table.py --checkare clean.🤖 Generated with Claude Code