Skip to content

test(audio): pin which device-id list answers presence. Principle VIII. - #6387

Merged
rfoust merged 3 commits into
aethersdr:mainfrom
on8st:fix/audio-device-id-lists
Oct 12, 2026
Merged

rfoust merged 3 commits into
aethersdr:mainfrom
on8st:fix/audio-device-id-lists

Conversation

@on8st

@on8st on8st commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Refs #6378

Answers the two forward notes on the approving review of #6378: the
id-list helpers move into a header a test can reach, and the user docs
gain the Linux clause.

Limits

  • Built and run on macOS only (Qt 6.12.0). The Linux rule is compiled and
    asserted on macOS through an explicit parameter; the Q_OS_LINUX
    default that selects it was not built or run here. One check reads that
    default and can only fail for the Linux case on a Linux lane. No per-PR
    lane runs it:
    this test is in no ctest -R filter in ci.yml, so its
    first run on Linux is full-suite.yml after merge. The Linux build job
    compiles that arm.
  • No behaviour change is intended on any platform. That rests on reading
    every caller before and after (below), not on an in-app run.
  • handleAudioDeviceListChanged() still has no test of its own. What is
    pinned is the list each question reads: the handler builds each
    direction's change with audioDeviceListChange(), and the test calls
    the same function. A handler that filtered the ids before passing them
    in would not be caught.
  • The accepted-prompt call still takes the enumerated list directly from
    the handler, as it does on main.

What moved

Out of the anonymous namespace in src/gui/MainWindow.cpp, into
src/core/AudioDevicePreferencePolicy.h, as pure functions over
QByteArray ids:

before (MainWindow.cpp) now (policy header)
isTransientAudioDeviceId(id), body under #ifdef Q_OS_LINUX isLinuxTransientAudioDeviceId(id), always compiled; isTransientAudioDeviceId(id, rule)
audioDeviceIds(devices) arrivalAudioDeviceIds(enumeratedIds, rule)
newlyAddedAudioDeviceIds, removedAudioDeviceIds, containsAudioDeviceId, and the two designated initialisers in the handler audioDeviceListChange(AudioDeviceListSnapshot, rule); the snapshot's fields are named, so the two lists and the two ids cannot be swapped

rule is AudioDeviceChurnRule::{None, Linux} and defaults to
kHostAudioDeviceChurnRule, which is Linux under Q_OS_LINUX and None
elsewhere. MainWindow.cpp keeps enumeratedAudioDeviceIds(devices), the
QAudioDevice to id extraction, and passes its result on.

The helpers extend the existing policy header instead of a new one: they
build AudioDeviceListChange, whose presentIds and addedIds fields
that header already documents, and its test already includes it. The
touchpoint tag note names the addition; the manifest is regenerated.

Why behaviour is the same:

  • Linux: the five patterns are copied unchanged (.monitor,
    pulse_input_loopback as a prefix, auto_null, echo-cancel,
    combined), and the default rule is Linux exactly where the old
    #ifdef was true.
  • Elsewhere: the default rule is None, so nothing is churn, as with the
    old empty body.
  • Callers. setupAudioDeviceChangeMonitor() seeds the two known lists
    with the arrival list, as before. In the handler, added ids are the
    arrival ids not in the known list, in device order; a removal is a
    known id not in the arrival list; the known lists are replaced by the
    arrival list after both are computed; presentIds is every enumerable
    id. Each is the same expression as on main, and no other code called
    the removed helpers.

Tests

tests/audio_device_preference_policy_test.cpp: 37 checks become 63. From
this PR's review (b0f34a17): the change carries the arrival list it was
built from, and the handler stores that list, so the two cannot be derived
with different rules; a second check built from the first one's list
reports no arrival (the #2864 loop).
Each new check was shown red by a mutation of the header, in a scratch
build of the test alone (nothing of it is in this PR):

mutation checks that fail
drop .monitor 2: "a .monitor source is churn", the arrival list
drop pulse_input_loopback 2: its own check, the arrival list
drop auto_null 2: its own check, the arrival list
drop echo-cancel 4
drop combined 2: its own check, the arrival list
loopback prefix becomes "contains" 1: "pulse_input_loopback counts as a prefix only"
every id is churn under the Linux rule 6, among them "a plain device id is not churn"
the rule parameter is ignored 4: the three "no rule" checks and the platform default
the default rule is Linux on this platform 1: "this platform's default rule filters on Linux and nowhere else"
the arrival list is not filtered 3
presentIds is the arrival list (the #6378 regression) 3: "the presence list contains a live churn id", "a live churn device is not missing when another device arrives", "a saved churn device that is enumerable is present to the prompt"
addedIds reads the enumerable list 2: "the arrival list reports the ordinary device only", "a saved churn device connecting is not restored live"
removals are never reported 1: "one device leaving as another arrives is one arrival and a removal"

Docs

docs/user/docs/audio-settings.md: after "switches back to the saved
device when that device connects, without asking", one sentence says that
on Linux a saved virtual device of the audio server is used again the
next time the app starts, not when it reappears.
tools/docs/gen_reference.py --check passes. The docs site build was not
run here.

Checks

Full ctest at b0f34a17, -j8: 762 of 763 passed, 1 failed, also on the
serial rerun.

unified_title_bar_test and vkamp_connection_test passed.
audio_device_preference_policy_test passed.

Every tools/check_*.py --strict that takes no arguments passes, as do
gen_touchpoint_manifest.py --check and gen_seam_probe_table.py --check. check_a11y.py on MainWindow.cpp: 0 findings.
tools/docs/test_log_rules.mjs was not run (no node on this machine).

73, ON8ST

🤖 Generated with Claude Code

on8st and others added 2 commits October 11, 2026 22:16
The hot-plug handler asks two questions of the PC audio device list:
which ids are present, and which ids are arrivals. On Linux the arrival
list leaves out the audio server's churn (monitor sources, loopbacks,
auto-null, echo-cancel and combined devices); the presence list never
does. The helpers that build both lists move from MainWindow.cpp into
AudioDevicePreferencePolicy.h as pure functions over ids, with the
platform rule as a parameter, so every CI lane runs the Linux rule.

audioDeviceListChange() builds one direction's change from the
enumerable ids and the previous arrival list; MainWindow.cpp keeps only
the QAudioDevice to id extraction. No behaviour change intended.

The user docs say when a saved virtual device is used again on Linux.

Refs aethersdr#6378

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
audioDeviceListChange() takes one struct with named fields, so the two
id lists and the two ids cannot be swapped at a call site.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@on8st
on8st requested a review from a team as a code owner October 11, 2026 20:29

@aethersdr-agent aethersdr-agent 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.

Issue fit

Yes. There is no Fixes issue — this answers the two forward notes left on the approving review of #6378 (merged as 7e080e008, which I confirmed is on main, so this is not building on an unmerged prereq). Both notes are delivered: the id-list helpers move out of MainWindow.cpp's anonymous namespace into src/core/AudioDevicePreferencePolicy.h where a socket-free CTest can reach them, and docs/user/docs/audio-settings.md gains the Linux clause that your own #6378 comment promised ("a saved churn-id device that returns is not in addedIds … it is used again at the next launch"). GOVERNANCE.md wants an RFC for architectural change; moving four file-local helpers into an existing header under an existing touchpoint row is not that, and no new issue was needed.

The "no behaviour change" claim is the load-bearing one, and it holds. I checked each of the four expressions against main:

question main this PR
seeded known list audioDeviceIds() → churn-filtered arrivalAudioDeviceIds(enumerated…) → churn-filtered
addedIds filtered ids not in m_known*, device order arrivalIds not in knownArrivalIds, same order
anyRemoved !removedAudioDeviceIds(known, filtered).isEmpty() known id absent from arrivalIds
presentIds enumeratedAudioDeviceIds() → unfiltered change.presentIds = now.enumeratedIds → unfiltered

Same in each row, and the five Linux patterns are copied byte-for-byte. grep over src/ and tests/ finds no remaining caller of the four removed helpers, so nothing was left behind pointing at the old names.

The doc sentence is accurate, not just plausible: AudioEngine.cpp:1989-1998 restores the saved id by walking the full QMediaDevices::audioInputs()/audioOutputs() with no churn filter, so a saved echo-cancel device really is adopted at launch; and because churn ids can never enter addedIds, RestorePreferred can never fire for one mid-session. "Next time AetherSDR starts, not when it reappears" is exactly right.

Scope

Everything in the diff is explained by the two forward notes. Six files, no stragglers.

file what it changes claimed? verdict
src/core/AudioDevicePreferencePolicy.h adds AudioDeviceChurnRule, isLinuxTransientAudioDeviceId, isTransientAudioDeviceId, arrivalAudioDeviceIds, AudioDeviceListSnapshot, audioDeviceListChange yes (forward note 1) in scope
src/gui/MainWindow.cpp deletes the four file-local helpers, calls the header; keeps enumeratedAudioDeviceIds yes in scope
tests/audio_device_preference_policy_test.cpp 37 → 55 expect call sites in two new blocks yes in scope
docs/user/docs/audio-settings.md one sentence, the Linux clause yes (forward note 2) in scope
docs/architecture/aetherd-touchpoint-tags.json extends the existing row's note yes in scope — required by gen_touchpoint_manifest.py --check
docs/architecture/aetherd-touchpoints.md the regenerated manifest line yes in scope — note text matches the JSON exactly, count stays 1 (no new includer)

No new public surface: no protocol verb, no settings key, no CLI flag, no capability field. CHANGELOG.md correctly untouched. Deleted behaviour check: the removed isTransientAudioDeviceId comment named a symptom — "treating them as 'new devices' is what re-fires the dialog in #2864" — and the replacement header comment keeps both the (#2864) reference and the reason ("never a new mic or speaker to offer"), so nothing a future reader needs was dropped with the #ifdef. No guard, early return or confirmation was removed. Preference check: nothing user-visible changes — no default moved, no UI reordered, no confirmation removed. This is a test-reach refactor plus a doc sentence, not a smuggled preference.

Socket tests: none added, modified or removed. The modified test links Qt6::Core only (tests/tests.cmake:3100-3105) and touches no socket, peer process or Fake* class.

Blockers

None.

Nits

All non-blocking.

  • src/gui/MainWindow.cpp:5298-5299 — the arrival list is derived twice, from two independent reads of the default rule.

        m_knownAudioInputIds = arrivalAudioDeviceIds(presentInputIds);
        m_knownAudioOutputIds = arrivalAudioDeviceIds(presentOutputIds);

    audioDeviceListChange() at :5284/:5290 already computed exactly this list internally, and both sites default to kHostAudioDeviceChurnRule, so today they agree and the behaviour is right. The coupling is implicit, though: the day someone passes an explicit rule to audioDeviceListChange() (the parameter exists precisely so they can) and does not also change these two lines, the stored known list and the list arrivals are judged against diverge — and the failure mode is the churn ids counting as arrivals on every check, i.e. the #2864 dialog re-firing, which is the bug this whole filter exists to prevent. Carrying the list it already built on the change makes the two unable to disagree and drops a duplicate pass over the ids:

    // in AudioDeviceListChange, src/core/AudioDevicePreferencePolicy.h
        QList<QByteArray> arrivalIds;  // the churn-filtered ids: next check's knownArrivalIds

    set with change.arrivalIds = arrivalIds; in audioDeviceListChange(), after which the handler stores inputChange.arrivalIds / outputChange.arrivalIds. Two files, so I have not written it out as a drop-in patch.

  • tests/audio_device_preference_policy_test.cpp:222-223 and :240 — &&-joined assertions do not localise a failure.

            expect(isTransientAudioDeviceId("echo-cancel-source") == hostFilters
                       && (arrivalAudioDeviceIds(enumerated) != enumerated) == hostFilters,
                   "this platform's default rule filters on Linux and nowhere else");

    This is the one check that can only go red on a Linux lane, and when it does the [FAIL] line will not say whether the predicate or the list helper disagreed with the host rule. Two expect calls would. Same for arrival.addedIds == … && !arrival.anyRemoved at :240. Cosmetic — the aggregate checks at :276-284 genuinely are one assertion about one struct, and those read fine as written.

  • Coverage lane, not coverage. audio_device_preference_policy_test appears in no ctest -R filter in ci.yml, so it runs only in full-suite.yml — post-merge. The Linux build job did compile the Q_OS_LINUX arm of the header (the lane is green at 921c14f2), but the hostFilters check that reads kHostAudioDeviceChurnRule has still never executed on Linux and will not before merge. Nothing to fix in the PR; worth stating plainly next to the body's "can only fail for the Linux case on a Linux lane", since the body reads as though a lane will catch it.

  • The over-broad substring match is now pinned by a test. expect(isTransientAudioDeviceId("combined", linuxRule), …) at :201 freezes contains("combined"), which also swallows an operator-named PipeWire node like combined-rig. #6378's thread already ruled that a separate issue and I am not reopening it here — just noting that fixing it later now means editing this check, which is the correct outcome for a pinned behaviour but is worth knowing.

Security audit

Required by the harness (82 CodeGuard findings across the audited files). I ran the security-audit subagent over /tmp/aetherclaude/pr-6387 and verified its conclusions against the files. No confirmed findings. Specifics, because "clean" should be checkable:

  • CS-VLN-CMDI-AMP / CS-VLN-CMDI-PIPE at AudioDevicePreferencePolicy.h:52,55-58,61,75,101,106 — I printed those exact lines. They are C++ reference declarators (const QByteArray& id) and ||/&& logical operators in isLinuxTransientAudioDeviceId's return expression. No shell is involved anywhere in the new code. Same class as the known CS-VLN-CMDI-BACKTICK-on-comments false positive: the tags.json hits at :10/:1145 are Markdown-quoted identifiers inside JSON note fields.
  • CS-VLN-PATH-PROC at MainWindow.cpp:6225/:6304 (hardcoded /proc/self/status for the RSS indicator), CG-PATH-001 at :9702-9938, CS-PII-IP at :6917 (224.0.0.1, the IANA local-control multicast group WSJT-X defaults to), and CS-INJ-role_override at :11754 (the C++ override specifier) are all pre-existing lines outside the two hunks this PR touches, shifted only by the ~100 deleted lines.
  • The audit file list is wider than the PR. PanadapterApplet.*, SpectrumOverlayMenu.* and SpectrumWidget.* are not in this PR's six changed files — they arrived on main with #6386 (414082197). Their homoglyph/override hits belong to that change, not this one.
  • Substantively there is no new attack surface to review: the added code is pure functions over QByteArray ids produced by QMediaDevices host enumeration plus two AppSettings strings, with no exec, no path construction, no parser, no network boundary and no raw buffer.

What I tried to break

Findings below are reasoned from the head checkout at /tmp/aetherclaude/pr-6387; I am headless, so nothing here was built or run.

  • The "no behaviour change" claim, expression by expression — the table in Issue fit above. The one I pushed hardest on is anyRemoved: main compared m_known* against the filtered current list, so a churn id disappearing never counted as a removal, and the new loop over knownArrivalIds against arrivalIds preserves that. Had the new code compared against enumeratedIds instead, every churn id would have looked removed on the first check after seeding; it does not.
  • The #6378 Linux regression, re-mutated by hand — I walked presentIds = arrivalIds through the new checks. arrival.presentIds.contains(kEchoCancel) at :245 goes red, as do the liveMissing check at :250 and the acceptedAudioDeviceSelection check at :252. The new block genuinely fails against the defect it is pinning, which is the whole point of the PR; it is not asserting the implementation back to itself.
  • Sibling call sites and dead additions — grep across src/ and tests/ for all four removed helper names plus the six added ones: the only non-header users are MainWindow.cpp:5226-5299 and the test. No second copy of the filter survives elsewhere, and nothing added is unreferenced. The test is registered with add_test at tests/tests.cmake:3105, so it is not an unregistered target.
  • Q_OS_LINUX in a header that includes only <QByteArray>/<QList> — qbytearray.h pulls qglobal.h → qsystemdetection.h, so the macro is defined by the time #ifdef is evaluated. If it were not, kHostAudioDeviceChurnRule would silently become None on Linux and re-open #2864; it is, and the green Linux build lane at 921c14f2 compiled that arm. inline constexpr at namespace scope is one entity, so no ODR hazard from the default argument that reads it.
  • Lifetime and ordering in the handler — addedInputIds/addedOutputIds are now references into inputChange/outputChange, which are const locals living to the end of handleAudioDeviceListChanged() and are still alive when AudioDeviceChangeDialog is constructed with them at :5345. The m_known* and m_knownDefault* writes still land after both changes are built and before either decision is taken, so a re-entrant handleAudioDeviceListChanged() (the dialog's exec() nested event loop, guarded by m_audioDeviceDialogOpen at :5256) cannot see a half-updated pair.
  • The doc claim, against the startup path — read AudioEngine.cpp:1989-1998 rather than trusting the sentence. Unfiltered enumeration at launch, filtered arrivals at runtime: the asymmetry the doc describes is real.
  • What I could not check: anything requiring a run — no real device was unplugged, no Linux lane executed the policy test, and the hot-plug handler still has no test of its own. The body states all three plainly, which is the right disclosure; handleAudioDeviceListChanged() passing the lists correctly into audioDeviceListChange() remains read-not-tested, exactly as the Limits section says.

Recommendation

Approve with nits. This is the cleanest kind of follow-up: it converts a reviewer's forward note into a tested seam without touching behaviour, the mutation table in the body is real rather than decorative (I re-derived the row that matters), and the Limits section discloses the gaps instead of papering over them — including the one check that cannot fail on the author's own machine. Next step: nothing blocking. The arrivalIds-on-the-change nit is worth taking now while the struct is fresh, since the failure it forecloses is the #2864 dialog loop; the rest can be ignored or folded in at the author's discretion.

Thanks for coming back to close the notes rather than leaving them as issues — and for naming the macOS-only build honestly a second time.


🤖 aethersdr-agent · cost: $11.6733 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (87 finding(s))

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6387/docs/architecture/aetherd-touchpoint-tags.json:10
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6387/docs/architecture/aetherd-touchpoint-tags.json:1145
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/AudioDevicePreferencePolicy.h /tmp/aetherclaude/pr-6387/src/core/AudioDevicePreferencePolicy.h:55
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/AudioDevicePreferencePolicy.h /tmp/aetherclaude/pr-6387/src/core/AudioDevicePreferencePolicy.h:56
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/AudioDevicePreferencePolicy.h /tmp/aetherclaude/pr-6387/src/core/AudioDevicePreferencePolicy.h:57
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/AudioDevicePreferencePolicy.h /tmp/aetherclaude/pr-6387/src/core/AudioDevicePreferencePolicy.h:58
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AudioDevicePreferencePolicy.h /tmp/aetherclaude/pr-6387/src/core/AudioDevicePreferencePolicy.h:52
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AudioDevicePreferencePolicy.h /tmp/aetherclaude/pr-6387/src/core/AudioDevicePreferencePolicy.h:61
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AudioDevicePreferencePolicy.h /tmp/aetherclaude/pr-6387/src/core/AudioDevicePreferencePolicy.h:75
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AudioDevicePreferencePolicy.h /tmp/aetherclaude/pr-6387/src/core/AudioDevicePreferencePolicy.h:101
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AudioDevicePreferencePolicy.h /tmp/aetherclaude/pr-6387/src/core/AudioDevicePreferencePolicy.h:106
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:9702
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:9814
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:9936
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:9937
  • [MEDIUM] CG-PATH-001 — Potential path traversal in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:9938
  • [HIGH] CS-VLN-PATH-PROC — Vulnerability: CS-VLN-PATH-PROC in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:6225
  • [HIGH] CS-VLN-PATH-PROC — Vulnerability: CS-VLN-PATH-PROC in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:6304
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:539
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:3610
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:537
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:1230
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:1754
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:1846
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:1923
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:1965
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:2000
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:2295
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:3552
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:3643
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:6167
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:7973
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:8038
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:8112
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:8197
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:8378
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:8581
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:9803
  • [LOW] CS-PII-IP — PII detected: ip_address in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:6917
  • [CRITICAL] CS-INJ-role_override — Prompt injection: role override attempt in src/gui/MainWindow.cpp /tmp/aetherclaude/pr-6387/src/gui/MainWindow.cpp:11754

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $11.8393 · model: claude-opus-5

Review of aethersdr#6387, nit 1: the hot-plug handler derived the arrival list a
second time for the stored known ids, from its own read of the default
churn rule. AudioDeviceListChange gains arrivalIds, set by
audioDeviceListChange(), and the handler stores that list, so the stored
ids and the ids arrivals are judged against come from one rule.

The test checks arrivalIds under the Linux rule and under none, and that
a second change built from the first one's arrivalIds reports no arrival
and no removal (aethersdr#2864).

Review nit 2: the host-rule check and the addedIds / anyRemoved check
are two expect calls each, so a failure names the half that broke.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@on8st

on8st commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Fixed in b0f34a17, a new commit on top (no rebase).

  • The arrival list is derived once. AudioDeviceListChange carries arrivalIds, set by audioDeviceListChange(), and the handler stores that. The two can no longer be derived with different rules. Seven new checks, 63 in all: the list is the filtered ids under the Linux rule and every id under none, and a second check built from the first one's list reports no arrival and no removal, which is the Linux Pipewire causes pop-up for device changes #2864 loop. Three mutations each turn the right ones red.
  • The two &&-joined expectations are split, so a red says which half broke.
  • The body now says it plainly: no per-PR lane runs this test. The Linux build job compiles the Q_OS_LINUX arm; the check that reads the host default first executes on Linux in full-suite.yml, after merge.

Not changed: contains("combined"), as #6378's thread left it for its own issue. You are right that the test now pins it.

Full ctest at b0f34a17 on macOS: 762 of 763; the one that fails is the clean-main failure #6008.

73, ON8ST

@rfoust rfoust self-assigned this Oct 12, 2026

@rfoust rfoust left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Issue fit

Approve with nits; no blockers. This delivers both forward notes from merged PR #6378: making the device-list helpers reachable by a socket-free test and documenting the Linux exception. Presence still uses every enumerated ID; arrivals and removals still use the filtered list. The extraction preserves behavior.

Scope

Everything in the diff is explained by the stated follow-up. All three commits are dated October 11; no unrelated commit or preference change was found.

File/group Change Claimed? Verdict
AudioDevicePreferencePolicy.h / MainWindow.cpp Extract helpers and preserve handler wiring Yes In scope
audio_device_preference_policy_test.cpp Exercise filtering, presence and successive changes Yes In scope
audio-settings.md Document Linux exception Yes In scope
Touchpoint tags / generated manifest Describe expanded policy Yes In scope

No new external protocol/settings/capability surface. No CHANGELOG entry. No socket-owning test was added, modified or removed.

Blockers

None.

Nits (non-blocking)

  1. Narrow the Linux documentation wording to the subset of virtual devices matched by the ID filter. See the inline comment on audio-settings.md:28.
  2. Replace the assertion that each CI lane tests this rule with wording about the test being able to exercise it on any host. See the inline comment on AudioDevicePreferencePolicy.h:54. The PR body already describes the actual CI boundary accurately.

These are optional wording improvements, not conditions for approval or merge.

Verification and attacks

Reviewed head b0f34a1. GitHub merge ref c96e896 was older than fetched main c2e543c, so I constructed a clean merge with that main in an independent scratch checkout; tested tree a631f4575ad8c798183c9d7c1a2a7610271fbda3.

  • ARM64 macOS app build passed with pinned Qt 6.12 and RADE enabled. Verified ARM64 host/system/executable and absence of RNNoise x86 sources. macOS icon packaging required normal system access. The demo launch required the runtime framework path pinned to Qt 6.12 and a session-only bridge token to avoid the local Keychain lookup.
  • Registered audio_device_preference_policy_test CTest passed, 63/63 checks; the current-main baseline test passed 37/37.
  • Six deliberate mutations failed as expected: filtered presence (3 failures), unfiltered arrivals (3), omitted removals (1), incorrect stored arrival list (2), ignored platform rule (6), and loopback substring instead of prefix (1).
  • An independent comparison with the old list logic found equivalent results across 134,162 current/previous/platform combinations, including empty IDs, duplicates and reordered lists. Repeated unchanged lists and simultaneous arrival/removal survived.
  • Engine-boundary, capability, command-plane and touchpoint checks produced identical findings on current main and the constructed merge. Generated documentation references and log-rule checks passed.
  • Isolated offscreen demo instance PID 81294, explicit review socket, TX disabled: connect list confirmed family sim and serial DEMO-0001; connect/get radio reached one slice and one pan with transmitting=false; get audio reported operational endpoints with NoError; the widget tree contained no AudioDeviceChangeDialog. After sim disconnect, get radio returned connected=false, connectState=idle, transmitting=false. The owned instance was stopped; the operator's instance was untouched.

Runtime smoke evidence is not a Linux hot-plug test. Linux host-default selection, physical unplug/replug and the complete hot-plug handler were not exercised. Their behavior was checked through the extracted policy and caller comparison. No model-invocable automated review pass was available; I performed the second lifecycle/failure-mode pass manually and verified the existing bot alerts rather than adopting them.

Current head checks are green. Recommendation: approve with nits.

Comment thread docs/user/docs/audio-settings.md
Comment thread src/core/AudioDevicePreferencePolicy.h

@rfoust rfoust left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved at b0f34a1 after the posted review and local verification. Both inline wording suggestions are optional and non-blocking; I am resolving my own nit threads to permit the authorized merge. No code changes are required for approval.

@rfoust
rfoust merged commit aa0dd0e into aethersdr:main Oct 12, 2026
7 checks passed
rfoust added a commit that referenced this pull request Oct 12, 2026
…VIII. (#6403)

Refs #6387, #6388

Answers rfoust's three optional notes on the approving reviews of #6387
and
#6388.

## Limits

- One documentation sentence, one comment and five include lines. No
  behaviour change: no statement, declaration or test registration is
touched, and the five headers were already reached through `<QtTest>`
and
  `<QApplication>`.
- Built and run on one Mac (macOS, Qt 6.12.0). Per-PR CI compiles these
tests but does not run them. The Linux rule the sentence describes is
not
run on Linux here; it is exercised by
`audio_device_preference_policy_test`
  on this host.
- The documentation now names the five ID patterns used by the Linux
hot-plug
filter. The source matches substrings or a prefix, including custom
names;
  it does not establish any audio server's default device names.
- The include pass covers the Qt classes #6387 and #6388 named first in
the
files they touched. Transitive includes that were there before them are
  listed below and left alone.

## Change

1. #6387, `docs/user/docs/audio-settings.md:28`:

   > this reads as applying to all audio-server virtual devices, but the
> exception depends on five ID patterns. A custom-named virtual device
   > outside those patterns restores live.

The reviewer's suggested sentence was the starting point. The follow-up
commit names all five matching ID patterns, including monitor sources
and
custom names. `isLinuxTransientAudioDeviceId` leaves those ids out of
the
arrival list, so a saved device matching one is never seen arriving; at
   startup the saved id is looked up among all enumerable devices.
2. #6387, `src/core/AudioDevicePreferencePolicy.h:54`:

> this comment should describe test portability without claiming every
   > lane runs it.

   The suggested wording is taken as written.
3. #6388, `tests/radio_setup_headphone_output_gate_test.cpp:38`:

   > Include it directly rather than relying on QtTest to supply it
   > transitively, per the repository explicit-include convention.

`<QSignalBlocker>` is included. The same pass over both PRs found four
   more Qt classes #6388 named first in that file without their header:
`QColor`, `QCoreApplication`, `QList`, `QStringList`. They are included
   too.

Found by the same pass and not changed, because the use predates both
PRs:
`QString` and `QWidget` in that test; `QString` in
`mixer_control_availability_test.cpp`, `RadioSetupDialog.cpp` and
`TitleBar.cpp`; `QByteArray` and `QList` in `MainWindow.cpp`. The other
files of #6387 include what they name.

## Evidence

Full ctest before the documentation-only follow-up, at `ac0dd8f6`,
`-j8`:
770 of 772 passed (4 skipped: the
`nr_stereo_independence_*` set), 2 failed:

- `peripheral_auth_dialog_interaction_test`: "dialog 960x680, viewport
658,
page minimum 784" (#6008); fails on clean main on this Mac, also on the
  serial rerun. This diff touches no dialog file.
- `vkamp_connection_test`: one of its two known checks, "commands after
a
disconnect are dropped, not queued" (#5254), at `-j8`; it passed on the
  serial rerun.

`radio_setup_headphone_output_gate_test`,
`audio_device_preference_policy_test` and
`mixer_control_availability_test`
passed. `tools/docs/gen_reference.py --check`,
`check_test_registration.py
--strict` and `gen_touchpoint_manifest.py --check` pass. At the
follow-up
commit `f7dd95c9`, `npm ci && npm run build` in `docs/user/` passed, and
the
source checks remained green.

73, ON8ST

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Robbie Foust <rfoust@gmail.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